Tactical Design by Example - Using Kotlin and Spring Boot (Part 10) - Creating Commands with Kotlin's invoke method
I wanted to elaborate on behaviour-centric development, but that didn't happen. Maybe I will get back to that later. Here's something different I have learned recently, the power of Kotlin's invoke method.
Revisiting commands
In part four I talked about a similar example of a command
data class UpdateDepositPrice(
val articleId: String,
val refundPrice: Int,
val currency: Currency,
val currencyUnit: CurrencyUnit
) : Command() {
companion object {
fun create(priceUpdated: PriceUpdated): UpdateDepositPrice {
val articleId = priceUpdated.getArticleId()
requireNotNull(articleId) { "No article id found in listing!" }
return UpdateDepositPrice(
articleId,
priceUpdated.price.customAttributes.refund,
Currency.valueOf(priceUpdated.price.currency),
CurrencyUnit.valueOf(priceUpdated.price.unit)
)
}
}
}
Problems
While this is a perfectly valid way to implement it, a few things bugged me about it
- requireNotNull throws an exception, which is quite harsh and requires handling.
- The create method is syntactical overhead that is distracting
try {
UpdateDepositPrice.create(priceUpdated).let {
workflow.process(it)
}
} catch (e: IllegalArgumentException) {
log.warn(e.message)
}
A more idiomatic approach
What I wanted to achieve was to have a constructor-like invokation, that returns an optional command. And this is exactly what you can achieve with Kotlin's invoke method.
data class UpdateDepositPrice(
val articleId: String,
val refundPrice: Int,
val currency: Currency,
val currencyUnit: CurrencyUnit
) : Command() {
companion object {
operator fun invoke(priceUpdated: PriceUpdated): UpdateDepositPrice? =
priceUpdated.getArticleId()?.let { articleId ->
UpdateDepositPrice(
articleId,
priceUpdated.price.customAttributes.refund,
Currency.valueOf(priceUpdated.price.currency),
CurrencyUnit.valueOf(priceUpdated.price.unit)
)
}
}
}
Result
Now this feels more like a constructor when using it, although it is just the invokation operator () being called on the companion object.
And at the same time, there is no more exception handling necessary
UpdateDepositPrice(priceUpdated)?.also {
workflow.process(it)
} ?: log.warn("message")
More improvements
Instead of returning null I could also return an Either-like object, that would contain detailed information of possibly errors, which could be logged in the adapter.
I also like to use the invoke method when passing commands to use cases or workflows, and get rid of the process method. The example could be even simpler then, and I'm tempted to write it in a single line of code
UpdateDepositPrice(priceUpdated)?.also { workflow(it) } ?: log.warn("message")
Thanks to Christopher Keenan for mentioning the invoke method in his blog post Invoking use cases the Kotlin way. Very helpful!
Go back to part nine.