In part 1 we looked at a stereotypical implementation of an Invoice
domain class. In part 2 we introduced event sourcing to extract the durability and reporting concerns from the behavioral requirements and to move closer to making the Invoice
immutable. In this part we’ll explore the first implementation of an immutable Invoice
.
For the first immutable implementation we’ll use a simple trick that can be used to turn any mutable structure into an immutable one: instead of modying the existing instance to perform mutation, we’ll return a new copy of the existing instance with the modifications already applied to it. This leaves the original instance unchanged. Here’s a quick example of such a transformation applied to a mutable counter:
class MutableCounter(var current: Int) {
def increment {
current += 1
}
}
class ImmutableCounter(val current: Int) {
def increment = new ImmutableCounter(current + 1)
}
Notice that we changed the var
into a val
and the increment
method was changed to return a new counter. So moving from a mutable to immutable implementation can basically be achieved by mechanical code translation. Maybe future IDEs will provide us with refactoring support for this?
Before we take on the Invoice
class we need to take care of some other details first. The previous Invoice
implementation provides two ways of construction:
In the second case we temporarily have an “invalid” invoice. This is a bad idea, but unfortunately all too common. In fact, JPA requires a default constructor, making it harder to always enforce validity.
Although not strictly required, we’ll move creation into a factory. Both the factory and the invoice will use event sourcing, so we define EventSourced
trait to extract commonality:
trait EventSourced[ES <: EventSourced[ES, Event], Event] {
def applyEvent: Event => ES
def unhandled(event: Event) = error("event " + event + " does not apply to " + this)
}
Notice that the applyEvent
method has been changed from type Event => Unit
to a method that takes an Event and returns an new instance of type ES
. The unhandled
method is just for convenience.
The definitions for the aggregate root and factory are listed below:
trait AggregateRoot[AR <: AggregateRoot[AR, Event], Event] extends EventSourced[AR, Event] {
def uncommittedEvents: List[Event]
def markCommitted: AR
}
trait AggregateFactory[AR <: AggregateRoot[AR, Event], Event] extends EventSourced[AR, Event] {
def loadFromHistory(history: Iterable[Event]): AR = {
var aggregate = applyEvent(history.head)
for (event <- history.tail)
aggregate = aggregate.applyEvent(event)
return aggregate.markCommitted
}
}
The AggregateRoot
trait defines two abstract methods: uncommittedEvents
should return the current list of uncommitted events, and markCommitted
should return a new instance (remember, immutability!) of the aggregate root with the uncommitted events cleared.
The AggregateFactory
provides the loadFromHistory
method. It first applies the initial event to itself to create an instance of the aggregate and then applies the remaining events to the successive instances of the aggregate. The final instance is returned, but not before clearing the uncommitted events. We’ll see why later. (Exercise: reimplement loadFromHistory
without the imperative for-loop. FoldLeft is your friend.)
Now that we’ve taken care of the infrastructure, let’s start with our shiny new immutable Invoice. First the factory, which we’ll make a companion object. Here it is:
object Invoice extends AggregateFactory[Invoice, InvoiceEvent] {
def create(invoiceId: Int) = applyEvent(InvoiceCreated(invoiceId))
def applyEvent = {
case event: InvoiceCreated => Invoice(event :: Nil, event.invoiceId)
case event => unhandled(event)
}
}
The factory simply provides a way to create a new invoice from scratch using an InvoiceCreated
event. The implemented applyEvent
method then instantiates a new Invoice
for us with the provided id and the creation event as its only uncommitted event.
The immutable invoice class is similar, just bigger. Let’s first define the data it needs:
case class Invoice (
uncommittedEvents: List[InvoiceEvent],
id: Int,
recipient_? : Boolean = false,
nextItemId: Int = 1,
items: Map[Int, InvoiceItem] = Map.empty,
sent_? : Boolean = false,
paid_? : Boolean = false,
dueDate: Option[LocalDate] = None)
extends AggregateRoot[Invoice, InvoiceEvent] {
// [... code omitted ...]
}
All data fields are now val
s instead of var
s and we’ve added the list of uncommitted events. The invoice has also been changed to a case class so that we can use the convenient copy
method in our applyEvent implementation.
Let’s take a look at the send
method and the corresponding case in the applyEvent
method:
def send: Invoice = {
require(!sent_?, "invoice already sent")
require(readyToSend_?, "recipient and items must be specified before sending")
val now = new LocalDate
applyEvent(InvoiceSent(id, sentDate = now, dueDate = now.plusDays(14)))
}
def applyEvent = {
// [... code omitted ...]
case event: InvoiceSent =>
copy(event :: uncommittedEvents, sent_? = true, dueDate = Some(event.dueDate))
// [... code omitted ...]
}
Compared to the previous implementation of send and applyEvent very little has changed. The main differences are that we return a new copy of Invoice
from send
and that we explicitly prepend the InvoiceSent
event to the list of uncommitted events.
Now that applyEvent
always adds the event to the uncommitted events it also becomes clear why we need to invoke markCommitted
when loading an invoice from its history. If we didn’t, all historical events would be part of the invoice’s uncommitted events after reloading!
Talking about markCommitted
, we still need to implement it. It should return a new copy of the invoice with the list of uncommitted events emptied. That’s easy:
def markCommitted = copy(uncommittedEvents = Nil)
That’s it! The full implementation of the immutable invoice can be found at Invoice.scala. Here’s an example on how you could use such an invoice in client code:
"ready to send invoice" should {
"generate invoice sent event" in {
val invoice = Invoice.create(1)
.changeRecipient(Some("Erik"))
.addItem("Food", 2.95)
.send
invoice.uncommittedEvents must contain(
InvoiceSent(1,
sentDate = new LocalDate(2011, 1, 29),
dueDate = new LocalDate(2011, 2, 12)))
}
}
That’s a pretty nice example of a fluent interface, something quite common to functional code.
Compared to the switch from JPA to event sourcing, the move from mutability to immutability was quite straightforward. No major surgery was required. This is a good thing. It shows that it is possible, and easy, to implement your domain using immutable objects. We’ve also managed to make some minor improvements, such as moving loadFromHistory
from the aggregate itself to a factory.
The main drawback is the need to explicitly deal with uncommitted events in applyEvent
and the addition of the markCommitted
method to each implementer of AggregateRoot
.
In the next parts we’ll explore how the immutable implementation can help improve the design and implementation of the Invoice
class and how we can remove some of the boilerplate code related to keeping track of uncommitted events.