Focus on Domain-Related Rules


Let's work with the remaining feature requirements from Chapter 4, one by one. We started out with the feature requirement number 6, which was called "An order may not have a total value of more than one million SEK," but we got interrupted when we realized that the context was missing from the sketched API. Let's see if we can transform the test a bit. It looked like this before:

[Test] public void CantExceedMaxAmountForOrder() {     Order o = new Order(new Customer());     OrderLine ol;     ol = new OrderLine(new Product());     ol.NumberOfUnits = 2000000;     ol.Price = 1;     o.AddOrderLine(ol);     Assert.IsFalse(o.IsValid); }


As you know by now, I think there are several problems with this snippet. To follow the ideas discussed so far, we could add a transition method to Ordered state; for example, the method could be called OrderNow(). But I would still not be happy with that, because OrderNow() would most probably throw an exception before we get a chance to check.

Again, I'd like to leave the focus of what is persisted and what is not for now. The interesting part here is that when we try to transition to Ordered, we can't do that. Let's reflect that with some changes to the test.

[Test] public void CantGoToOrderedStateWithExceededMaxAmount() {     Order o = new Order(new Customer());     OrderLine ol = new OrderLine(new Product());     ol.NumberOfUnits = 2000000;     ol.Price = 1;     o.AddOrderLine(ol);     try     {        o.OrderNow();        Assert.Fail();     }     catch (ApplicationException ex) {} }


It's important to consider whether when we are in the Ordered state if we will then check in AddOrderLine() so that we aren't going to break the rule. I could let AddOrderLine() transition the order back to NewOrder, if AddOrderLine() would be allowed at all. That avoids the problem.

Another problem is that if one of the OrderLines is changed when we are in Ordered, that might break the rule. A typical solution in this case would be to work with immutable OrderLines instead so that they can't be changed. Let's change the test to reflect that.

[Test] public void CantGoToOrderedStateWithExceededMaxAmount() {     Order o = new Order(new Customer());     o.AddOrderLine(new OrderLine(new Product(), 2000000, 1));     try     {        o.OrderNow();        Assert.Fail();     }     catch (ApplicationException ex) {} }


Note

I could use the ExpectedException attribute of NUnit instead of the TRy catch and Assert.Fail() construction. That would make the test shorter. The only advantage of the used construction is that I will check that the expected exception happens at the OrderNow() call, and that is actually most often a small advantage.


When an OrderLine has been added, it can't be changed any longer, and for this simple example, the rule only has to be checked in the OrderNow() and AddOrderLine() methods now.

Let's move on with the list.

Rules that Require Cooperation

Feature number 5 was: "A customer may not owe us more than a certain amount of money." This rule sounds extremely simple at first, but the devil is in the details. Let's start and see where we end up. An initial test could go like this:

[Test] public void CanHaveCustomerDependentMaxDebt() {     Customer c = new Customer();     c.MaxAmountOfDebt = 10;     Order o = new Order(c);     o.AddOrderLine(new OrderLine(new Product(), 11, 1));     try     {         o.OrderNow();         Assert.Fail();     }     catch (ApplicationException ex) {} }


However, that will probably not work at all. Why not? Well, the order needs to be able to find the other orders of a certain state for the customer at hand and what typically solves that is the OrderRepository.

Note

As you might recall, I chose to go for a unidirectional relationship between Order and Customer classes. That's why I ask the OrderRepository for help (as one of several possible strategies for simulating bidirectionality) for going from Customer to its Orders.


We could deal with that by injecting _orderRepository to the order in the constructor like this:

Order o = new Order(c, _orderRepository);


But was this a good solution? As usual, that depends on the particular application. Letting the Order check the current debt for the Customer when it's time for OrderNow() might be a bit late regarding the usability aspect for the user. Perhaps a better solution would be to ask what the maximum amount of a new order can be before creating the order. Then the user creates the new order, and the user can get feedback in the UI when the limit is reached. This doesn't change the API we just saw, but it is more of a convenience.

A variation to the location of responsibility would be to ask the Customer about IsOrderOK(orderToBeChecked). In a way, it feels natural to let the Customer be responsible for deciding that because the Customer has the data that is needed to make the judgment. If we think about the domain and try to code in the spirit of the domain, is it likely that we give (or should give) the customer the responsibility of checking the orders? Perhaps it is if we state the question a bit differently, saying that we should ask the customer to accept the order. As a matter of fact, that's pretty similar to what we coded previously except that OrderNow() was located on the Order. That might be an indication of something.

Another problem with the solution shown in the code snippet is that the _orderRepository is injected in the order instance. If you reconstitute an order from persistence and you do that by using an O/R Mapper, you're not in charge of the creation of the order instance. You could inject the _orderRepository in the Repository during reconstitution, though, but it feels a little bit like a hack (consider, for example, when you read a list of orders and then you have to inject the Repository into each instance of the list). After having thought about it, let's try to keep the responsibility in the order for checking itself, but provide the Repository via a setter instead or at the time of the check as a parameter to the OrderNow() method (but that strategy has a tendency of not scaling well).

Note

I think this discussion points out a weakness in at least some OR Mappers: that you don't control the instance creation during reconstitution as much as you would like, especially considering the growing popularity of Dependency Injection frameworks, as we will discuss in Chapter 10, "Design Techniques to Embrace."


Locating Set-Based Processing Methods

No matter which of the styles previously discussed you prefer, you might think a minute or two about how to check the current debt. Of course, you could write code like this (assuming that we use orders and not invoices for determining the current debt, which is probably a bit twisted):

//Order, code for checking current debt for OrderNow() decimal orderSum = 0; IList orders = _orderRepository.GetOrders(c); foreach (Order o in orders) {     if (_HasInterestingStateRegardingDebt(o))         orderSum += o.TotalValue; }


I think that code is a no-no! A better solution would be to expose a method on the OrderRepository with a method called something like CurrentDebtForCustomer(), taking a Customer instance as parameter.

Note

With Repository interface design, it's often a good idea to provide overloads, such as X(Customer) and X(Guid). It is annoying having to instantiate a Customer just to be able to call the method if you hold on to the ID of the Customer.


I know what the database guys are thinking right now. "What about real-time consistency? Will the Repository method execute with the transaction isolation level of serializable in an explicit transaction during save at least?" Well, it is possible, but there are problems.

One problem is that the O/R Mapper you are using (if we assume for the moment that an O/R Mapper is being usedmore about that in later chapters) might not allow you to take detailed control of the persist process regarding validation, locking, and so on. Another problem is that the nature of the persist process means it might take some time if you have lots of dirty instances to be persisted, so the lock time for all orders for the customer might be pretty long.

I think that in this case it's usually OK not to expect real-time consistency here, only something pretty close to real-time consistency. I mean, check the sum of the other orders just before saving this order, but do not span the read with a high isolation level or not even do the check within the same transaction.

I think that goes pretty much hand in hand with what Evans says regarding Aggregates [Evans DDD]. He says something like strive for real-time consistency within an Aggregate, but not between Aggregates. Consistency problems between Aggregates can be dealt with a little later.

So if you find the approach acceptable (again, we are talking about a pretty low risk here, for a typical system at least), but you would like to detect the problem, you could create some batch program that kicks in every now and then and checks for problems like this and reports back. Or why not put a request on a queue at persist time that checks for problems like this regarding the particular customer? It's such an unlikely problem, so why should any user have to wait synchronously for it to be checked?

The fact that I think the method is atomic signals that we should use a Service [Evans DDD] instead to make the whole thing more explicit. The Service could check the current debt and therefore be used proactively by the Order at good places in time. It could also be used pretty much reactively at save time, and some sort of request could be put on a queue and be used reactively after save. The Service would probably use the same Repository method as was just discussed, but that's not anything the caller needs to know, of course.

What should it do reactively after the save? Perhaps move the order to one of two states so that when the user stores an order, it goes into state NewOrder. Then the Service kicks in and moves the order to state Accepted or Denied, as shown in Figure 7-1.

Figure 7-1. State graph


After all this rambling I think we have three decent solutions to the problem: one with pretty tight coupling (everything being done in real time, possibly with high transaction isolation level), one with less coupling (but that is perhaps a bit more complex because there are more "moving parts"), and finally one where we don't do anything extra beyond what was shown in the test (but with a small risk of inconsistency that we will have to track delayed). Would one of these suit you?

OK, we touched upon using a Service for helping us out with a costly processing problem. Services in the Domain Model often arise from another necessity as well.

Service-Serviced Validation

Completely by coincidence, we do need such a rule that is by its very nature service-based. I'm thinking about feature 8, "Before a new customer is considered OK, his or her credit will be checked with a credit institute."

We could very well call that credit institute service in the Grant() method, or we could use a solution similar to the one I just sketched, letting our Service kick in after persist, taking care of both calling the external service and moving the customer to one of two states. One problem with this approach is that I'm binding domain rules a bit too much to persistence.

Another big problem with this approach of moving some functionality out of the sandbox of the current Unit of Work/Identity Map is that what you might have in your local cache will be out of sync with the database because processing takes place in the autonomous Service. Of course, that's the risk you always have with caching, if the cache could be bypassed.

If you know that the cache will be invalidated because of an explicit Service call in real time, it's pretty easy. Just kill the cache. It might be expensive to recreate it, though, or perhaps you even know exactly what needs to be refreshed. On the other hand, if you don't know when the operation that invalidates the cache will happen, you have to take extra care and refresh the cache as often as possible. As I see it, all this hints at being aggressive with your cache management and not letting the cache get too big and live for a long time, at least not for the data that changes quite frequently.

Note

When I talk about cache here, I'm mostly thinking about the Identity Map [Fowler PoEAA].


Let's focus some more on transitions.

Trying to Transition when We Shouldn't

I used different states for increasing the fulfillment of the "always savable" principle before. But a "critical" passage is when the user wants to make a transition in the state graph. Sure, we could deal with this in a similar way to what we discussed before so that we move to a temporary state and then a Service takes responsibility for moving to the real state or to a failure state. I guess there are situations where this makes sense, but for feature 11, "Orders have an acceptance status that is changed by the user," it might just feel ridiculous, depending on what the context is.

Note

Did you notice how it all came together? I talked about OrderNow() before, and that might have seemed to be kind of a technical solution to the problem of "always savable," but it was core to the requirements. And according to the requirements, Accept() might make more sense.


OK, "ridiculous" is too strong a word, but if we assume that the rules for checking are just a couple of simple constraints without the cross Aggregate border problems, I would prefer to apply the principle discussed earlier: that is, letting the user make a proactive check, and if the result is positive, the state-changing method will execute successfully. A picture, I mean code, says more than a thousand words:

[Test] public void CanMakeStateTransitionSafely() {     Order o = new Order(new Customer());     Assert.IsTrue(o.IsOKToAccept);     //We can now safely do this without getting an exception...     o.Accept(); }


Checking the IsOKToAccept property is optional, but not doing it and then calling Accept() at a time when we shouldn't is exceptional, and we will get an exception. Exceptions are for exceptional problems; for "expected problems" bool is fine.

We often don't just wonder if it will be OK or not, but we also want to get a list of reasons why it's not OK. In these cases, the proactive method could return a list of broken rules instead, something like this:

[Test] public void CanMakeStateTransitionSafely() {     Order o = new Order(new Customer());     Assert.AreEqual(0, o.BrokenRulesIfAccept.Count);     //We can now safely do this without getting an exception...     o.Accept(); }


I prefer to do this on a case-by-case basis. There is no standard interface or anything to implement, just a principle for interface design. So this is actually just a protocol for making it possible to be proactive regarding broken rules detection.

The Transaction Abstraction

The transaction abstraction is a powerful one, and one that is underutilized. An alternative solution to the transition problem would be to use the transaction abstraction for the Domain Model and validations as well. Then during the transaction, it wouldn't matter if the state wasn't correct; only before and after would matter. For example, the API could then look like this:

//Some consumer... DMTransaction dMTx = Something.StartTransaction(); c.Name = string.Empty; c.Name = "Volvo"; dMTx.Commit();


In the code snippet, the rules weren't checked until Commit(). As you understand, this opens up a can of worms that needs to be dealt with, but it might be an interesting solution for the future.

The solution becomes even more interesting if you consider the new System.Transaction namespace of .NET 2.0. In the future, we might get transactional list classes, hash tables, and so on. It gets even more interesting considering transactions spanning both the Domain Model and the database. Again, loads of problems, but interesting.


Obviously there are lots of variations to the problem of state transitions. One such problem is deciding when an action should be taken as well. Stay tuned.

Business ID

The example of a state transition that is paired with an action is feature 7 on the feature list, "Each order and customer should have a unique and user-friendly number."

A spontaneous solution to the problem is letting the database take care of the problem with a built-in mechanism, such as IDENTITY does for MS SQL Server. But there are problems with this approach. First of all, you will have processing outside of your Domain Model, which comes at a cost. You will need to force a refresh of the affected instance, which is not too hard but is something to keep track of and might get a bit complex.

Another problem is that the value might come too early, long before the user has decided to move on with the Order, when he still just considers it to be in "perhaps" state. (Obviously, there are solutions to that as well.)

Yet another problem is that O/R Mappers will often cause you problems if you also use the IDENTITY as a primary key. The semantics for using a Guid as the primary key, which requires less coupling to the database, are often a bit different.

Note

In no way am I saying that you must use the IDENTITY (as the auto increasing property is called in SQL Server) column as a primary key just because you have such a column. You can let it be an alternate key only, but you might get tempted from time to time.


So what are the alternatives? There are several of them. Probably the easiest and most direct one is to let the Repository have a method for grabbing the next free Id in some way. It could look something like this (assuming that an Order shouldn't get an OrderNumber until it's accepted):

//Order public void Accept() {     if (_status != OrderStatus.NewOrder)         throw new ApplicationException             ("You can only call Accept() for NewOrder orders.");     _status = OrderStatus.Accepted;     _orderNumber = _orderRepository.GetNextOrderNumber(); }


Note

Instead of ApplicationException (or a subclass), it might be a good idea to use InvalidOperationException or a derivative.


Yet this opens up a whole can of worms, at least in systems that are somewhat stressed from time to time or if the OrderNumber series is not allowed to have holes in it. It means that the GetNextOrderNumber() won't write changes to the database, or at least it will not commit them. This means that you need an expensive lock wrapping the Accept() call and the following persist method.

Yet another approach is to use what will probably be the next value and then be prepared for duplicates if someone else already has used it.

If you are prepared to catch exceptions during the persist method because of duplicate key, what do you do then? Well, you could call Accept() again, but then Accept() will throw an exception the way it is currently written.

Perhaps it's better to just ask for a new OrderNumber in the code trying to recover from the duplicate key exception. That in its turn creates another set of problems, such as talking to the database during the persist method or detecting which instance has the problem in the first place (and for which column if there are several candidate keys for the instance).

All these problems can be avoided if you accept holes in the number series and if the method for grabbing the next number also commits the current max in a way similar to how IDENTITY works in SQL Server. I mean, IDENTITY allows holes, and it will waste a value if you do a ROLLBACK.

To make it even clearer that the GetNextOrderNumber() actually makes instant changes to the database and doesn't wait for the next call to the persist method, I think we should use a Service instead. My Repositories don't make decisions for themselves when doing a commit and such, but Services do because Services should be autonomous and atomic.

So with these changes in place, the Accept() method could now look like this (assuming that an IOrderNumberService has been injected to _orderNumberService in the order before the call):

//Order public void Accept() {     if (_status != OrderStatus.NewOrder)         throw new ApplicationException             ("You can only call Accept() for NewOrder orders");     _status = OrderStatus.Accepted;     _orderNumber = _orderNumberService.GetNextOrderNumber(); }


Now the risk of problems during the persist method is reduced so much that it's exceptional if one arises.

You'd be right if you think I avoided the problem by accepting holes in series (which was actually even stated as OK with the defined requirements), but sometimes that just isn't possible. I strongly dislike such series, but there are still situations where you can't avoid them. Then what? Well, I guess I would go back to square one. If that wasn't enough, I would probably turn the whole transition method into a Service instead, moving the order to Accepted status and setting an ordernumber. This is not as pure or clean as doing the work in the Domain Model, but when reality calls, we should respond.

OK, so far we have discussed quite a lot of different problems that we have to deal with when it comes to rules in the Domain Model. There is one more to look at before we move on.

Avoiding Problems

Feature 9, "An order must have a customer; an order line must have an order," is a great example of a rule that I'd like to not have as a rule at all if possible, but I'd rather force it in the constructor instead. Then there's no need to check for it.

Clean, simple, efficient. Just good. I know, I know, this sets your alarm bells ringing. Of course there are problems, too.

For example, it's possible that you'd like to have an instance of an order without first having to decide on the customer because of the UI design.

Note

This can be dealt with in the UI, of course, in several different ways. Still, it might not feel good to create a "problem" if we don't have to.


Another problem is that the creation code might get so complex that it can throw exceptions itself, and then we are back into the problems again with how to expose broken rules, and so on. We also said that exceptions should be exceptional, so it just doesn't feel right to toss exceptions in the creation code for saying that there was a broken rule.

There is one thing that eases the burden somewhat. We could let the feature request talk about persistent instances and not transient instances. I'm thinking about OrderLine, for example. In my current design, OrderLine isn't an Aggregate on its own, but is one part of the Order Aggregate instead. Therefore, the only way to make an OrderLine persistent is to associate it to an Order with AddOrderLine(). Until then, I probably don't care that the OrderLine doesn't have an Order. The OrderLine can't be stored, and the persist method won't throw an exception either because of an orphan OrderLine (because the persist method won't know about it at all).

Sure, it might be the case that some processing in the OrderLine requires knowledge of the Order, but if not, I think this is a fine solution.

Note

And right now I think it's arguable whether the OrderLine needs the knowledge of its Order at all. We can probably most often avoid that bidirectionality that we will have otherwise.


I don't know if you've noticed it, but I come back to one of the DDD-patterns, the Aggregate, over and over again. I'd like to take a break and talk about that before moving on.

Aggregates as the Tool Again

The unit for checking rules is... Aggregates! I have found Aggregate design to be extremely important, and it is something I spend quite a lot of time on in my current design work.

I briefly talked about rules aggregation earlier in this chapter, but let's reiterate and delve a little bit deeper.

Aggregate Roots Aggregate Rules

When the consumer of an Aggregate root asks for broken rules regarding persistable or not, he should receive broken rules from the complete Aggregate and not only the root instance.

The aggregation doesn't necessarily have to use the same IValidatableRegardingPersistence interface for the children classes; I actually prefer it not to because that will disturb my strategy for reactively checking for validity at persist. (I don't mean disturb as in creating severe problems, but more as in duplicating execution cycles for no reason.)

Also, this way the Aggregate root has to decide if the ordinary rules of the children should be used or not. Context is king.

An Obvious Aggregate-Level Rule: Versioning

I'd like to end this short section about Aggregates with just a few words about feature 4. Concurrency conflict detection is important. That feature could be thought about as a kind of business rule, but I'd like to think about it as a technical problem more than a business problem. It's also the case that most O/R Mappers have at least some support for concurrency conflict detection, but I'll leave talking about that here and postpone it until Chapter 8, "Infrastructure for Persistence," and Chapter 9, "Putting NHibernate into Action."

Let's change focus a little bit. Before we get into discussing the implementation of the API, let's first see if we should add anything more.




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