First hurdle, finding and understanding the equivalent of Composer in Kotlin. In this case I was lucky to have the Jetbrains IDE (community version is enough) that makes your life easier by creating a console project from scratch.
Also in my case I had to use the 16 version of Java due to incompatibilties with 17 version.
So far so good. Now the good begins...
1. Put the mower kata README to define the goals
As we said, laying the first stone means starting with the tests usind DDD as a structural element.
In Java and Kotlin, by heritage of the former, the main and test folder are at the same level within src.
Let's create inside main/kotlin our bounded context mower and module mower.
Java naming convention rule. The folders (or packages as it is usually called) always in lowercase.
2. Watch how to create a Value object
In this case we're going to see how to create the smallest piece of the DDD universe. The Value object.
A value object must meet the following specifications.
Specifications |
---|
Should have a primitive or another value objects |
Should be immutable |
Should be able to validate itself when built |
Should be able to be constructed in different ways if it's necessary "named constructors" |
In PHP, a Value object might look like this.
final class YCoordinate
{
private const MINIMUM_AXIS_VALUE = 0;
/**
* @throws YCoordinateOutOfBoundsException
*/
private function __construct(private int $value)
{
if (self::MINIMUM_AXIS_VALUE > $this->value) {
throw YCoordinateOutOfBoundsException::withCoordinate($this->value);
}
}
public static function build(int $value): self
{
return new self($value);
}
public function value(): int
{
return $this->value;
}
}
This class conforms to the standards for a Value object.
It should be noted that I am a strong advocate of using named constructors. They provide context and when an object can be created in different ways, having made a first named constructor then opens the door to adding a second or third one, focusing on the context of why that one was used and not another one that the object may have.
Now let's see if a Value object in Kotlin can be created in a similar way with the same rules.
Looking at the language specifications, it seems that the named constructor issue is different. We'll have to work on it a bit...
As we said before... we write tests with our targets for the Value object before we write anything else.
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.*
import kotlin.test.Test
private const val Y_POSITION: Int = 3
private const val INVALID_POSITION: Int = -1
internal class YMowerPositionTest
{
@Test
fun `Should be build` ()
{
var yMowerPosition = YMowerPosition.build(Y_POSITION)
assertInstanceOf(YMowerPosition::class.java, yMowerPosition)
assertThat(yMowerPosition.value).isEqualTo(Y_POSITION)
}
@Test
fun `Should throw exception for invalid values` ()
{
assertThrows(Exception::class.java) {
YMowerPosition.build(INVALID_POSITION)
}
}
}
Using data class
The Data class is promising... with few code you have almost everything we need, but there's a problem with named constructors that invalidates this option.
We can make it immutable declaring its values as "val" instead of "var". They accept "copy", "equals", "toString" directly without our intervention... and access values is easy... buuuuut we can't make the original constructor private, since the "copy" needs to have access to the constructor... and that would create a doorway to a contextless constructor, which is exactly what we want to avoid.
While it would have been nice to have all of that by default, just because you can't make the original constructor private, it's ruled out. Although they can still be used as DTOs.
private const val MINIMUM_AXIS_VALUE: Int = 0
data class YMowerPosition constructor (val value: Int) {
init {
if(value < MINIMUM_AXIS_VALUE){
throw Exception("Invalid value")
}
}
}
Data class as Value Object | Meets | Comments |
---|---|---|
Should have a primitive or another value objects | - | |
Should be immutable | - | |
Should be able to validate itself when built | - | |
Should be able to be constructed in different ways if it's necessary "named constructors" | We cannot make private primary constructor |
Using normal class with "companion object"
With the companion object in a standard class we can achieve the goal of named constructors.
class YMowerPosition private constructor (val value: Int) {
init {
if(value < MINIMUM_AXIS_VALUE){
throw Exception("Invalid value")
}
}
@JvmStatic
companion object {
private const val MINIMUM_AXIS_VALUE: Int = 0
fun build(value: Int): YMowerPosition {
return YMowerPosition(value)
}
}
}
Normal class with companion object | Meets |
---|---|
Should have a primitive or another value objects | |
Should be immutable | |
Should be able to validate itself when built | |
Should be able to be constructed in different ways if it's necessary "named constructors" |
You may be wondering why there is no accessor for "value".
It's simple, by being able to declare a property as immutable with "val", we can make it public, so that cannot change anymore.
Optimizing the Value object with Inline class
If we are going to host primitives, Kotlin brought out the concept of Inline classes.
Its use is intended to optimize resources and discards the concept of identity implicit in an object at the instance level.
By declaring a class as "value class", you basically just allow values to be compared with == operator.
So, our value object can be refactored a bit...
@JvmInline
value class YMowerPosition private constructor (val value: Int) {
...
}
And the tests will continue to pass.
Inline classes have a limit!!!
They are only capable of holding a value using their constructor, so if your value object has more values when it is initialized, it should be a normal class.
Bonus track about constants and static functions
Constants can be at companion object level o at file level out of the class. I prefer within the class. There are other ways to achieve something similar, but without using the const* keyword and it doesn't make sense to me. Also, note that constants can be declared with a data type.
Static functions, in order to be called from Java, must hace tha annotation @JvmStatic. If not, Java doesn't be able to use it comfortably.
class C {
companion object {
@JvmStatic fun callStatic() {}
fun callNonStatic() {}
}
}
C.callStatic(); // works fine
C.callNonStatic(); // error: not a static method
C.Companion.callStatic(); // instance method remains
C.Companion.callNonStatic(); // the only way it works
Links and resources
Naming conventions
https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html
https://www.oracle.com/java/technologies/javase/codeconventions-namingconventions.html
https://www.thoughtco.com/using-java-naming-conventions-2034199
Data classes
https://kotlinlang.org/docs/data-classes.html
Inline classes
https://kotlinlang.org/docs/inline-classes.html
Constructors and validation
https://kotlinlang.org/docs/classes.html#constructors
https://kotlinlang.org/docs/classes.html#companion-objects
Constants
https://kotlinlang.org/docs/basic-types.html#literal-constants
https://kotlinlang.org/docs/properties.html#compile-time-constants
Static functions
https://kotlinlang.org/docs/java-to-kotlin-interop.html#static-methods
Top comments (0)