Starting to Create an API


We'll come back to these principles and evaluate how we fulfilled them later in the chapter, but now it's time to come back down to Earth and take up some of the problems outlined in Chapter 4. I'm going to start with the external API and see where we end up. I believe that will give a good, pragmatic feeling about the whole thing.

So to remind you, the problems or requests outlined in Chapter 4 were the following:

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

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

  3. An order can have many different lines.

  4. Concurrency conflict detection is important.

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

  6. An order may not have a total value of more than one million SEK.

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

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

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

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

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

Let's take one of the problems; say, problem 6, "An order may not have a total value of more than one million SEK."

How shall we approach this problem? There are several options to choose from.

You could create something like a simple rule-checking engine where you can send your instances to be validated, or you could let the instances take responsibility for that themselves. I like the idea of letting the instances take responsibility for themselves as much as possible (or at least as far as is appropriate) and therefore prefer the latter. Perhaps this is a bit naïve, but also very simple in a positive way.

If I write a test to get started, it could look like this:

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


What we did was to add a property to the Order called IsValid. A pretty common approach I think. There are also some obvious problems, such as the following:

  • What was the problem? We only know that there was one, not what it was.

  • We allowed an incorrect transition.

  • What if we forgot to check?

All we did was to check if everything was OK or not. We will come back to the mentioned problems, but we have something more basic to deal with first. A more important problem is this question:

What is it the order is valid for (or not valid for)?

Context, Context, Context!

Context is, as always, very important but pretty much forgotten in the approach we started out with. We need to address this before moving on because it's crucial to the rest of the discussion.

I think the reason for the common approach of IsValid as some kind of general, catch-all test might be because of too much focus on persistence. If we decouple persistence from rules a bit and try to adhere to the principle of letting all states be savable, we might end up with another approach.

Then I think the transitions will be the interesting thing regarding rules. For example, when an order is in the state of NewOrder, more or less everything might be allowed. But when the order should transition to the Ordered state, that will only be allowed if certain rules are fulfilled.

So saving an order that is in the state of NewOrder should be possible even if the order isn't valid for enter Ordered state. Approaching rules this way will focus rules on the model meaning and not let a technicality, such as persisted or not, affect the model.

That said, there is a reality full of practical problems, and we need to deal with that well. So even if we have the intention of focusing on the model, we have to be good citizens regarding the infrastructure also. Let's see if thinking about persisted or not as a special state helps, but first I think it's important to discuss what could give us problems regarding rules if we don't think enough about the infrastructure.

Database Constraints

Even though you might choose to focus on putting all your rules in the Domain Model, you will still probably end up with some rules in the database. A typical reason for this is efficiency. Some rules are most efficiently checked in the database; that's just how it is. You can probably design around most of them, but there will probably still be a couple of situations where this is a fact of life.

Another reason is that you might not be able to design a new database, or you might not be able to design the database exactly as you would like it, but you have to adjust to a current database design. Yet another reason is that it's often considered a good thing that the database can take care of itself, especially if several different systems are using the same database.

The impedance mismatch between object-orientation and Relational databases that we discussed in Chapter 1, "Values to Value," will also show up here. For example, the strings in the database will most likely have static max lengths, such as VARCHAR(50). That a certain string isn't allowed to be longer than that is kind of a generic rule, but it's mostly related to persistence. The transition to Persisted state won't be allowed if the string is too long.

So it's probable that you will have to deal with rules whose invalidity won't be detected until the state is persisted to the database. That in its turn represents a couple of problems.

First, parsing error information is troublesome, especially when you consider that a typical O/R Mapping solution often tries to be portable between different databases. Or rather, the heart of the problem is that the O/R Mapper probably won't be able to do the mapping for you when it comes to errors and tell you what field(s) caused the problem in the Domain Model; for example, when there's a duplicate key error.

Furthermore, the error will come at an inconvenient point in time. Your Domain Model has already been changed, and it will probably be very hard for you to recover from the problem without starting all over again. In fact, the general rule is often that you should start over instead of trying to do something smart.

Still, we do like the idea of making it possible to save our instances at any time. (In any case, we have set up some preferences even if we can't follow them to 100%. The extreme is often extremely costly.)

As I see it, try to design for as much proactivity as possible (or as appropriate) so that the number of exceptions from the database will be extremely few. For those that are still possible, be prepared to deal with them one by one in code, or be prepared to roll back the transaction, toss out the Domain Model changes, and start all over again. A bit drastic, but that's the way I see it. The key to all this is design, so you make the problem as small as possible.

OK, that was the purely technical side of it (that the database might show its discomfort with us). But it's not just a matter of that the rules are infrastructure-related only or domain-related only. There are connections.

Bind Rules to Transitions Related to the Domain or the Infrastructure?

What we have said so far is that we should try hard to not enter an invalid state. If we follow that, we can always persist (if we don't think about the infrastructure-related constraints for the moment).

That principle is the ideal. In reality, it will be hard not to break certain rules during a multiple step action. For example, should you disallow the change of the name of a customer to a name that the rule thinks is too short? Well, perhaps, but it might just prevent the user from doing what he wants to do: namely deleting the old name and inserting a new one instead.

It's possible to deal with this by having a method called Rename(string newName) that takes care of the work. During the execution of the method, the state of the object isn't correct, but we only consider it a problem before and after the method execution.

Note

This is in line with invariants according to Design by Contract [Meyer OOSC]. Such invariants are like assertions that the instance should abide by at all times, both before the method execution and after. The only exception is during the method execution.


Another approach is to simply not set the property until you've finished with the change. Let's see if I can come up with another example. Assume that there are two fields, and either both field have to be set or neither of them. Again, a method would solve the problem, but if you scale up this problem or consider that the UI would prefer to have the properties exposed with setters, it's clear that it's hard not to get into an inconsistent state there from time to time.

Another question might be if we should trust that we didn't reach an invalid state? And if we against all odds did reach an invalid state (a really invalid one, such as an erroneous change when the state of the order is Ordered), should we allow that to be stored?

It might be that the database won't allow us to have that protection for some error situations. But even if you try hard to let the database take care of itself regarding rules, you will probably let some things slip through, and if you have moved to Domain Model-focused way of building applications, your database is probably not as rigid when it comes to self-protection. After all, domain-focused rules that are somewhat advanced are most often much easier to express in the Domain Model than in a Relational database, and you also want to have them in the Domain Model.

The question is: can we trust the "environment"? I mean, can we trust that we won't persist a state that shouldn't be possible to reach? For example, can we expect that we don't have any bugs? Can we expect that the consumer isn't creative and won't use reflection for setting some fields to exciting values?

Refining the Principle: "All States, Even when in Error, Should Be Savable"

Let's see if we can form an example of how I think about the principle I called "All states, even when in error, should be savable."

Assume again an order that can be in two states, NewOrder and Ordered. We have only one domain-related transition rule: the order may not be too large to enter Ordered. That gives us the valid combinations in Table 7-1.

Table 7-1. Valid Combinations of Order States and Rule Outcomes

Order States

Rule Outcomes

New Order

Not too large

New Order

Too large

Ordered

Not too large


So far, it feels both simple and exactly what we want. Let's add the dimension of persisted or not, as shown in Table 7-2.

Table 7-2. Valid Combinations of Order States and Rule Outcomes

Order States

Rule Outcomes

Persisted or Not

New Order

Not too large

Not persisted

New Order

Not too large

Persisted

New Order

Too large

Not persisted

New Order

Too large

Persisted

Ordered

Not too large

Not persisted

Ordered

Not too large

Persisted


First, I'd like to point out that we expect that the transitions to Ordered combined with Too large will not be possible.

Even more important, if for some reason it does happen, it's an example of a state that is in real exceptional error and that should not be savable.

Note

Your mileage might vary regarding if you will check the domain-related transition rules at persist time as well or not. It might be considered a bit extreme in many cases.


Please note that by adhering to the refined principle of "all states should be savable," we are not back to the IsValid idea I started the chapter with. It's now IsValidRegardingPersistence, and lots of "errors" are now OK to persist.




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