A First Sketch
To make it a bit more concrete, let's use a list of problems/features to discuss how some common design problems of an ordering application could be
I know, I know. The answer to how I would deal with the feature list with my current favorite style of a Domain Model depends on many factors. As I said before, talking about a "default architecture" is
However, I still want to discuss a possible solution here. It will be very much a sketched summary, with details following in later chapters. What I will be discussing here will be kind of a first rough attempt, and I will leave it like that, letting the design
Problems/Features for Domain Model ExampleI will reuse this list of requirements in later chapters for discussing certain concepts. Let's dive in and examine the details for each problem/feature in random order.
So we now have a nice, simple little feature list that we can use for discussing solutions from an overview perspective.
Note You might get the feeling that the feature list is a bit too data focused. In some cases, what might seem data focused will in practice be solved with properties that have some behavior attached. But the important thing to note here is that what we want is to apply the Domain Model. The main problem with that is how to deal with data if we use a relational database. Therefore, it's more important to focus a bit more on the data than normal when it comes to object-orientation. With the problems/features in place, it's time to discuss a possible solution for how to deal with them. Dealing with Features One by One
What does all this come down to? Let's find out by examining how I would typically solve the problems/features listed earlier in this chapter. I will focus solely on the Domain Model so that we concentrate on the "right" things for now and try to catch and form the Ubiquitous Language without getting distracted by the infrastructure or other
Still, it can be good to have some idea about the technical environment as well. We keep down the technical complexity and decide that the context is a Rich GUI application executing at the client desktops together with the Domain Model and a physical database server with a relational database, all on one LAN.
Note
I'm going to mention loads of patterns everywhere, but
Without further ado... 1. List Customers by Applying a Flexible and Complex FilterThe first requirement is pretty much about data and a piece of search behavior. First, let's sketch what the Customer class and surroundings (without the Order for now) could look like (see Figure 4-2). Figure 4-2. The Customer class and surroundings
Note
Believe it or not, there's a reason for using hand-drawn figures. I'm showing early
As you see, I used a simple (nave) Customer class and let it be a composition of Address and ReferencePersons . I prefer to not put this kind of search behavior directly on the Customer class itself, so at this point I only have data on the classes shown in Figure 4-2.
Note
I could use the Party Archetype pattern [Arlow/Neustadt Archetype Patterns] instead or other types of role
We could solve the first requirement by using a Query Object [Fowler PoEAA], which I talked about in Chapter 2, "A Head Start on Patterns," but in practice I have found that it's nice to encapsulate the use of Query Objects a bit if possible. There are often a lot of things to define regarding the result of queries that aren't just related to the criteria. I'm thinking about
In Chapter 2, we discussed the Factory pattern [Evans DDD] and said it was about the start of the lifecycle of an instance. The Repository takes care of the rest of the lifecycle, after the creation until the "death" of the instance. For example, the Repository will bridge between the database and the Domain Model when you want to get an instance that has been
I could let the method on the Repository take a huge list of parameters, one for each possible filter field. That is a sure sign of smelly code, however: code that's not going to be very clear or easy to maintain. What's more, a parameter of string type called "Name" is hard to understand for the Repository regarding how the parameter should be used, at least if the string is empty. Does it mean that we are looking for customers with empty names or that we don't care about the names? Sure, we can invent a magic string that means that we are looking for customers with empty
Figure 4-3. A CustomerRepository for dealing with flexible criteria
Note Of course, we might use a Query Object, for example, inside the Repository.
DDD and TDD are a great combination. You get instant feedback, and trying out the model with test code is a great way of gaining insight. With that said, I'm not going the TDD route here and now. I will explain the model sketch with some tiny tests here, but only as a way of providing another view of the solution idea. In the
So to fetch all customers in a certain town (Ronneby), with at least one order with a value of > 1,000 SEK, the following test could be used:
[Test]
public void
CanGetCustomersInSpecificTownWithOrdersOfCertainSize()
{
int numberOfInstancesBefore = _repository.GetCustomers
("Ronneby", 1000).Count;
_CreateACustomerAndAnOrder("Ronneby", 20000);
Assert.AreEqual(numberOfInstancesBefore + 1
, _repository.GetCustomers ("Ronneby", 1000).Count);
}
As you saw in the example, the Repository method GetCustomers() is used to fetch all the customers that fulfill the criteria of town and minimum order size. 2. List the Orders When Looking at a Specific Customer
I could go for a bidirectional relationship between
Customer
and
Order
. That is, each
Customer
has a list of
Orders
, and each
Order
has one
Customer
. But
Figure 4-4. OrderRepository , Order , and surroundings
So when the consumer wants to look at the Orders for a certain Customer , he has to ask the OrderRepository like this:
[Test]
public void CanGetOrdersForCustomer()
{
Customer newCustomer = _CreateACustomerAndAnOrder
("Ronneby", 20000);
IList ordersForTheNewCustomer =
_repository.GetOrders(newCustomer);
Assert.AreEqual(1, ordersForTheNewCustomer.Count);
}
I could let the Customer have an OrderList property and implicitly talk to the Repository, but I think the explicitness is better here. I also avoid coupling between the Customer and the OrderRepository . The total value of each order is worth a separate mention. It's probably trickier than first expected as the calculation needs all orderLines of the order . Sure, that's not tricky, but it does alarm me. I know this is premature, but I can't help it (because loading the orderLines will be expensive when you just want to show a pretty large list of orders for a certain customer , for example). I think one helpful strategy is that if the orderLines aren't loaded, a simple amount field, which probably is stored in the Orders table, is used for the TotalAmount property. If orderLines are loaded, a complete calculation is used instead for the TotalAmount property. This isn't something we need to think about right now. And no matter what, for consumers, it's as simple as this:
[Test]
public void CanGetTotalAmountForOrder()
{
Customer newCustomer = _CreateACustomerAndAnOrder
("Ronneby", 420);
Order newOrder = (Order)_repository.GetOrders
(newCustomer)[0];
Assert.AreEqual(420, newOrder.TotalAmount);
}
In my opinion, the "right" way to think about this is that lines are there because we work with an Aggregate and the lines are an intimate part of the order. That might mean that performance would suffer in certain situations. If so, we might have to solve this with the Lazy Load pattern [Fowler PoEAA] (used to load lists from the database just in time, for example) or a read-only, optimized view. But, as I said, that's something we should deal with when we have found out that the simple and direct solution isn't good enough. 3. An Order Can Have Many Different LinesFeature 3 is pretty straightforward. Figure 4-5 describes the enhanced model. Figure 4-5. Order and OrderLines
Note that I'm considering a unidirectional relationship here, too. I can probably live with sending both an Orderline and its Order if I know that I need both for a certain piece of logic. The risk of sending an order together with a line for another order is probably pretty small.
[Test]
public void CanIterateOverOrderLines()
{
Customer newCustomer = _CreateACustomerAndAnOrder
("Ronneby", 420);
Order newOrder = (Order)_repository.GetOrders
(newCustomer)[0];
foreach (OrderLine orderLine in newOrder.OrderLines)
return;
Assert.Fail("I shouldn't get this far");
}
Also note the big difference in the model shown in Figure 4-5 compared to how a relational model describing the same thing would look. In the relational model, there would be a one-to-many relationship from
Product
to
OrderLine
and
4. Concurrency Conflict Detection Is ImportantHmmm...feature 4 is tricky. I think a reasonable solution here is that Customer is on its own, and Order together with OrderLines is a concurrency unit of its own. That fits in well with making it possible to use the Domain Model for the logic needed for features next. I use the pattern called Aggregate [Evans DDD] for this, which means that you decide what objects belongs to the certain clusters of objects that you typically work with as single units, loading together (by default, at least for write scenarios), evaluating rules for together, and so on. It's important to note, though, that the Aggregate pattern isn't first and foremost a technical pattern, but rather should be used where it adds meaning to the model. Concurrency unit is one thing Aggregate is used for. (See Figure 4-6.) Figure 4-6. Aggregates
5. A Customer May Not Owe Us More Than a Certain Amount of Money
Feature 5 is also pretty tricky. The first solution that springs to mind is probably to add a
TotalCredit
property on the
Customer
. But this is
I think I need a Service [Evans DDD] in the Domain Model that can tell me the current total credit for a customer. You'll find this described in Figure 4-7 Figure 4-7. TotalCreditService
The reason for the overload to GetCurrentCredit() of TotalCreditService is that I think it would be nice to be able to check how large the current credit is without considering the current order that is being created/changed. At least that's the idea. Let's see how the service could look in action (and skipping the interaction here between the different pieces).
[Test]
public void CanGetTotalCreditForCustomerExcludingCurrentOrder()
{
Customer newCustomer = _CreateACustomerAndAnOrder
("Ronneby", 22);
Order secondOrder = _CreateOrder(newCustomer, 110);
TotalCreditService service = new TotalCreditService();
Assert.AreEqual(22+110
, service.GetCurrentCredit(newCustomer));
Assert.AreEqual(22,
service.GetCurrentCredit(newCustomer, secondOrder));
}
I think you can get a feeling about the current idea of the cooperation between the Order , the Customer , and service from Figure 4-7 and the previous snippet. 6. An Order May Not Have a Total Value of More Than One Million SEKBecause I established with the concurrency conflict detection feature that an Order including its OrderLines is a concurrency conflict detection unit of its own or more conceptuallyan Aggregate, I can deal with this feature in the Domain Model. An Aggregate invariant is that the Order value must be one million SEK or less. No other user can interfere, creating a problem regarding this rule for a certain order, because then I or the other user will get a concurrency conflict instead. So I create an IsOKAccordingToSize() method on the Order that can be called by the consumer at will. See Figure 4-8 for more information. Figure 4-8. Order with IsOKAccordingToSize() method
A simple test for showing the API could look like this:
[Test]
public void CanCheckThatAnOrdersTotalSizeIsOK()
{
Customer newCustomer = _CreateACustomerAndAnOrder
("Ronneby", 2000000);
Order newOrder = (Order)_repository.GetOrders
(newCustomer)[0];
Assert.IsFalse(newOrder.IsOKAccordingToSize());
}
Note I need to consider if I should use the Specification pattern [Evans DDD] for this kind of rule later on. 7. Each Order and Customer Should Have a Unique and User-friendly NumberAs much as I dislike this requirement from a developer's perspective, as important and common it is from the user's perspective. In the first iteration, this could be handled well enough by the database. In the case of SQL Server, for example, I'll use IDENTITYs. The Domain Model doesn't have to do anything except refresh the Entity after it has been inserted into the table in the database. Before then, the Domain Model should probably show 0 for the OrderNumber and CustomerNumber (see Figure 4-9). Figure 4-9. Enhanced Order and Customer
Hmmm...I already
I'm actually intermingling two separate concepts, namely the Identity Field pattern [Fowler PoEAA] for coupling an instance to a database row (by using the primary key from the database row as a value in the object) and the ID that has business meaning. Sometimes they are the same, but quite often they should be two different identifiers. It also feels strange that as soon as a customer is persisted it gets a user-friendly number kind of as a marker that it is OK. Shouldn't it rather be when a customer is operational in the system (after credit checks and perhaps manual approval)? I'm sure we'll find a reason to come back to this again in later chapters. 8. Before a New Customer Is Considered OK, His or Her Credit Will Be Checked with a Credit InstituteSo we need another Service [Evans DDD] in the Domain Model that encapsulates how we communicate with the credit institute. See Figure 4-10 for how it could look. Figure 4-10. CreditService
Again, the idea is a matter of cooperation between an Entity ( Customer in this case) and a Service for letting the Entity answer a question. Let's see how the cooperation could be dealt with in a test. (Also note that the real service will probably use the OrganizationNumber of the Customer , which is not the same as the CustomerNumber , but the official identification provided by the authorities for registered companies.)
[Test]
public void CantSetTooHighCreditLimitForCustomer()
{
Customer newCustomer = _CreateACustomer("Ronneby');
//Inject a stubbed version of CreditService
//that won't allow a credit of more than 300.
newCustomer.CreditService = new StubCreditService(300);
newCustomer.CreditLimit = 1000;
Assert.IsFalse(newCustomer.HasOKCreditLimit);
}
Note
Gregory Young pointed this out as a code smell. The problem is most likely to be that the operation isn't atomic. The value is first set (and therefore the old value is overwritten) and then checked (if
9. An Order Must Have a Customer; an Orderline Must Have an OrderFeature 9 is a common and reasonable requirement, and I think it's simply and best dealt with by referential integrity constraints in the database. We can, and should, test this in the Domain Model, too. There's no point in sending the Domain Model changes to persistence if it has obvious incorrectness like this. But instead of checking for it, as the first try I make it kind of mandatory thanks to an OrderFactory class as the way of creating Orders , and while I'm at it I deal with OrderLines the same way so an OrderLine must have Order and Product (see Figure 4-11). Figure 4-11. OrderFactory and enhanced Order
Let's take a look at the interaction between the different
[Test]
public void CanCreateOrderWithOrderLine()
{
Customer newCustomer = _CreateACustomer("Karlskrona");
Order newOrder = OrderFactory.CreateOrder(newCustomer);
//The OrderFactory will use AddOrderLine() of the order.
OrderFactory.CreateOrderLine(newOrder, new Product());
Assert.AreEqual(1, newOrder.OrderLines);
}
Hmmm... This feels quite a bit like overdesign and not exactly fluent and smooth, either. Let's see what we think about it when we really get going in the next chapter.
While we are discussing the
OrderFactory
, I want to mention that I like the idea of using Null Objects [Woolf Null Object] for
OrderType
,
Status
, and
ReferencePerson
. (That goes both for the Domain Model and actually also for the underlying relational database.) The Null Object pattern means that instead of using null, you use an empty instance (where empty means default values for the
this.NoNulls.At.All.Here.Description
When it comes to the database, you can cut down on outer joins and use inner joins more often because foreign keys will at least point to the null symbols and the foreign key
As you saw in Figure 4-11, I had also added a method to the Order class, AddOrderLine() , for adding OrderLines to the Order . That's part of an implementation of the Encapsulate Collection Refactoring [Fowler R], which basically means that the parent will protect all changes to the collection. On the other hand, the database is the last outpost, and I think this rule fits well there, too. 10. Saving an Order and Its Lines Should Be AtomicAgain, I see Order and its OrderLines as an Aggregate, and the solution I plan to use for this feature will be oriented around that. I will probably use an implementation of the Unit of Work pattern [Fowler PoEAA] for keeping track of the instances that have been changed, which are new, and which are deleted. Then the Unit of Work will coordinate those changes and use one physical database transaction during persistence. 11. Orders Have an Acceptance StatusAs we specified, orders have an acceptance status (see Figure 4-12). Therefore, I just add a method called Accept() . I leave the decision about whether or not to internally use the State pattern [GoF Design Patterns] for later. It is better to make implementation decisions like this during refactoring. Figure 4-12. Order and Status
When we discuss other states for Order , we add more methods. For now, nothing else has been explicitly required so we stay with just Accept() . The current idea could look like this:
[Test]
public void CanAcceptOrder()
{
Customer newCustomer = _CreateACustomer("Karlskrona");
Order newOrder = OrderFactory.CreateOrder(newCustomer);
Assert.IsFalse(newOrder.Status == OrderStatus.Accepted);
newOrder.Accept();
Assert.IsTrue(newOrder.Status == OrderStatus.Accepted);
}
The Domain Model to This PointSo if we summarize the Domain Model just discussed, it could look like Figure 4-13. Figure 4-13. A sketched Domain Model for how I now think I will approach the feature list
Note
The class
ReferencePerson
is in two different Aggregates in Figure 4-13, but the instances aren't. That's an example of how the static class diagram lacks in
What's that? Messy? OK, I agree, it is. We'll partition the model better when we dive into the details further.
Note
The model shown in Figure 4-13 and all the model
Another note is that I only showed the
I said that there are lots of variations of how the Domain Model pattern is used. To show you some other styles, I asked a couple of
To give us a better feeling of the requirements, I'd like to take a look at them from yet another view and sketch a few forms for an upcoming UI. |