I have many classes that are "configuration classe...
# library-development
c
I have many classes that are "configuration classes". Ideally, each field of these classes would be a default parameter, but adding a new one would be a breaking change, so they're in classes instead. For example;
Copy code
myLib.foo(FooOptions { … })
myLib.bar(BarOptions { … })
I also have many options that are common between multiple types. What's the best way to avoid duplicating all common options between all types? So far, my best candidate is:
Copy code
interface CommonOptions {
    fun option1(…)
    fun option2(…)
}

class FooOptions : CommonOptions {
    private var option1 …
    private var option2 …
    private var option3 …

    override fun option1(…) { option1 = … }
    override fun option2(…) { option2 = … }
    fun option3(…) { option3 = … }
}

class BarOptions : CommonOptions {
    private var option1 …
    private var option2 …
    private var option4 …

    override fun option1(…) { option1 = … }
    override fun option2(…) { option2 = … }
    fun option4(…) { option4 = … }
}
and, sure, it works, but it's very verbose :/
👀 2
m
What I normally do in this kind of situation is make
FooOptions
and
BarOptions
also interfaces. Then put all the private stuff into
internal class FooOptionsImpl : FooOptions
Then you can use delegation:
Copy code
interface FooOptions : CommonOptions { ... }

class FooOptionsImpl(
   val commonOptions: CommonOptions = CommonOptionsImpl()
) : CommonOptions by commonOptions {
    // foo options here
}
This allows you to add common options without touching any of the derived class code.
👍 1
c
Ah, you're right, that would simplify it. Additional question: for such an 'options' class, do you think it is more idiomatic: • that the options is mutable (like a builder) • that the options is immutable (like Compose's Modifier) I think immutable would be a bit nicer, but it makes such delegation more complex
m
Well, for my projects, immutable data is critical (the data structures generated by the UI end up being used in a GPU-based renderer that can render multiple frames concurrently in different threads, so the only way to do that sanely is to have immutable data). Your use cases may be very different from mine, so bearing that in mine, for my particular requirements, I usually choose one of these two approaches: 1. Have a builder that returns something immutable
Copy code
interface FooOptions {...}
fun FooOptions( block: FooOptionsBuilder.()->Unit ) : FooOptions { ... }

val options : FooOptions = FooOptions {
   // Use a builder DSL here
}
2. Have mutable and immutable interfaces
Copy code
interface FooOptions : CommonOptions { ... }

interface MutableFooOptions : MutableCommonOptions { ... }

val options : FooOptions = MutableFooOptions().apply {
    // mutate things here however you like
}.asImmutable()
Or if you need "soft immutability" like
List<..>
where the caller is "trusted" not to cast it to something mutable, you could also do:
Copy code
interface FooOptions : CommonOptions { ... }

interface MutableFooOptions : MutableCommonOptions { ... }

val options : FooOptions = MutableFooOptions().apply {
    // mutate things here however you like
} as FooOptions
Which one is best? It really depends on your use-case (and some other entirely different option may be even better for what you're doing). But for example, in my case... I'm using Compose (the low-level stuff like Applier, etc. not to be confused with the high level UI stuff that's also, confusingly, called "Compose") so I need to have a mutable tree of nodes that the Applier works with, but that can be transformed into actually immutable data. So for that, I use the
asImmutable()
approach. And for anything that doesn't need to be touched by an Applier, I use builder DSLs. Buy your milage my vary. Hopefully this gives you some ideas though 🙂
c
Well, for my projects, immutable data is critical […]. Your use cases may be very different from mine
In my situation, the option classes are really just used as parameters to functions, so hopefully escape analysis will optimize them away entirely anyway. Since I expect them to be constructed and immediately discarded, there is not much risk of concurrency here. However, having them be immutable means users can create "default instances" with common options they want to reuse, which is interesting.
m
Yeah, in that case, my approach sounds like overkill. Maybe having the classes mutable under-the-hood but exposing write-only builders may be an interesting option. Actually, if you use the builder approach, you can have variations of your library functions that take the builder lambdas directly. For example...
Copy code
fun foo( options: FooOptions ) { ... }
fun foo( options: FooOptionsBuilder.()->Unit ) = foo( FooOptions(options) )  // Convenience function
With that, you can simplify...
Copy code
myLib.foo(FooOptions { option1(…) })
myLib.bar(BarOptions { option2(…) })
....can be rewritten as....
Copy code
myLib.foo({ option1(…) })
myLib.bar({ option2(…) })
...or even...
Copy code
myLib.foo { option1(…) }
myLib.bar { option2(…) }
Not sure if that works for your use-case, though.
Actually, you don't even need the builder...
Copy code
fun foo( options: FooOptions ) { ... }
fun foo( options: FooOptions.()->Unit ) = foo( FooOptions().apply(options) )  // Convenience function