2015-05-10

Advancing Enterprise DDD - Maintaining Intra-aggregate Constraints

In this essay of the Advancing Enterprise DDD series, we begin look to at immutable objects - a common point of contrast between functional and object-oriented programming approaches. In the last essay, we saw at how MongoDB would be a much better vehicle than RDB for persisting well-shaped DDD aggregates. In this essay, we continue to investigate at the way the tools we use affect our thinking and coding, as we witness the contortions we have to go through to make something as straightforward as an intra-aggregate constraint work in the face of mutability.
There's something we've seen in a couple of the code samples so far in this series that I'd like to revisit. In The Entity and the Aggregate Root, we were looking at the relationship between the Order entity and the Order Items:

public class Order {

    private List<OrderItem> orderItems;

    public List<OrderItem> getOrderItems() {
        // defensive copy
        return new ArrayList<>(orderItems);
    }
}

We don't return the original orderItems list to the client in getOrderItems(). Instead, we make a defensive copy. You will find an extensive discussion on this technique in Joshua Bloch's book Effective Java, a must-read book for Java developers. Our concern here is that the client calling getOrderItems() might modify the list underneath us:

val orderItems = order.getOrderItems();
orderItems.add(newOrderItem);

On the surface, this seems like a natural way to add an Order Item to an Order. But it violates an intra-aggregate constraint - a constraint that is confined within an aggregate, but involves more than one entity. Let's remember some more details from our Order example:

public class Order {
    private List<OrderItem> orderItems;
    private Amount totalPrice;
}

public class OrderItem {
    private Integer quantity;
    private Amount price;
}

There is an intra-aggregate constraint here that we haven't explicitly expressed yet: the total price for the Order must equal the sum of all the prices for the Order Items. We can express this in UML with a note. In UML, constraints are indicated with the use of curly brackets:


When the caller of Order.getOrderItems() adds a new Order Item to the list, the constraint is broken, because the Order did not have a chance to update the total price. The Order maintains this constraint by including methods in its API for adding and removing Order Items, and it adjusts the total price accordingly in these methods:

public void addOrderItem(OrderItem orderItem) {
    orderItems.add(orderItem);
    recomputeTotalPrice();
}

private void recomputeTotalPrice() {
    totalPrice = 0.0;
    for (OrderItem orderItem : orderItems) {
        totalPrice +=
            orderItem.getQuantity() * orderItem.getPrice();
    }
}

We also have to make a defensive copy for any API method that sets the orderItems list, such as a constructor or a setOrderItems() method. This is because the caller who passed us the list might hold on to it, and modify it as well. Our Order constructor might look like this:

public Order(List<OrderItem> orderItems) {
    // defensive copy
    this.orderItems = new ArrayList<>(orderItems);
    recomputeTotalPrice();
}

We are maintaining our constraint fairly well, but that's a lot of defensive copies! Imagine some old-fashioned Java code such as this:

int totalQuantity = 0;
int numOrderItems = order.getOrderItems().size();
for (int i = 0; i < numOrderItems; i++) {
    totalQuantity += order.getOrderItems().get(i).getQuantity();
}

If we have n Items in our Order, we just made n + 1 defensive copies! Admittedly, the code could be rewritten in a couple of different ways to prevent this problem, but should the Order client really be responsible for understanding that getOrderItems() is a costly operation?

Making defensive copies is expensive. In Big O notation, it is an O(n) operation in both time and space. It also increases the cognitive load of our developers, as they have to parse through the defensive copies when reading the code, and keep in mind that what appears to be a simple getter method may well have a hidden cost.

Functional/Object-Oriented hybrid languages such as Scala avoid these kinds of problems by using immutable lists. When you perform some kind of update operation on an immutable list, the changes are not made in place. Instead, a new list is constructed containing the the updated contents. For instance, consider this Scala snippet:

val orderItems1: List[OrderItem] = order.orderItems

// append a new Order Item to the list, creating a new list
val orderItems2: List[OrderItem] = orderItems1 :+ newOrderItem

println(orderItems1.size) // prints 5
println(orderItems2.size) // prints 6
println(order.orderItems.size) // prints 5

When we append a new OrderItem to orderItems1, we get a new list containing the extra element. The original list is unsullied. There is no need to make defensive copies. It might seem expensive to create a new copy of the list for every update. But thanks to the use of some very clever data structures, while these operations are more expensive than the corresponding operations on an ArrayList, they still exhibit constant time behavior.

Immutable data structures have other advantages over their mutable cousins. They are easier to reason about, and in a multi-threaded environment, we don't have to worry about synchronization blocks. In Java, if I am iterating over an ArrayList, and another thread adds or removes elements during my iteration, I will get a ConcurrentModificationException. To avoid this, I need to set up synchronization blocks to prevent the concurrent modification. Note that it is not sufficient to work off a copy of the original list, because making a copy of the list also requires iterating over the list.

Guava or Functional Java are both Java libraries that provide us with immutable collections. But JPA only supports standard Java collection classes for one-to-many and many-to-many associations.

Unfortunately, creating defensive copies of the orderItems list is not sufficient to maintain our constraint, as the client can simply modify the OrderItem itself:

OrderItem firstOrderItem = order.getOrderItems().get(0);
Int quantity = firstOrderItem.getQuantity();
firstOrderItem.setQuantity(quantity + 1);

Our constraint just broke again, as Order.totalPrice is no longer up to date with the Order Items. There are a variety of approaches we can take to remedy this. For one, we can make the relationship between Order and Order Item bidirectional, and then have OrderItem.setQuantity(Int) call the Order.recomputeTotalPrice() method itself. For this to work, the recomputeTotalPrice method can no longer be private. We don't want to expose this method publicly, so we put both entities in the same package, and make the method package-private. The relevant code for our Order class would look something like this:

package storefront.domain.orders;

public class Order {
    private List<OrderItem> orderItems;
    private Amount totalPrice;

    // no visibility modifier means package-private
    void recomputeTotalPrice() {
        totalPrice = 0.0;
        for (OrderItem orderItem : orderItems) {
            totalPrice +=
                orderItem.getQuantity() * orderItem.getPrice();
        }
    }
}


And our Order Entity would look something like this:

package storefront.domain.orders;

public class OrderEntity {
    private Order order;
    private Int quantity;

    public void setQuantity(Int quantity) {
        this.quantity = quantity;
        order.recomputeTotalPrice();
    }
}

I'm not particularly fond of this approach, because it entangles the two classes by splitting the constraint maintenance work between the two. We've also made what was naturally a unidirectional relationship into a bidirectional relationship, which complicates our model.

A better approach would be to force requests for updating the quantities to go through the Order. With this approach, it is the OrderEntity.setAmount method that becomes package private:

public class OrderEntity {
    private Int quantity;

    // visibility is limited to the storefront.orders package
    void setQuantity(Int quantity) {
        this.quantity = quantity;
    }
}

And we add a new API method to Order:

public class Order {
    private List<OrderItem> orderItems;

    public void updateOrderItemQuantity(Int index, Int quantity) {
        orderItems.get(index).setQuantity(quantity);
        recomputeTotalPrice();
    }
}

Making the Order Item entities immutable would also solve the problem, as it would similarly force all changes to the Order Items to go through the Order API. But this approach would probably not work so well with JPA, which keeps a cache of managed entities keyed on object identity. An update to an Order Item would create a new Java object, which would no longer be a JPA managed entity. It would have to be merged back into the session before persisting.

It's pretty clear that JPA was not designed with an immutable mindset. Immutability is more of a functional approach to things, whereas in the OO mindset, we work with mutable objects by default. There are many techniques that we can use within JPA to maintain our intra-aggregate constraints. But most of these difficulties arise as a direct result of working with mutable objects. In the next essay, we will take a closer look at what life might be like if we could design our entities with immutability in mind.

No comments:

Post a Comment