"Fundamentally, Domain Driven Design is the principle that we should be focusing on the deep issues of the domain our users are engaged in, that the best part of our minds should be devoted to understanding that domain, and collaborating with experts in that domain to wrestle it into a conceptual form that we can use to build powerful, flexible software." -Eric Evans
Introduction: Eric Evans in his excellent book, Domain Driven Design, organizes common patterns and practices into a logical framework for developing software. Most of what he writes is not necessarily new, but he brings a common vocabulary and organization to design that is very helpful.
- Putting the Domain Model to Work
- What is a Domain
Domain: A sphere of knowledge, influence, or activity.
We need to concentrate on the domain of software - really learn the business environment of our software. Since the entire business domain is too large for our meager brains, we construct a model of the domain, a subset of the actual business domain, that is essential to our software.
For many projects the primary problem in writing software is the complexity of the domain. The problem is not all the plumbing of GUIs and data persistence, but the domain.
This is not true of all projects. Many smaller projects will not benefit from Domain Driven Design. The larger, more interesting projects need a clean, well conceived model.
- Modeling
"Financial analysts crunch numbers... domain modelers are knowledge crunchers." - p13 DDD
Definition for Model: A system of abstractions that describes selected aspects of a domain and can be used to solve problems related to those aspects of that domain.
Domain Driven Design is based on Model Driven Design which puts the Model at the heart of development.
Key Aspects of Models:
- The Model should not be as realistic as possible. It should not contain aspects of the domain that don't end up in code.
- UML diagrams are not sufficient to convey the concepts behind a model. Use different diagrams types and written documents to describe a model. The diagrams and documents are not the model - the model is held in the heads of people.
- Developing an effective domain model takes many conversations between the software developers and the domain experts. This is an iterative process.
- Extensible software needs a deep domain model.
- Models of the same thing are different based on the requirements. Use a road map when touring in a car, use a subway map for traveling the underground.
- Ubiquitous Language (I'm kinda of a big deal - really!)
- To effectively communicate between business personal and software developers a common language is needed, a "Ubiquitous Language". The Ubiquitous Language should make sense to the domain experts. The software team needs to learn the language of the domain experts.
- A historical model is the use of a pidgin language. When two groups interact without sharing a common language, a pidgin language is often used. The new language is not very expressive, but powerful enough to conduct simple trade agreements.
- The terms used in the Ubiquitous Language need to be used in the software itself as object and method names.
The Model Should Write the Code
A project should not have an "Analysis Model" and a "Programming Model", but a single model. If the model cannot be efficiently turned into code, the model must be refactored.
If you are working for Hogwarts School and they say "A wizard casts a spell", your code should look like this:
wizard.cast(spell);
not this:
spellCaster.Implements(magicDocumentText);
One more time: The Ubiquitous Language lives in your code.
- What is a Domain
- The Building Blocks of a Model-Driven Design
- Layering
Cohesive software should be layered together and only depend on the layers beneath it.
A layer depends only on the layers beneath it. When a layer communicates upward it does so through an interface.
Advantages of Layered Systems:
- Components can evolve separately
- Layers can be deployed more easily on separate servers
- Objects in a layer can be more effectively tested using test doubles for the layers above and beneath
- Layers provide a natural separation for teams to divide the work
- Layers, like persistence, can be replaced easily when better software becomes available
- Layers are easier to understand and maintain
(The "Smart UI". On small projects being developed by entry level programmers, using a "Smart UI", where all the layers are munged into one, makes sense because the overhead of doing a Model Driven approach is too costly.)
Example Layering of Four Concepts:
Layer Description User Interface Shows the user information and receives input. (aka 'View') Application Thin layer to coordinate application activity. No business logic or business object state is in this layer. (aka 'Controller') Domain All the business objects and their state reside here. Objects in the domain layer should be free of concern about displaying or persisting themselves. (aka 'Model') Infrastructure The objects dealing with housekeeping tasks like persistence. (aka 'Plumbing') A clean design starts like this:
Then, some of the younger programmers start to work on it:
Then slowly over time you get this:
What's wrong with mixing the layers just a bit?
- Object Types
- Entities - have an identity that persists over time. The identity is independent of the state of its attributes.
Entity objects with the exact same attributes are different objects.
- Value Objects - have no identity.
- If two value objects have the same attributes they are considered the same.
- If Value Objects are sharable they should be immutable.
- Value Objects may contain other Value Objects and references to Entity Objects.
- Value Objects should only have constructors and functions (no side-effects) since they are immutable.
Are all pennies interchangeable?
- Service Objects
Unfortunately Eric used the way-overloaded term "Service" for this next thing. To differentiate it from other services, like "web services", DDD services are sometimes referred to as "Domain Services".
- A Domain Service provides functionality when the service doesn't neatly fit into an object.
- A service has no internal state, so it can be called repeatedly with the same input and it always gives the same output.
- Services may span several objects.
- Putting Service functionality into domain objects creates too many dependencies between objects.
- Services can be in the domain, infrastructure, or application layer.
- Example:
TransferService.Transfer(Account from, Account to, Money amount);
A waiter is a form of service. You could get the food yourself, but you'd have to learn where the kitchen is, where are those trays, who is the cook. You'd have to learn a bunch of things which really aren't what you are about. It's easier to know a waiter.
- Modules.
Put related software objects together in Modules. Two methods of grouping:
- Communicational cohesion - objects that operate on the same data
- Functional cohesion - objects that cooperate together to perform a task
Individual software objects should have high cohesion and low coupling. This is also true of modules. Modules should have a few well-defined interfaces to the outside world and high cohesion.
Modules should have low coupling. An object from Module A should not call four objects from Module B, that would be high coupling, instead it should make a single call to an interface on Module B that queries the needed four objects.
Modules should have names from the Ubiquitous Language.
- Aggregates - a domain pattern dealing with ownership and boundaries of objects.
- An aggregate is a group of related objects. An Aggregate has one root which is an Entity, called the "Aggregate Root".
- Access to an aggregate member must come only from its root. An outside object cannot hold a real reference to an aggregate member, only the local one provided by the root.
- When the root object is deleted from memory, all its aggregates are flushed as well since they can only be accessed from the root.
- If all the aggregate objects are deleted when the parent is deleted, that's a good sign you have a true aggregate. If the aggregate objects still have a meaningful life after their Aggregate Root is deleted, you have references, not an Aggregate Root.
- Aggregate Roots interact with Repositories, but the children of the Aggregate Root do not.
- Aggregates can impose rules on the objects it manages enforcing consistency.
- Being able to treat all members of an Aggregate Root as one is the advantage of this pattern. We can delete the root with confidence that all its members are deleted and we can persistent the root with confidence that all its children are persisted.
- Factories - used to create new complex objects.
Creation should be atomic. The factory gives the object an identity.
carFactory.CreateCar(Engine engine, Body body, Transmission transmission, Interior interior);
- Repositories
- Repositories do not create identity.
- Repositories are a Facade pattern over the complexity of getting objects from storage, usually a database.
- Repositories look like a collection to your application, like a List<T>.
- Repositories give our objects Persistence Ignorance (PI). The objects themselves have no idea how they will be stored.
customerRepository.GetById(int id); customerRepository.GetAllByZipCode(string zipCode); customerRepository.GetByAddress(Address customerAddress);
- Entities - have an identity that persists over time. The identity is independent of the state of its attributes.
- Layering
- Refactoring Toward Deeper Insight
- Making Implicit Concepts Explicit
The code needs to tell the maintainer what the code is doing. Instead of hiding functionality deep in the bowels of the code, a good programmer makes the intent of the code clear by several means.
- Specifications
Specification objects are value objects that return a true or false, taking an object as an argument. Specifications make the intent of the code clearer and allow for reuse. Specification objects are useful in these three scenarios:
- Validating that an object meets the specification.
Is this customer eligible for special terms?
- Selecting from a collection
Give me all the customers that are 60 days behind in payments
- Creating an object
Create an object that obeys all these criteria...
Example code of first two reasons
using System; using System.Collections.Generic; using NUnit.Framework; namespace PlayingAround { class SpecificationsExample { internal class Animal { public string Name; public bool IsWarmBlooded { get; set;} public bool LaysEggs { get; set;} public bool HasHair { get; set;} } public class MammalSpecification { public static bool IsSatisfiedBy(Animal animal) { return animal.IsWarmBlooded && ! animal.LaysEggs && animal.HasHair; } } private static void Main() { Animal horse = new Animal {Name = "horse", IsWarmBlooded = true, LaysEggs = false, HasHair = true}; Animal turtle = new Animal { Name = "turtle", IsWarmBlooded = false, LaysEggs = true, HasHair = true }; Assert.IsTrue(MammalSpecification.IsSatisfiedBy(horse)); List<Animal> animals = new List<Animal> {horse, turtle}; List<Animal> mammals = animals.FindAll(MammalSpecification.IsSatisfiedBy); Assert.IsTrue(mammals.Contains(horse) && mammals.Count == 1); Console.Out.Write("press return to exit.");Console.In.ReadLine(); } } }
- Validating that an object meets the specification.
- Supple Design
In Chapter 10, Eric Evans gives some advice on making a design more flexible.
- Pure Functions and Command Methods
Pure Functions do not change state. They only query, do calculations, and return a value. Pure Functions can be called repeatedly and always return the same value when given the same inputs (technically this is called idempotence). Use as many Pure Functions as possible since they are simpler and easier to test.
Commands are methods that change the state of an application. Although they can return a value, it is better to refactor the method to only change state.
A good method should be either a Command or a Pure Function, but like oil and water, they don't mix.
- Low Coupling
Like in humans, relationships between objects complicates things. Try to reduce the connections between objects. The ultimate goal would be to have a StandAlone object that has no dependencies on any other classes.
- Closure of Operations
"Closure of Operations" means you can perform an operation and return the same type as its argument. For example,
Person parent = GetParent(Person student);
GetParent() takes a Person and returns a Person. This helps reduce the mental load on the developer since only one type of object is being used. It also helps to string method calls together in that trendy "fluent" style.
"Closure of Operations" is usually done with Value objects.
- Put Complex Logic in Value Objects
Moving complex logic and calculations into value objects with pure functions makes testing easier and modifications to the calculations simpler.
- Pure Functions and Command Methods
- Directory Structure Maps to the Design
The directory structure of a project should mirror a clean division between Domain objects and other objects. The Domain object directory should not have references to libraries of supporting code like persistence, just to Interfaces for those objects.
- Analysis Patterns
Believe it or not you are probably not the first person to write software in your domain. Books such as the one below can walk you through some common domains:
Analysis Patterns: Reusable Object Models - Ways to Clarify the Model.
- Constraints
Constraints are a way of forcing an invariant. Making Constraints separate objects helps to clarify the intent and provide a flexible future.
- Processes
These should be implemented as a Service object.
- Specification
A test to determine if an object obeys a criteria.
- Constraints
- Making Implicit Concepts Explicit
- Strategic Design
- Preserving Model Integrity
Unification is the term for a model's internal consistency.
- Continuous Integration
As time goes on, the model will change. The model should be checked for errors as developers add new functionality to ensure no duplicate code is written and the objects maintain their purpose. Code integration should be done as soon as possible, ideally after each checkin. All the unit and integration tests should be run as frequently as needed.
- Shared Kernel
On large projects, sometimes separate models intersect functionally and need to have shared code. This should be done very carefully with both teams notifying the other of changes in their shared code. The shared kernel is often the core domain objects.
- Customer-Supplier
When two teams are competing for rights to modify a shared resource, one team should play the role of customer and the other the supplier. Changes should be carefully coordinated between the two teams.
- Conformist
When the Customer-Supplier relationship breaks down, perhaps from economic or political reasons, the Customer must simply conform to the code/database schema of the Supplier.
- Anticorruption Layer
When two systems that do not share a common model need to communicate, a translation service may need to be created that acts as a Facade and Adapter pattern to move data between models.
- Separate Ways
When two systems have little in common, sometimes it's better to have them not tightly integrated, but have them go their separate ways with their own bounded context, with just a thin veneer like a GUI holding them together.
- Open Host Service
When many systems need to communicate with a particular subsystem, instead of writing full translators for each system, create a generic set of services that can be used by all other systems with only minor modifications.
- Shared Language
Sometimes by creatubg ir using a published language, communication between multiple applications can be easier.
- Continuous Integration
- Deep Refactoring
Sometimes in the life of a project you have a new eureka moment of understanding the underlying domain. It's time to do major refactoring. A clue for this need is when you customers don't understand you object model. Another clue is when the customers keep using a term that is not in your domain model.
- Constant Refactoring
As your knowledge of the domain increases you should refactor your code often. Meet as a team over a few days, sleep on the design changes, then be courageous and make big changes.
As Miss Frizzle says on the Magic School Bus: "Take chances, make mistakes, get messy".
- Strategic Design:
- Bounded Context
In large projects, multiple models may describe different parts of the system. Make sure the boundaries between the models is clear. Define the relationship between code bases and models. Know when to use which model.
Within a bounded context splintering may occur. When two people have different meanings for the same term, these are called "false cognates". People may also duplicate code funtionality. This should be rigorously refactored.
Each model needs to have "unification", be internally consistent and have unambiguous terms with no rules conflicting.
How do all the contexts in a large project relate? This should be described in a Context Map in the form of diagrams, text, or conversations.
- Distillation
Often in larger projects the core business objects are mixed in with other supporting objects in the same modules. To better focus on your core objects, separate the other objects into their own modules which have no knowledge of the core objects.
The better developers tend to choose to work on the more technically interesting aspects of a system which add to their personal technical resume, leaving less experienced people to work on the core business objects. This can lead to disaster. Put your best developers on the core module.
When a domain gets too large, refactor the model to make it smaller and put less critical business models into generic domains. Only have the central business objects in a small core domain. A few ways to do this:
- Generic Subdomains
Put non-core objects that are vital to your software into their own generic subdomain. These objects are needed by your core objects, but can be organised into their own modules. This allows the generic modules to be worked on by other parties and not take the time of your core module developers.
Eric gives an example of time zone calculations as a generic subdomain. The time zone code is critical to the application, but can be put into a module which has no awareness of the core business objects.
- Domain Vision Statement
A Domain Vision Statement is a few paragraphs describing what is in the core domain and perhaps what is not. This can guide the partitioning of the system into core and supporting modules.
- Hilighted Core
The core objects need to be highlighted to increase their visibility. This can be done by writing a few page description of them called the "Distillation Document", highlighting them in a longer document, or adding attributes into the code.
- Cohesive Mechanisms
Cohesive mechanisms are similiar to Generic Subdomains in that they reduce the clutter in the core domain by moving the complexity to another place, but Cohesive Mechanisms work with complex operations and algorithms.
- Segregated Core
- Generic Subdomains
- Large Scale Structure
Eric Evans discusses four ways to help manage the complexity of large projects.
- System Metaphor
A system has an easily understood metaphor that drives the design.
- Responsibility Layers
Modules are placed into a few layers which are dependent on the layers below them. Modules in the same layer have a cohesive story and have a similar rate of change.
- Knowledge Level
A layer of software constrains other components on their behavior.
- Pluggable Component Framework
A Framework that is accessed by interfaces can be replaced with a different framework easily.
- System Metaphor
- Bounded Context
- Preserving Model Integrity
- Books:
Domain Driven Design
Domain Driven Design Quickly