CLOVIS
10/30/2024, 8:59 PMmyLib.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:
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 :/Matthew Feinberg
10/30/2024, 11:08 PMFooOptions
and BarOptions
also interfaces.
Then put all the private stuff into internal class FooOptionsImpl : FooOptions
Then you can use delegation:
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.CLOVIS
10/31/2024, 8:44 AMMatthew Feinberg
10/31/2024, 9:55 AMinterface FooOptions {...}
fun FooOptions( block: FooOptionsBuilder.()->Unit ) : FooOptions { ... }
val options : FooOptions = FooOptions {
// Use a builder DSL here
}
2. Have mutable and immutable interfaces
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:
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 🙂CLOVIS
10/31/2024, 10:07 AMWell, for my projects, immutable data is critical […]. Your use cases may be very different from mineIn 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.
Matthew Feinberg
10/31/2024, 10:26 AMfun foo( options: FooOptions ) { ... }
fun foo( options: FooOptionsBuilder.()->Unit ) = foo( FooOptions(options) ) // Convenience function
With that, you can simplify...
myLib.foo(FooOptions { option1(…) })
myLib.bar(BarOptions { option2(…) })
....can be rewritten as....
myLib.foo({ option1(…) })
myLib.bar({ option2(…) })
...or even...
myLib.foo { option1(…) }
myLib.bar { option2(…) }
Not sure if that works for your use-case, though.Matthew Feinberg
10/31/2024, 10:28 AMfun foo( options: FooOptions ) { ... }
fun foo( options: FooOptions.()->Unit ) = foo( FooOptions().apply(options) ) // Convenience function