Kotlin DSL | Coding a DSL: 3 — The ‘Triangle’ and ‘Rhombus’ objects
Building and adding ‘Triangle’ and ‘Rhombus’ objects
You can go to any article of this series by clicking on one of the links below:
Kotlin DSL
- Introduction
- Base knowledge to build a DSL in Kotlin — Part 1
- Base knowledge to build a DSL in Kotlin — Part 2
- Codebase: Project Shapes-DSL
- Coding a DSL: 1 — Package structure and the ‘Panel’ object
- Coding a DSL: 2 — The ‘Square’ object
- Coding a DSL: 3 — The ‘Triangle’ and ‘Rhombus’ objects
- Coding a DSL: 4 — The ‘Empty Space’ and the ‘Composed Shape’ object
- Coding a DSL: 5 — plus and minus operators and inline functions
- Coding a DSL: 6 — The @DslMarker annotation
- Experimenting and conclusions
In the previous article, we defined the square
function which creates and adds Square shapes to the Panel through the SquareBuilder
class. At the end of the previous article, the main
method’s content looked like this:
Building the ‘Triangle’ object
Repeating what we did for the Square shape in the previous article, we are going to change the Triangle instantiation and its addition to the Panel by replacing the following instructions…
…to make them look like this:
square
and triangle
functions are practically identical so let’s move forward a little bit faster using what we already have learned. We can assume that triangle
is an extension function of the Panel
class, just like square
function. Instead of a Square object, it should return a Triangle object. Its signature should look as follows:
fun Panel.triangle(?): Triangle
💬 Again, I temporarily replace the lambda function type with a question mark since we don’t know it yet.
Since the triangle
function follows the same pattern as the square
function, we can assume that it applies the builder design pattern to create Triangle objects, so let’s create a new class for it.
Create a new Kotlin class named TriangleBuilder
inside console_shapes_dsl.builders
package. Its code should look like this:
💬 Don’t worry about duplicated code, we are going to abstract their common behavior to a parent class at the end of this article.
The lambda function type that we are looking for is analogous to the lambda function type of the square
function. Let’s define it like this: TriangleBuilder.() -> Unit
.
Finally, the signature of the triangle
function looks as follows:
fun Panel.triangle(init: TriangleBuilder.() -> Unit): Triangle
Go to the console_shapes_dsl.external
package and add the following code to the Extension.kt
file:
fun Panel.triangle(init: TriangleBuilder.() -> Unit): Triangle {
}
We know that this function has to do the following:
— Create a Triangle object through a TriangleBuilder.
— Add the new Triangle object to the Panel.
— Return the new Triangle object.
Its code should look like this:
fun Panel.triangle(init: TriangleBuilder.() -> Unit): Triangle {
val builder = TriangleBuilder()
builder.init()
val triangle = builder.build()
this.addShape(triangle)
return triangle
}
Finally, we can reduce all its code to a single line with scope functions like apply
and also
:
▶️ If you run the code, you’ll see that it works again.
So far, the main
method’s content looks like this:
Building the ‘Rhombus’ object
Now that we have created both functions square
and triangle
, we can create the rhombus
function the same way.
Our goal now is to replace the following instructions…
…to make them look like this:
As we already have learned, we will need an intermediary object to create Rhombus objects, so let’s create its class.
Create a new Kotlin class named RhombusBuilder
inside console_shapes_dsl.builders
package. Its code should look like this:
Now let’s create the rhombus
function taking square
and triangle
functions as a guide. We can tell that rhombus
function is an extension function of the Panel
class and, at the same time, it is a high-order function whose lambda function type corresponds to RhombusBuilder.() -> Unit
. It also has to return a Rhombus object. Its signature should be as follows:
fun Panel.rhombus(init: RhombusBuilder.() -> Unit): Rhombus
Go to the console_shapes_dsl.external
package and add the following code to the Extension.kt
file:
fun Panel.rhombus(init: RhombusBuilder.() -> Unit): Rhombus {
}
Just like square
and triangle
functions, rhombus
function has to do the following:
— Create a Rhombus object through a RhombusBuilder.
— Add the new Rhombus object to the Panel.
— Return the new Rhombus object.
Its code should look like this:
fun Panel.rhombus(init: RhombusBuilder.() -> Unit): Rhombus {
val builder = RhombusBuilder()
builder.init()
val rhombus = builder.build()
this.addShape(rhombus)
return rhombus
}
▶️ If you run the code, you’ll see that it works again.
And just like we did before, let’s reduce its code to a single line with the scope functions apply
and also
:
Go to the main
method and make sure everything works. Its code should look like this:
We could leave all 3 builders just like that, however, as a good practice we should abstract its common behavior to a parent class.
Abstracting builders’ common behavior
As a good practice we should define a constant that stores the default character to be used in case their property char
is not set during their construction.
We can summarize their common code to the following:
— Definition of their property lines
set to 0
.
— Definition of their property char
set to the default character.
— Definition of their member function build
that returns a Shape
object.
Go to the console_shapes_dsl.builders
package and create a new abstract Kotlin class named ShapeBuilder
. Its implementation should look like the following code:
We mark its member function build
as abstract
so every subclass must define its own behavior.
Now go to the SquareBuilder
class and make it extend ShapeBuilder
class. Then remove its lines
and char
properties. Finally, modify its member function build
as follows:
override fun build() = Square(lines, char)
SquareBuilder
class should look like this:
💬 Make sure everything works before modifying the other 2 builders.
Now let’s modify TriangleBuilder
and RhombusBuilder
classes the same way:
TriangleBuilder
:
RhombusBuilder
:
We have reached the end of this article. Now we only have space
and composed
functions definition left. These functions add “empty spaces” and composed shapes to the Panel respectively. We are going to create them in the next article and we will apply other Kotlin features to create a more idiomatic DSL.
Continue with the next article Coding a DSL: 4 — The ‘Empty Space’ and the ‘Composed Shape’ object
💬 If you enjoyed this article, you can show your appreciation by buying me a coffee at the link below. Thanks for reading and for your support.
- 📖 You can learn about High-Order Functions on Kotlin’s official documentation at https://kotlinlang.org/docs/lambdas.html
- 📖 You can learn about Lambdas with Receivers on Kotlin’s official documentation at https://kotlinlang.org/docs/lambdas.html#function-literals-with-receiver
- 📖 You can learn about Scope Functions on Kotlin’s official documentation at https://kotlinlang.org/docs/scope-functions.html