I'm working on a text GUI library which comes with builders for UI components (Button
s, Panel
s, etc). I have a base class for these builders which I'd like to redesign so that meaningless component states can't be represented with the builder. Right now the API looks like this (over simplified):
interface ComponentBuilder<T : Component, U : ComponentBuilder<T, U>> : Builder<T> {
// aligns a component within a grid (only if it is not positioned directly)
fun withAlignmentWithin(tileGrid: TileGrid, componentAlignment: ComponentAlignment): U
// sets the position of a component directly (only if it is not aligned
fun withPosition(x: Int, y: Int): U
// sets the title of a component (only applicable if it is wrapped in a box)
fun withTitle(title: String): U
// defines the type of box we use for decorating the component with
fun withBoxType(boxType: BoxType): U
// wraps the component with a box (only if no decoration renderers are set)
fun wrapWithBox(): U
// wraps the component with a shadow (only if no decoration renderers are set)
fun wrapWithShadow(): U
// sets the given decoration renderers (only if we dont use box/shadow wrapping)
fun withDecorationRenderers(vararg renderers: ComponentDecorationRenderer): U
}
and it can be used like this:
Components.panel()
.withSize(10, 10)
.withPosition(5, 5)
.wrapWithBox()
.wrapWithShadow()
.withTitle("Test")
.withBoxType(BoxType.SINGLE)
.build()
and the result looks like this:
The problem is that the builder can get into a meaningless state like this:
Components.panel()
.withTitle("Test") // meaningless, we have no box
.withBoxType(BoxType.SINGLE) // same as above
.build()
The problem is that the builder is supposed to be a state machine where some of the methods above can be an initial state, but when I move into one of those states it determines where I can go from there (for example I should only see title/box type if I moved into the wrap with box state first.
I tried to write a prototype for this but I realized that this leads into a combinatorial complexity boom:
interface ComponentBuilder<T : Component, U : ComponentBuilder<T, U>> : Builder<T> {
fun withAlignmentWithin(tileGrid: TileGrid, componentAlignment: ComponentAlignment): AlignedComponentBuilder<T, U>
fun withPosition(x: Int, y: Int): PositionedComponentBuilder<T, U>
fun withDecorationRenderers(vararg renderers: ComponentDecorationRenderer): DecoratedComponentBuilder<T, U>
fun wrapWithBox(wrapWithBox: Boolean = true): WrappedComponentBuilder<T, U>
fun wrapWithShadow(wrapWithShadow: Boolean = true): WrappedComponentBuilder<T, U>
}
interface DecoratedComponentBuilder<T : Component, U : ComponentBuilder<T, U>> : Builder<T> {
fun withAlignmentWithin(tileGrid: TileGrid, componentAlignment: ComponentAlignment): AlignedDecoratedComponentBuilder<T, U>
fun withPosition(x: Int, y: Int): PositionedDecoratedComponentBuilder<T, U>
fun withDecorationRenderers(vararg renderers: ComponentDecorationRenderer): DecoratedComponentBuilder<T, U>
}
interface AlignedDecoratedComponentBuilder<T : Component, U : ComponentBuilder<T, U>> : Builder<T> {
fun withAlignmentWithin(tileGrid: TileGrid, componentAlignment: ComponentAlignment): AlignedDecoratedComponentBuilder<T, U>
fun withDecorationRenderers(vararg renderers: ComponentDecorationRenderer): AlignedDecoratedComponentBuilder<T, U>
}
interface WrappedComponentBuilder<T : Component, U : ComponentBuilder<T, U>> : Builder<T> {
fun withAlignmentWithin(tileGrid: TileGrid, componentAlignment: ComponentAlignment): WrappedAlignedComponentBuilder<T, U>
fun withPosition(x: Int, y: Int): WrappedPositionedComponentBuilder<T, U>
fun withTitle(title: String): WrappedComponentBuilder<T, U>
fun withBoxType(boxType: BoxType): WrappedComponentBuilder<T, U>
fun wrapWithShadow(wrapWithShadow: Boolean = true): WrappedComponentBuilder<T, U>
}
interface AlignedComponentBuilder<T : Component, U : ComponentBuilder<T, U>> : Builder<T> {
fun withDecorationRenderers(vararg renderers: ComponentDecorationRenderer): U
fun wrapWithBox(wrapWithBox: Boolean = true): WrappedComponentBuilder<T, U>
fun wrapWithShadow(wrapWithShadow: Boolean = true): WrappedComponentBuilder<T, U>
}
interface PositionedComponentBuilder<T : Component, U : ComponentBuilder<T, U>> : Builder<T> {
fun withDecorationRenderers(vararg renderers: ComponentDecorationRenderer): PositionedDecoratedComponentBuilder<T, U>
fun wrapWithBox(wrapWithBox: Boolean = true): WrappedPositionedComponentBuilder<T, U>
fun wrapWithShadow(wrapWithShadow: Boolean = true): WrappedPositionedComponentBuilder<T, U>
}
interface PositionedDecoratedComponentBuilder<T : Component, U : ComponentBuilder<T, U>> : Builder<T> {
fun withDecorationRenderers(vararg renderers: ComponentDecorationRenderer): PositionedDecoratedComponentBuilder<T, U>
}
interface WrappedPositionedComponentBuilder<T : Component, U : ComponentBuilder<T, U>> : Builder<T> {
fun withTitle(title: String): WrappedPositionedComponentBuilder<T, U>
fun withBoxType(boxType: BoxType): WrappedPositionedComponentBuilder<T, U>
fun wrapWithShadow(wrapWithShadow: Boolean = true): WrappedPositionedComponentBuilder<T, U>
}
interface WrappedAlignedComponentBuilder<T : Component, U : ComponentBuilder<T, U>> : Builder<T> {
fun withAlignmentWithin(tileGrid: TileGrid, componentAlignment: ComponentAlignment): WrappedAlignedComponentBuilder<T, U>
fun withTitle(title: String): U
fun withBoxType(boxType: BoxType): U
fun wrapWithShadow(wrapWithShadow: Boolean = true): U
}
My question is: how can I write a builder API which retains its integrity but avoids the complexity boom above? I might add some other options later so it needs to be scalable.
I'm working with Kotlin so any answer should be within the bounds of what's possible with Java and/or Kotlin.
Components.panel().withBox(new box().withTitle("Test"))
. This would make it impossible to add a title to a component without a box. – John Wu Jun 4 '19 at 22:55