A lot of people read my article Gang of Four Patterns in Kotlin. I honestly didn't expect so much resonance. It was featured, retweeted and discussed. While writing this, the github repository has received more than 100 stars. Thank you all for that! π
With this article, I want to address the most discussed patterns once again: the Decorator and the Builder.
Additionally I want to add an example of Kotlin implementation for an important pattern that was missing from the repository so far: the Visitor.
Decorator
The Decorator pattern as demonstrated in GoF is just a very convoluted way to achieve functions composition
This is what Mario Fusco left me in the comments. What should I say? I think he's correct.
I did not want to use the same solutions Mario used in his repository, so I fell back to a "pure" Kotlin approach. However, funKTionale, a library that adds a lot of "standard" functional patterns to Kotlin, shows that function composition is actually possible with Kotlin, and nothing less "pure" than using extension functions.
Here is the same old decorator example, but this time with function composition:
fun ((String) -> String).then(f: (String) -> String): (String) -> String {
return { t -> f(this(t)) }
}
There is a lot going on here. (String) -> String.then
means that a function then
is added to all functions that take a String
and return a String
. That means that then
is an extension function to a function.
The interesting part here is that then
also takes the same type of function as a parameter that it extends; and that it also returns the same type of function.
It's therefore possible to write something like this:
val compose: (String) -> String = { string: String -> "$string?" }.then { string: String -> string.replace('c', 'k') }
println(compose("functional")) // will print "funktional?"
It simply chains the function calls together, first adding a ?
to the original value and then
replacing all c
with k
.
It's possible to clean this up a little by using the infix
modifier. An infix
function is a function that can be accessed without brackets or dots.
One example is Kotlin's built-in to
function, that creates a Pair
:
val pair = "key".to("value") // this is possible
val pair = "key" to "value" // but this is nicer. it works thanks to infix
Applying the infix
modifier to the then
function results in the following code:
infix fun ((String) -> String).then(f: (String) -> String): (String) -> String { ... }
...
val compose = { ... } then { ... }
To make this example a little bit more similar to the old one, I introduce another infix
extension function:
infix fun String.decorate(f: (String) -> String): Text {
return DefaultText(f(this))
}
This function, decorate
is an extension function to String
that constructs a DefaultText
object with the result of the function f
.
I am now actually decorating Strings
and not the Text
objects anymore, but the result is the same.
With these two extension functions, I can now create and draw a decorated Text like this:
fun underline(text: String) = "_${text}_"
fun background(text: String) = "\u001B[43m$text\u001B[0m"
val text = "Functional" decorate (::underline then ::background)
text.draw()
The then
function composes the underline
with the background
function. I use function references here, (::
) but lambdas would also work.
This is very specific example of function composition, and if I would refactor and generalize it, It will eventually be the same as the composition from the funKTionale library:
infix fun <P1, IP, R> ((P1) -> IP).andThen(f: (IP) -> R): (P1) -> R = { p1: P1 -> f(this(p1)) }
Builder
Some people have noted that my example of the Builder wasn't really idiomatic, since you could simply use constructors with named arguments.
I do agree with that statement, but not under all circumstances.
My example was simple; a Car
class with two parameters.
class Car() {
var color: String = "red"
var doors = 3
override fun toString() = "$color car with $doors doors"
}
But that's already the problem. It's too simple. You can easily (and more idiomatically) write it with constructor parameters:
class Car(var color = "red", var doors = 3) {
override fun toString() = "$color car with $doors doors"
}
//called e.g. like this:
val car1 = Car(color="green")
val car2 = Car(doors=4, color="blue")
val car3 = Car()
That's a nice pattern. And it can definitely replace the Builder pattern for simple examples. But what if you have more properties? It can become quite ugly. Apart from visuals, I'd say that a class with more than 5 constructor parameters is almost always a bad idea, since it's likely doing too many things (unless it's all data).
Another drawback with this approach is, that I can't reuse and pass around the configuration. With apply
, I can prepare a car configuration like this:
val config: Car.() -> Unit = {
color = "yellow"
doors = 5
}
and apply it later by using Car().apply(config)
. I could also apply multiple configurations at once, which can make the code a lot more readable than having dozens of constructor parameters.
val car = Car()
.apply(COBALT_BLUE)
.apply(LEATHER_PACKAGE)
.apply(TIRES_18_INCH)
So in order to stay flexible, I still prefer the apply
approach in most cases, but the constructor approach is equally useful and valid.
Visitor
Represent an operation to be performed on the elements of an object structure.
Visitor lets you define a new operation without changing the classes of the elements on which it operates
When I want to provide a set of unrelated algorithms that operate on the same elements, a Visitor can be the solution. The difference with e.g. the Strategy is, that the operation that gets executed depends on two types: the type of the Visitor and the type of the element it "visits". This is called double-dispatch. And this is the key to this pattern. Instead of adding a lot of methods to the elements, and therefore statically binding all possible functionality to them, elements will just declare a method accept
, that basically says "please operate on me" and make use of one of OOP's most important concepts - dynamic binding.
This has some implications, though. While it's easy to add new operations, it's pretty hard to add new classes to the element hierarchy. For every new class in the hierarchy every Visitor needs to implement a new method.
A visitor and its corresponding elements may look like this:
interface ShapeVisitor<T> {
T visit(Square element);
T visit(Circle element);
T visit(Rectangle element);
// ... and another `visit` method for every element of the hierarchy
}
interface Shape {
<T> T accept(ShapeVisitor<T> visitor);
}
(I stole Mario Fusco's example this time. I feel no shame!)
There is one method for every subtype of Shape
and one method in each shape to accept an algorithm. A concrete visitor might look like this:
public static class AreaVisitor implements ShapeVisitor<Double> {
public Double visit(Square element) {
return element.side * element.side;
}
public Double visit(Circle element) {
return Math.PI * element.radius * element.radius;
}
public Double visit(Rectangle element) {
return element.height * element.width;
}
}
And would be called like this:
double totalArea = 0.0;
ShapeVisitor<Double> areaVisitor = new AreaVisitor();
for (Shape figure : figures) {
totalArea += figure.accept(areaVisitor);
}
System.out.println("Total area = " + totalArea);
To add another operation, I can simply add another Visitor
public static class PerimeterVisitor implements ShapeVisitor<Double> {
public Double visit(Square element) {
return 4 * element.side;
}
public Double visit(Circle element) {
return 2 * Math.PI * element.radius;
}
public Double visit(Rectangle element) {
return (2 * element.height + 2 * element.width);
}
}
As others have pointed out already, it's just a complicated way to do a switch
statement over the class of an object. Java does not have a switch
on class hierarchies. This is considered anti-oop anyway. So we use the visitor pattern.
Functional programming has a better way of doing things like this: Pattern Matching.
Luckily, Kotlin is a multi-paradigm language so we don't need to be purely object-oriented. And also luckily, Kotlin has Pattern Matching "light" with the when
expression and sealed
classes.
sealed class Shape()
class Square(val side: Double) : Shape()
class Circle(val radius: Double) : Shape()
class Rectangle(val width: Double, val height: Double) : Shape()
The benefit of sealed classes are that - unlike interfaces or abstract
classes -
the compiler knows every possible manifestation of that class. It's therefore possible to use a when
expression that checks whether all possible subclasses have been checked:
val areaVisitor = { shape: Shape ->
when (shape) {
is Rectangle -> shape.height * shape.width
is Circle -> shape.radius.square() * Math.PI
is Square -> shape.side.square()
}
}
If I'd remove e.g. the line is Square -> ...
the code wouldn't compile (this is similar to how an enum
would behave. Also notice the usage of smart casts, that allow accessing properties of subclasses (e.g. radius
or side
) without explicit casting.
Now, I can just use that lambda-visitor to sum up all areas.
val totalArea = figures.sumByDouble { areaVisitor(it) }
println("Total area = $totalArea")
Adding a new Visitor is also easy, just define a new lambda.
val perimeterVisitor = { shape: Shape ->
when (shape) {
is Rectangle -> 2 * shape.height + 2 * shape.width
is Circle -> 2 * Math.PI * shape.radius
is Square -> 4 * shape.side
}
}
Even more things are possible with the when
statement, e.g. an else
branch or sharing the same operation for multiple conditions.
For more info, I suggest to check out the documentation on when
and sealed
classes.
I added the new examples in full length to the github repository. π₯
The cover image was taken from stocksnap.io
Top comments (4)
More examples of
sealed class
/when
usage can be found in this article: dev.to/danielw/from-network-respon...Lovely example of a visitor, thank you!
Thanks! I'm glad you like it! π
Nice post! Thank you for the example!