Blog

Towards an immutable domain model – immutability achieved (part 3)

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.

Other parts

Mutable to immutable: a cheap trick

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?

The problem of creation

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:

  1. Construct a brand new invoice by passing a new invoice id to the constructor
  2. Construct an invoice using the default constructor, which should only be used when reloading from durable storage

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.)

Immutable Invoice

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 vals instead of vars 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.

Conclusion

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.

Zilverline gebruikt cookies om content en advertenties te personaliseren en om ons websiteverkeer te analyseren. Ook delen we informatie over uw gebruik van onze site met onze partners voor adverteren en analyse. Deze partners kunnen deze gegevens combineren met andere informatie die u aan ze heeft verstrekt of die ze hebben verzameld op basis van uw gebruik van hun services.

Okee