Strengthening your domain: Avoiding setters
Previous posts in this series:
- A primer
- Aggregate Construction
- Encapsulated Collections
- Encapsulated Operations
- Double Dispatch Pattern
As we start to move our domain model from an anemic existence towards one with richer behavior, we start to see the aggregate root boundary become very well defined. Instead of throwing a bunch of data at an entity, we start interacting with it, issuing commands through closed, well-defined operations. The surface of our domain model becomes shaped by the actual operations available in our application, instead of exposing a dumb data object that lets anyone do anything, and consistency and validity are a pipe dream.
With more and more of a behavioral model in place, something begins to look out of place. We locked down behavioral entry points into our domain model, yet left open back doors that would render all of our effort completely moot. We forgot to address that last vestige of domain anemia in our model: property setters.
Encapsulation Violated (Again)
In our Fee, Customer and Payment example, we ended with an operation on Fee that allowed us to record a payment. Part of the operation of recording a payment is to update the Fee’s balance. In the last post, we added the double dispatch pattern, creating a separate BalanceCalculator to contain the responsibility of calculating a Fee’s balance:
public Payment RecordPayment(decimal paymentAmount, IBalanceCalculator balanceCalculator) { var payment = new Payment(paymentAmount, this); _payments.Add(payment); Balance = balanceCalculator.Calculate(this); return payment; }
As an aside, I think some were thrown off in the last post that I pass in an interface, rather than a concrete class, making this a bit of a Strategy pattern. I don’t like getting bogged down in names, but the whole “double dispatch” piece comes into play here because the Fee’s RecordPayment method passes itself into the BalanceCalculator. The implementation then inspects the Fee object (and whatever else it needs) to perform the calculation.
If we zoom out a bit on our Fee object, we can see that we’ve encapsulated the Payments collection:
public class Fee { private readonly Customer _customer; private readonly IList<Payment> _payments = new List<Payment>(); public Fee(decimal amount, Customer customer) { Amount = amount; _customer = customer; } public decimal Amount { get; set; } public decimal Balance { get; set; } public IEnumerable<Payment> GetPayments() { return _payments; }
And the BalanceCalculator then uses the Fee object to do its calculation:
public class BalanceCalculator : IBalanceCalculator { public decimal Calculate(Fee fee) { var payments = fee.GetPayments(); var totalApplied = payments.Sum(payment => payment.Amount); return fee.Amount - totalApplied; } }
So what does the BalanceCalculator need to do its job properly? From the Fee object, it needs:
- List of payments
- Fee amount
And from each Payment object, the Amount. But because we’ve exposed Amount and Balance with public setters, we can do completely nonsensical, non-supported operations such as this:
[Test] public void Should_be_able_to_modify_a_fee_and_payment_amount() { var customer = new Customer(); var fee = customer.ChargeFee(100m); var payment = fee.RecordPayment(25m, new BalanceCalculator()); fee.Amount = 50m; payment.Amount = 10m; fee.Balance.ShouldEqual(40m); // Do we even support this?!? }
I have these nice operations in ChargeFee and RecordPayment, yet I can just go in and straight up modify the Amount of the Fee and Payment. The Balance is no longer correct at this point, because I’ve exposed a back door around my aggregate root boundary. We have again violated the encapsulation of our domain model by exposing the ability to modify state directly, instead of allowing modifications through commands. But this is an easy fix for us!
Ditching the mutators
The simplest fix here is to hide the setters for Balance and Amount on our Fee and Payment objects:
public Fee(decimal amount, Customer customer) { Amount = amount; _customer = customer; } public decimal Amount { get; private set; } public decimal Balance { get; private set; }
Our RecordPayment method still compiles just fine, as it has access to this private setter for updating the balance. Because we only want to allow modification of Balance through the Fee maintaining its own consistency, there’s no reason to expose Balance’s setter publicly. The only place we’ll run into potential issues is in our persistence tests, and only then if we decide that we want to start with the database first when building features. We’ll also modify the Payment object accordingly:
public class Payment { public Payment(decimal amount, Fee fee) { Amount = amount; Fee = fee; } public decimal Amount { get; private set; } public Fee Fee { get; private set; } }
Payment had an even bigger issue earlier – it exposed the Fee property setter. That would lead to extremely bizarre behavior, where I could switch the Payment’s associated Fee at any time. Not exactly desirable behavior, nor behavior we ever, ever want to think about if it’s not a supported operation.
If our application does not contain a single screen where we can change a Fee’s or Payment’s Amount, then why do we allow this ability in our domain model? We shouldn’t. We should only expose the operations, data and behavior supported by our application, and nothing more.
Suppose there is a screen that allows modification of a Payment’s Amount. In that case, we’ll want to design an encapsulated operation on Fee (most likely) that allows us to keep the Balance correct and our aggregate root boundary intact.
Finding the balance
In many applications I work with, operations can be generally divided into two categories: data operations and behavioral operations. For data operations, where Fee has a Comments property, there’s no inherent need to encapsulate changing the Comments behind a method, simply because there’s no behavior there. Anyone can change the Fee’s comments at any time, and it won’t affect the consistency of the Fee aggregate root. For data operations, I personally leave the setters in place. The trick is to find what operations are behavioral in nature, and what operations are data-centric in nature.
If we start from the UI/interaction perspective, we’ll find that the design of our model pretty much imitates the design of the UI. If we have a data-centric interaction, we’ll have a data-centric model. If we have a task-based, behavioral interaction, the domain model will follow. What I’ve found is that not many applications are entirely in one camp or the other, and often have pockets and areas where it falls more into one camp or the other. If you get rid of ALL your setters, but have data-centric screens, you’ll run into friction. And if you have behavioral screens but an anemic, data-centric model, you’ll also run into friction.
In any case, the clues to where the design lies often come from conversations with the domain experts. While our domain experts might talk about charging fees and recording payments, they’ll also talk about editing comments. Behavioral in the former, data-centric in the latter.
Wrapping it up
Aggregate root boundaries are fun to talk about in theory, but many domain models you might look at only have boundaries in name only. If a domain model exposes operations and commands only to also expose bypassing these operations by going straight to property setters, then there really isn’t a boundary at all. Through crafting intention-revealing interfaces that allow only the operations and behavior we support through interaction of our application, we can avoid wonky half-baked scenarios and confusion later. This confusion arises where our domain models expose one set of behaviors that our UI needs, and another set of behaviors that are not supported, nonsensical and lead to an invalid state in our model.
The justification for leaving public setters in place is often expressed in terms of “easier testing”. From experience, invalid domain objects are more confusing and harder to test, simply because you can’t know that you’ve set up a context that is actually valid when using your application.
We can avoid this confusion and likely extra defensive coding by removing public setters for data that should only be changed through operations and commands executed on our domain model. Once again, this is what encapsulation is all about. Our model only exposes what is supported, and disallows what is not.