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

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 strange because the architecture must first and foremost depend on the problem at hand.

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 evolve in later chapters.

Problems/Features for Domain Model Example

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

  1. List customers by applying a flexible and complex filter.

    The customer support staff needs to be able to search for customers in a very flexible manner. They need to use wildcards on numerous fields such as name, location, street address, reference person, and so on. They also need to be able to search for customers with orders of a certain kind, with orders of a certain size, with orders for certain products, and so on. What we're talking about here is a full-fledged search utility. The result is a list of customers, each with a customer number, customer name, and location.

  2. List the orders when looking at a specific customer.

    The total value for each order should be visible in the list, as should the status of the order, type of order, order date, and name of reference person.

  3. An order can have many different lines.

    An order can have many order lines, where each line describes a product and the number of that product that has been ordered.

  4. Concurrency conflict detection is important.

    It's alright to use optimistic concurrency control. That is, it's accepted that when a user is notified after he or she has done some work and tries to save, there will be a conflict with a previous save. Only conflicts that lead to real inconsistencies should be considered conflicts. So the solution needs to decide on the versioning unit for customers and for orders. (This will slightly affect some of the other features.)

  5. A customer may not owe us more than a certain amount of money.

    The limit is specific per customer. We define the limit when the customer is added initially, and we can change the limit later on. It's considered an inconsistency if we have unpaid orders of a total value of more than the limit, but we allow that inconsistency to happen in one situation, that is if a user decreases the limit. Then the user that decreases the limit is notified, but the save operation is allowed. However, an order cannot be added or changed so that the limit is exceeded.

  6. An order may not have a total value of more than one million SEK (SEK is the Swedish currency, and I'm using it for this example).

    This limit (unlike the previous one) is a system-wide rule.

  7. Each order and customer should have a unique and user-friendly number.

    Gaps in the series are acceptable.

  8. Before a new customer is considered acceptable, his or her credit will be checked with a credit institute.

    That is, the limit discussed in step 5 that is defined for a customer will be checked to see if it's reasonable.

  9. An order must have a customer; an order line must have an order.

    There must not be any orders with an undefined customer. The same goes for order lines: they must belong to an order.

  10. Saving an order and its lines should be atomic.

    To be honest, I'm not actually sure that this feature is necessary. It might be alright if the order is created first and the order lines are added later, but I want the rule for this example so that we have a feature related to transactional protection.

  11. Orders have an acceptance status that is changed by the user.

    This status can be changed by users between different values (such as to approved/disapproved). To other status values, the change is done implicitly by other methods in the Domain Model.

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

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 please note that we will come back to those in later chapters and talk and show a lot about them then. (See Appendix B.)


Without further ado...

1. List Customers by Applying a Flexible and Complex Filter

The 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 sketches, just a quick idea of what I think I can start with. The code is the executable, crisp artifact. The code is the most important representation of the real model.


As you see, I used a simple (naïve) 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 implementations, but let's use simple structures for now.


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 sort order, other hidden criteria, optimizations, where to send the Query Object for execution, and so on. I therefore prefer to use a Repository [Evans DDD] for encapsulating the query execution a bit. It also cuts down in code verbosity for the consumers and increases the explicitness in the programming API.

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 persisted before. The Repository will then use the infrastructure to fulfill its task.

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 names. Or slightly better, we force the user to add an asterisk if he or she isn't interested in the name. Anyway, neither is a very good solution. On the other hand, going for a Query Object [Fowler PoEAA], especially a domain-specific one, or the Specification pattern [Evans DDD] is to start by going too far, too quickly, don't you think? Let's go for the simplest possible and only consider two of all possible criteria for now. See Figure 4-3 for an example of how this could be done.

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 next chapter, we will take a step back and more thoroughly and true to TDD try out and develop the model.

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 bidirectionality costs. It costs in complexity, tight coupling, and overhead. Therefore, I think it's good enough to be able to get to the Orders for a Customer via the OrderRepository. This leads to the model shown in Figure 4-4.

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 Lines

Feature 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 many-to-one, but so far we think that the one-to-many relationship isn't really interesting in this particular Domain Model.

4. Concurrency Conflict Detection Is Important

Hmmm...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 problematic because it's then a bit too transparent, so that the consumer sees no cost at calling the property. It's also a consistency issue. I'm not even seeing the Customer Aggregate as having the Orders, so I don't expect the unit to be the correct one for assuming consistency handling directly in the Domain Model, which is a good sign that I should look for another solution.

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 SEK

Because 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 Number

As 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 regret this sketch a bit. This means that I allocate an OrderNumber as soon as the Order is persisted the first time. I'm not so sure that's a good idea.

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 Institute

So 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 remembered). Perhaps something like Customer.RequestCreditLimit(1000) would be a better solution. We get back to similar discussions in Chapter 7, "Let the Rules Rule."


9. An Order Must Have a Customer; an Orderline Must Have an Order

Feature 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 parts in a test as usual.

[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 members, such as string.Empty for the strings). That way you can always be sure to be able to "follow the dots" like this:

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 columns will be non-nullable. To summarize, null objects increase the simplicity a lot and in unexpected ways as well.

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 Atomic

Again, 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 Status

As 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 Point

So 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 expressiveness, but also an example that is simply explained with a short comment.


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 fragments were created up-front, and they are bound to be improved a lot when I move on to developing the application.

Another note is that I only showed the core Domain Model and not things that are more infrastructure related, such as the mentioned Unit of Work [Fowler PoEAA]. Of course, that was totally on purpose. We'll get back to infrastructure later in the book. Now I'm focusing on the Domain Model.


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 friends of mine to describe their favorite ways of applying Domain Models. You'll find those in Appendix A.

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.




Applying Domain-Driven Design and Patterns(c) With Examples in C# and  .NET
Applying Domain-Driven Design and Patterns: With Examples in C# and .NET
ISBN: 0321268202
EAN: 2147483647
Year: 2006
Pages: 179
Authors: Jimmy Nilsson

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net