A recent thread on the ALT.NET forum got me thinking about KNOWLEDGE LEVEL, a seldom discussed but useful pattern from domain driven design. As with many patterns near the end of the book its a pattern that isn't going to suit every situation but it is a pattern that I think has a lot of value. In summary we aim to explicitly split our model into:
- Operations Level - The place that we do our day-to-day business. For example Shipment, Account.
- Knowledge Level - Objects that describe/constrain the objects in the basic (operations) level. For example EmployeeType, RebatePaymentMethod, ShipmentMethod.
I'm not a big fan of just repeating what's already in books and to my mind if you want to understand DDD then I'd suggest you read the DDD bible. I'm thus not going to go over the pattern, but I did think it was worth a post about what I've found when coming to the detailed design and implementation of classes in a knowledge layer. I thought I'd start by discussing how you can persist/load/model objects in the KNOWLEDGE LEVEL, moving on to other topics in later posts. Two caveats:
- Simple Example - Rather than using a real example I've chosen to use the shipping method example from the thread, I've never worked in that domain so this may be a bad idea but it felt right...
- Might seem obvious - If you are an object/behaviour first type of developer then you'll be looking at using OO techniques and so some of this will just be waffle. However I've seen situations where people used data-driven techniques when approaching their KNOWLEDGE LEVEL and so I wanted to explain why I think it's a bad idea.
Option 1 - Data First Approach
Since we store our operational objects in the database why not do the same for our KNOWLEDGE LEVEL? If we go down this path then we'd have a ShippingMethod table with one row for each shipping method we support. Of course each shipping method has different characteristics, perhaps certain shipping methods are only available to long standing customers or customers in certain countries. To support this we'll end up with extra columns in the ShippingMethod table and maybe even associations to other tables. To get the ShippingMethods from the database we'd probably use a REPOSITORY, probably making sure that the REPOSITORY is read-only (maybe not always, see pattern for information on modifying the KNOWLEDGE LEVEL).
Disadvantages
This approach obviously has disadvantages:
- Loss of clarity - Our ubiquitous language might refer to a "local carrier shipping method" but it won't be visible in the domain model and in fact to see details about this shipping method you need to look at the appropriate row in the ShippingMethod table. Even if you don't value the ubiquitous language this is a problem because as a developer you'll end up switching between the code and database in order to understand even simple aspects of the behaviour.
- Goodbye OO, hello procedural - Since there is no LocalCarrierShippingMethod class I have nowhere to put its behaviour, instead I have to fall back on a procedural style of program where we load in the data about the ShippingMethod with an ID of 3 (which happens to be the local carrier shipping method) and then look at its SupportsInternationalDelivery boolean flag before deciding how to proceed (hopelessly naive example alert). We can encapsulate this logic in a service, perhaps a ShippingMethodSelectionService but its still more than a little smelly.
- No explicit associations - Imagine if we know that the local carrier shipping method is only available for certain types of Orders and that the decision making also takes into account the Customer placing the Order. If we're using the database-driven approach then managing this becomes very difficult, we can't just look at our LocalCarrierShippingMethod class or at some code that sets up the associations. Instead we fall back and run a database query involving all sorts of joins.
Advantages
Those are, as I see it, the primary disadvantages of hiding this important information in the database. Ofcourse its not all bad:
- Consistency - Our KNOWLEDGE LEVEL is handled in the same was the rest of the domain model, loaded from the database as required. Not sure its a massive advantage though because we've explicitly chosen break out the KNOWLEDGE LEVEL its fair enough to say that the consistency has limited value.
- Flexibility - If we want to add a new kind of ShippingMethod we just add a row to the ShippingMethod table, no need to redeploy. That's the idea anyway, but its not always going to work especially when the related procedural code has to change.
Its also fair to say that the KNOWLEDGE LEVEL changes at a different pace to the rest of the model, we probably don't add ShippingMethods too often. However we might want to be able to change characteristics of existing ShipmentMethod's, such as changing their price (remember this probably only affects future Shipments) or changing what Countries they can be used in. So on the flexibility angle you've actually got a couple of types of changes:
- Adding/removing concepts - Perhaps adding a donkey based shipment method, to me its safe to require a redeploy for this sort of change and in any case without some serious thought we're not necessarily going to be able to do just by modifying a table anyway.
- Changing configuration/associations - Its maybe fair enough to expect to be able to ban using donkey based shipping for all future orders in Spain without requiring the code to be redeployed. As I'll show below this can work even if you go for an object-oriented approach and I also think this is a case where a DSL would really add value (not tried that though).
Those are the main advantages I've heard about, and as I say I'm not sold on them. That brings me on to how I think you should go about it...
Option 2 - Object Oriented Approach
Take that "local carrier shipping method" concept and turn it into a LocalCarrierShippingMethod class, probably inheriting from a ShippingMethod base class. There's only one instance of this class and since its read-only (at least as far as normal usage goes) it's totally safe to share it. The ShippingMethod has any data it needs to support the behaviour it contains and to support the interface that it exposes, so for example you might have an IsApplicableForSending(Shipment) with each subclass providing their own implementation.
Implementation
How does this look in practice, well one approach I've used is to a variant of the typesafe enum pattern. The following is just pseudo code to show the idea:
public abstract class ShipmentMethod
{
private static Dictionary<ShipmentMethodKind, ShipmentMethod> _shippingMethods = new Dictionary<ShipmentMethodKind, ShipmentMethod>();
static ShipmentMethod()
{
// NOTE: In practice you wouldn't do it like this but it is just an example....
_shippingMethods.Add(ShipmentMethodKind.LocalCarrier, new LocalCarrierShippingMethod());
_shippingMethods.Add(ShipmentMethodKind.DonkeyBased, new DonkeyBasedShippingMethod());
}
public IEnumerable<ShipmentMethod> GetAll()
{
return _shippingMethods.Values;
}
public ShipmentMethod GetByKey(ShipmentMethodKind key)
{
return _shippingMethods[key];
}
public abstract bool IsApplicableFor(Shipment toEvaluate);
}
public enum ShipmentMethodKind
{
LocalCarrier = 0,
DonkeyBased = 1
}
public class LocalCarrierShippingMethod : ShipmentMethod
{
public override bool IsApplicableFor(Shipment toEvaluate)
{
Don't get too hung up on the implementation, some of it is optional (e.g. ShipmentMethodKind) and it could be refractored quite a bit, I'm really just trying to show the idea not to show how to actually implement a solution.
You can see that in this case ShipmentMethod is really just a SPECIFICATION but in real situations a ShipmentMethod might well have more behaviour and data. The base class gives us an easy way to access all the ShipmentMethods that the system handles, this can be useful because it could easy provide useful methods like GetAllShipmentMethodsThatCanHandle(Shipment).
In reality there are multiple ways of implementing this, in particular you might want to load in the configuration for each ShipmentMethod from a database or other data source. This is more flexible than including it in the code but slightly more complicated to implement.
Referential Integrity
If you choose not to load the data from the database then we might seem to have a problem, how do we associate this ShipmentMethod with other objects in the KNOWLEDGE LEVEL and operational level?
It would seem that we've lost referential integrity but we have choices:
- Generate tables - The code in the static constructor in ShipmentMethod can be used to generate a table in the database.
- Use enum value as key - We can instead treat the value of the ShipmentMethodKind as a "key" in the database, automated tests will make spotting any issues really quick.
- Have separate config table - As I said earlier we may want to load the configuration information for each ShipmentMethod from the database, if so we have a ShipmentMethod table which resolves the problem.
In any case I haven't found this to be a major issue, by and large I don't care that my ShipmentTable has a ShipmentMethodId table which doesn't lead me anywhere as if I want to understand what's going on I go back to the domain model.
In Closing
Not sure this topic needed as much details I've put in here, really its the KNOWLEDGE LEVEL pattern that's key but I hoped that by enumerating some of the implementation choices that I've seen I'd help you make an informed decision.