Kotlin DSL | Codebase: Project Shapes-DSL
Initial code description
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 this article, I’ll show you the codebase on which we will build a DSL. In order to build a DSL, it is imperative to know some Kotlin features that are applied intensively. These features are:
- Lambdas
- Higher-order functions
- Extension functions
- Lambda with receiver
- Scope functions
- Builder design pattern
In addition, for a more idiomatic DSL, other Kotlin features will also be applied. These features are:
If you’re not familiar with any of them, I suggest you to click on it and take a look before moving forward.
Project description
The code that I will show you in this article consists of a console application that prints out geometric shapes. Specifically, there will be 4 of them: Square, Triangle, Rhombus and Composed Shape.
All shapes will be able to be merged with Union and Intersection operations, resulting in a composed shape. These operations are analogous to the Set operations in mathematics. Union operation will result in a shape that covers all the space occupied by both shapes. Intersection operation will result in a shape that covers only the space that both shapes have in common.
The idea is add geometric shapes of different sizes to a Panel and print them all top-down, line by line, arranged side by side, simulating a physical printer that prints on paper.
For example, the code below…
…prints out to the console the following:
Our goal is to build a DSL on top of this code so creating a Panel and its Shapes becomes clearer and more idiomatic:
Codebase
I will present the initial code and although it is quite short and simple, I suggest you to keep reading to ensure you haven’t missed any details.
🔗 You can find the initial code on my GitHub account:
Make sure to download the project and checkout to the master
branch where you will find the codebase which won’t be modified, except for the main
method since our intention is to change its syntax for a more idiomatic one using our DSL.
The dsl
branch contains the final code which is the result of what we are going to do during these next articles. The ultimate goal is that you learn how to build a DSL on top of existing code, especially on code that you can’t modify, either because it is code that is part of a module that you don’t have access to or permission to modify, or because it is packaged in a library. At the same time, you will discover what many libraries that we include in our projects have under the hood.
Project Package Organization
If you open the project and display its directory tree structure under the ‘Project’ view option, you will see the following:
Under the console_shapes
package you will find 2 packages: container
and shapes
. The container
package contains only the Panel
class which will behave as a container for all the geometric shapes. The shapes
package contains classes that represent geometric shapes able to be added to the Panel.
‘Shapes’ package
Our project has 4 geometric shapes of which 3 are simple shapes and 1 is a composed shape. Square
, Triangle
and Rhombus
shapes correspond to the simple ones. ComposedShape
corresponds to the composed one since it can be built only by merging 2 shapes, whether they are both simple, both composed or one simple and one composed. Additionally, we have the Space
shape that represents an “empty” space within the Panel.
- Shape.kt
The abstract class Shape
is the base class that determines a common behavior for all geometric shapes.
WHITE_SPACE
: establishes the character that will take place on every empty space of the shape.grid
: corresponds to a matrix of Char — array of CharArray — which represents the shape itself. Its dimension will be determined by the number of lines established during every concrete class instantiation, as well as its characters.size
: defines a variable with custom accessor without backing field that returnsgrid
size.
🔗 If you’re starting with Kotlin and you don’t know how custom accessors and backing fields work, take a look at the section Getters and setters of Kotlin’s official documentation at: https://kotlinlang.org/docs/properties.html#getters-and-setters
- Square.kt
Square
class extends from Shape
class and represents a Square shape. During its instantiation, it receives the following parameters:
— lines
: determines the grid
size.
— char
: determines the grid
character.
Both, grid
width and height, will be the same size.
- Triangle.kt
Triangle
class extends from Shape
class and represents a Triangle shape. During its instantiation, it receives the following parameters:
— lines
: determines the grid
size.
— char
: determines the grid
character.
grid
width will double its height.
- Rhombus.kt
Rhombus
class extends from Shape
class and represents a Rhombus shape. During its instantiation, it receives the following parameters:
— lines
: determines the grid
size.
— char
: determines the grid
character.
grid
width and height will be approximately the same size.
💬 The variable
width
can be calculated directly based on thelines
parameter, however, I decided to calculate it based on thecenter
variable. Sometimes I sacrifice performance for code clarity, but only when the loss of performance seems harmless to me. Expressing its width in function of its center makes things clearer than expressing its width in function of its height.
- ComposedShape.kt
ComposedShape
class is a special type of Shape since it is determined by merging 2 other Shapes.
The enum class Operation
defines all the operations that can be applied on the shapes involved in the merger. These operations are UNION
and INTERSECTION
.
grid
width and height depend on the grids involved in the merging operation.
- Space.kt
Space
class defines an object by applying the Singleton design pattern. It extends from Shape
class so it can be added to the Panel. This object represents an “empty” space and it will be used as a Shape separator. Since this object’s composition doesn’t vary, it can be reused which makes it a good candidate for a Singleton instead of instantiating a new object every time a separator is required.
WIDTH
constant determines its CharArray line
length.
grid
will return an empty CharArray.
line
defines a lambda that always returns a CharArray of length WIDTH
filled with WHITE_SPACE
characters.
‘Container’ package
There is only one class whose role is to contain a collection of geometric shapes to print them to the console.
- Panel.kt
shapes
property defines a list that contains all the Shapes added to the Panel.
addShape
function adds a Shape to the list.
print
function prints out to the console all the Shapes from the list, one line at a time.
Undoubtedly, Shapes-DSL project could implement more geometric shapes and could even be improved. For example, we can prevent the Space object to be used to create a ComposedShape. However, the goal of this series of articles is to create a DSL on top of existing code, assuming that we can’t modify it. In the next article, we’re going to start coding our DSL.
Continue with the next article Coding a DSL: 1 — Package structure and the ‘Panel’ 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 get the full project Shapes-DSL on my GitHub account at https://github.com/pencelab/Shapes-DSL