Feels like I'm missing something with doing a buil...
# getting-started
k
Feels like I'm missing something with doing a builder pattern where I need to have a couple of levels of hierarchy of builders, with each layer exposing a few functions that add to the internal state and then return the builder instance for a fluent pattern. Code in thread, any suggestions welcome
Copy code
open class BaseBuilder<out B: BaseBuilder<B>> {
    protected var _foo: Int = 0

    fun foo(foo: Int): B {
        this._foo = foo
        return this as B
    }
}

class Builder: BaseBuilder<Builder>() {
    protected var _bar: Int = 0

    fun bar(bar: Int): Builder {
        this._bar = bar
        return this
    }
}

fun main() {
    Builder().bar(1).foo(2)
    Builder().foo(1).bar(2)
}
In the
return this as B
in the
BaseBuilder.foo
, the compiler says
Unchecked cast of 'BaseBuilder<B (of class BaseBuilder<out B : BaseBuilder<B>>)>' to 'B (of class BaseBuilder<out B : BaseBuilder<B>>)'.
If I remove
as B
, the compiler says that there is a return type mismatch. And if I change the
foo
method in the
BaseBuilder
to return
BaseBuilder<B>
and that function to return
this
, then the chain of
Builder().foo(1).bar(2)
does not compile because
bar
is not seen on the
BaseBuilder
The original code snippet compiles with that warning, and runs with no issues, but I feel like there should be a way to express this chaining where different links live at different class levels with no warnings
c
What are you trying to do? What's the semantic difference of the order between
foo
and
bar
?
k
No order, and I'd like to not impose any order on the usage
c
Any reason why it has to be a fluent pattern?
k
I'm porting a Java API that looks like this
Where the different
add
methods come from different levels in the builder class hierarchy
c
Do you need source/binary compatibility with the Java API? You could do this much simpler if you don't care
k
I don't care
Maybe a DSL thing would be a better match
c
Copy code
open class BaseBuilder {
    protected var _foo: Int = 0

    fun foo(foo: Int) {
        _foo = foo
    }
}

class ABuilder : BaseBuilder() {
    protected var _bar: Int = 0

    fun bar(bar: Int) {
        _bar = bar
    }
}

fun buildBase(block: BaseBuilder.() -> Unit): Base =
    …

fun buildA(block: ABuilder.() -> Unit): A =
    …
Using your example, it would look like
Copy code
buttonBar {
    button(newButton)
    relatedGap()
    button(editButton)
    relatedGap()
    button(deleteButton)
}
The order doesn't matter, no need for self-types and stuff
If you absolutely do want to use a fluent API, and it's important that the order doesn't matter, then you have to go full self-type emulation and it's a pain. Here's a full example: • Parent: gitlab.com/opensavvy/ktmongo/-/blob/…/Pipeline.kt?ref_type=heads#… • Two intermediate builders: gitlab.com/opensavvy/ktmongo/-/blob/…/PipelineType.kt?ref_type=heads • and then ALL implementations must re-override everything, for example: gitlab.com/opensavvy/ktmongo/-/blob/…/MongoAggregationPipeline.kt?ref_type=heads#… • but then even its children need to do so as well: gitlab.com/opensavvy/ktmongo/-/blob/…/CoroutineMon…?ref_type=heads#… and as you can see, the very leaf of the hierarchy still needs the cast
Note that the DSL also makes it much more clear that there is mutation involved, whereas the builder doesn't (in fact, in the KtMongo example, the builder is immutable)
k
Thanks for the pointers! I think I was stuck too much with the Java things in this part of the port.
c
No worries!
Kotlin a DSL language, not a Builder language
Btw if you're near the UK and want to know more… @simon.vergauwen and I will be speaking about DSLs at kotlinleeds.co.uk
🔥 2
y
For your original question, the cast is unsafe because code like this is possible:
Copy code
class FakeBuilder: BaseBuilder<Builder>()
Which gives you a CCE at runtime. Of course, if you know that your implementations are reasonable, then it's fine. The DSL approach is way better thought
k
Yes, it looks like this now:
🎉 1
I'll probably move the
padding
call to be at the top level and not as a DSL function, since it doesn't make sense to call it more than once. Even though in the original Java implementation it's at the level of the fluent API which was available to call more than once as well.
I'll probably remove the word "Builder" from that function name, and move padding to be a parameter right next to
modifier
y
Looking very compose-y. You can actually use Compose the compiler plugin without using Compose UI, as demonstrated here This also reminds me of the IntelliJ platform UI DSL
c
Another big advantage of DSLs is that you can use arbitrary Kotlin code in them:
Copy code
ButtonStackBuilder(…) {
    for (button in listOf("Start", "Pause", "Stop")) {
        button(button)
        unrelatedGap()
    }
}
This would be much more difficult with a builder!
➕ 2