visit
null
)?In most applications I’ve seen a mix of slightly specialized objects and setting fields to null
has been used. The developers did not wanted to create specialized objects for all use cases. Partly because there was no clear rule when to introduce new data transfer objects and partly because they had no idea how to name these new classes.
As a result over time it becomes hard to update the service layer. There is no clear logic when which data transfer objects are used and which fields of the used objects can be expected to be set. This leads to tons of not-null
-checks in null-safe languages or more likely and even worse to many NullPointerExceptions
.
I don’t know how often I had to deal with this issue in my life already. As a last resort most teams implement some kind of locking to prevent parallel editing of business objects. But think of a tool like — a simple Kanban-board solution — and how useless it would be if you couldn’t collaborate with your colleagues on one card at the same time…
A command is an object that describes a request to do something and contains exactly the information required to perform an action of one specific type.
Commands and events are most fun in languages which provide syntax for one-line data classes like for example Kotlin or Scala. So lets take a look at how commands for working with cards on a Kanban board might look in Kotlin:// Commands.kttypealias UserId = Stringinterface Command { val user: UserId}// CardMessages.kttypealias CardId = Stringdata class CardPosition(val column: ColumnId, val before: CardId?)sealed class CardCommand : Command { abstract val card: CardId}data class CreateCard( override val user: UserId, val board: BoardId, val pos: CardPosition, override val card: CardId, val title: String) : CardCommand()data class RenameCard( override val user: UserId, override val card: CardId, val currentTitle: String, val title: String) : CardCommand()data class MoveCard( override val user: UserId, override val card: CardId, val currentPos: CardPosition, val pos: CardPosition) : CardCommand()data class DeleteCard( override val user: UserId, override val card: CardId) : CardCommand()
Don’t care about the Kotlin details here like sealed classes and type declarations — I’ll explain them in another post.
From the code above you can derive some of the characteristics of commands:
I can’t stress often enough, that commands are requests to do something. Thus a command needs all information required to validate whether the request is valid and can be processed. This is also a benefit compared to sending around entities: Entities contain only their data — data required for validation must be collected from somewhere else.
From the code above you can see for example, that we’ve included information about the user who submitted the command. This allows us to verify, if the user is allowed to execute this request or dismiss it otherwise.You may wonder about the currentTitle
property of the RenameCard
command: This allows us to verify, that the client (GUI, app) who send the command is in sync. If the currentTitle
does not match the card's real current title as known by the server we might want to reject the request.
// CommandReceiver.kt...fun receive(cmd: Command) = when (cmd) { is BoardCommand -> boardCommandReceiver.receive(cmd) is ColumnCommand -> columnCommandReceiver.receive(cmd) is CardCommand -> cardCommandReceiver.receive(cmd) ... else -> Unit}...
The specialized command receiver implements our business logic. It validates the command and rejects it if necessary. If the validation is successful it mutates our entity’s state:
// CardCommandReceiver.kt...fun receive(cmd: CardCommand) = when (cmd) { is CreateCard -> validateAndCreateCard(cmd) is RenameCard -> validateAndRenameCard(cmd) is MoveCard -> validateAndMoveCard(cmd) is DeleteCard -> validateAndDeleteCard(cmd)} ...private fun validateRenameCard(cmd: RenameCard) { val user = getUser(cmd.user) // throws if user doesn't exist val card = getCard(cmd.card) // throws if card doesn't exist if (!user.canModify(card)) throw NotAllowedException() if (card.title != card.currentTitle) throw IllegalStateException("Concurrent change") cardDao.renameCard(card.id, cmd.title)}...
We no longer deal with large entity objects with lots of optional fields. In the end we can work with one card object representing our card for displaying it (more on this in the next part). As soon as we want to mutate our card we don’t use the entity class at all. Instead we send tiny, specialized, immutable command objects exactly describing our intend. For a single command it is totally clear to our service layer (our command receivers) which properties of the command can be expected to be not-null
, reducing the number of NullPointerException
s (and totally eliminating them as we use Kotlin here).
Regarding our second problem we haven’t yet implemented undo, but it becomes as simple as that:
fun undoCommandFor(cmd: CardCommand) = when(cmd) { is CreateCard -> DeleteCommand(cmd.user, cmd.card) is RenameCard -> RenameCard(cmd.user, cmd.card, cmd.title, cmd.currentTitle) is MoveCard -> MoveCard(cmd.user, cmd.card, cmd.pos, cmd.currentPos) is DeleteCard -> ... }
See how easy it is to invert most of the commands to undo them. Just the DeleteCard
-command isn't that easy, as you really would need to restore the whole object...
It would also be quite easy to implement a change history based on the successfully executed commands, but this will become even easier based on events as you might see in the next post.
Same is true for collaboration: We’ve already made things much better here as concurrent changes will no longer override each other. Instead of writing a whole object when editing its title we request our service layer to just rename the title. If another user moves the card in the same moment, both changes will succeed independent of each other. If two users would edit a card’s title the first one would win and the request of the second user will fail with an IllegalStateException
as the command's currentTitle
parameter wouldn't be "valid" anymore.
But we still haven’t solved the collaboration topic fully: We need to find a way to send the status updates back to all clients. Commands are not perfect here as they are requests and we need to communicate facts to the clients instead. More on this in part 2…
Commands express fine-grained intends of what a user wants to change in our system. Instead of sending whole entity objects (or even worse: partial ones with lots of null
-properties resulting in NullPointerException
s) we send tailor-made one-shot objects exactly describing the user's request.