The defining characteristic of a good developer is making the code they write easy to read, easy to reason about and easy to change. 1
Back in the 90s Kent Beck wrote 4 rules of simple design (Martin Fowler’s recapitulation can be found here) which can help us achieve our objectives of code that’s easy to read, reason about and change.
- Passes the tests
- Reveals intention
- No duplication
- Fewest elements
J.B. Rainsberger wrote an excellent article on how rules 2 and 3 (minimises duplication and reveals intent) form a tight feedback loop when used together. He argues that removing duplication causes new structure to emerge, which we name clearly to reveal intention. Over time we add more abstraction to the new things which in turn drives us to remove further (new) duplication and to find better names for things.
The drive to reveal intent means we model the real world more closely. We use nouns and verbs in our code to describe that real world behaviour more accurately. Those nouns and verbs map to objects and functions in our application. And so we find we’ve developed a domain specific language almost without thinking.
Here’s a really good example from Liz Keogh on how a custom DSL can help us embed meaning in our tests without having to implement a tool like Cucumber or JBehave.
And here’s a simple worked example from me to show you how you might do it in Kotlin. Let's talk about sandwiches.
Photo by OLA Mishchenko on Unsplash
I usually bring my simple home-made sandwiches to the office for lunch, but sometimes I like to splash out on a fancy ‘piece’ from a local cafe. Problem is the number of possible permutations of filling always gets me stuck in analysis paralysis.
Let’s model creating a sandwich in Kotlin.
Here’s a fairly typical Ham and Cheese Toastie in an Object-Oriented style.
val ooSandwichOrder = Sandwich()
ooSandwichOrder.type("toastie")
ooSandwichOrder.bread("white")
ooSandwichOrder.fillings(listOf("Cheese", "Ham"))
ooSandwichOrder.dressings(listOf(
"Pepper", "Worcestershire sauce"
))
ooSandwichOrder.side("Crisps")
val ooSandwich = ooSandwichOrder.makeMeASandwich()
println(ooSandwich.receipt())
As you can see, we create a sandwich, then set its properties through method calls before building it and getting a receipt. I’m using Strings for the settings in the example, but I might use enums or separate classes in a production implementation.
By using OO principles we are able to define a domain specific vocabulary without any bells and whistles just by defining the method names we want to use. However, we may want to adjust this to hide some of the OO nature and make it more expressive.
Let’s re-write it using some Kotlin capabilities to make it more like a typical DSL.
val dslSandwich = sandwich {
with type "toasted"
bread = "baguette"
filling("cheese")
filling("ham")
filling("tomato")
dressings {
+"Basil"
+"Pepper"
}
sideOrders {
side("French Fries")
}
}
println(dslSandwich.receipt())
This code is a little longer (it covers more lines than before), but it’s easier to read. I’ve used a few different techniques to demonstrate how you might write it which increases the variation - your production version would likely be more consistent.
Let’s take a look at some of the key features.
sandwich
is a function but it looks like a keyword
We’re wrapping our object constructor in a function here and passing in some configuration options as a lambda. A nice feature of Kotlin is that we can move lambda definitions outside of parentheses in function calls. And because the lambda is the only parameter we don’t need the parentheses at all.
fun sandwich(order: Sandwich.() -> Unit): Sandwich =
Sandwich().apply(order)
Our sandwich
method creates a Sandwich object then applies the lambda we pass as a parameter to it.
type
is an infix function
By adding infix
to our method definition we can call type
without the .
and ()
(there are some requirements). This is a less than good example as I’ve also had to define with
as a property which points to this
to make it work.
with type "toasted"
// is the same as
with.type("toasted")
In our Sandwich class
class Sandwich {
public val with = this
infix fun type(sandwichType: String) {
type = sandwichType
}
...
}
We’re overriding the String.unaryPlus()
method using an extension method
We do this inside Dressings
to allow us to use the +”salt”
syntax to add a new dressing. We can do the same inside SideOrders
and Fillings
to allow us to override the same operator to do different things in those methods in our DSL.
class Dressings {
private val dressings = mutableListOf<String>()
operator fun String.unaryPlus() = dressings.add(this)
}
We're using Receivers with our lambdas.
We’re using Receivers in our dressings
and sideOrders
methods to allow us to execute the lambda in the context of the relevant object (Dressings
and SideOrders
respectively)
// in Sandwich
fun dressings(toAdd: Dressings.() -> Unit): Sandwich {
dressings.toAdd()
return this
}
Read more about function literals with receivers
All the sample code is available on this bitbucket repo
Incorporating domain specific vocabulary into your applications is a good thing. It helps you write code that’s simple, easily understood and readily extended. As we've seen in this blog post, Kotlin provides useful features that help you achieve that clarity.
If you want to take it further, there are lots of helpful resources available to you online, here is just a small sample:
-unsplash
Get in Touch
If you are interested in any of our Kotlin courses please contact us via the website or by email for further information.
Now, I don't know about you but I'm off for a BLT…
- I can't claim to be a good developer, but I know what one looks like.↩