Domain Driven Design with Web API revisited Part 6: associations and standalone classes
August 24, 2015 2 Comments
Introduction
In the previous post we built up the objects that will surround the central domain object, i.e. Loadtest. We saw examples of entities and value objects. We are deliberately keeping the objects simple so as to keep the size of the demo manageable for a blog like this. Always keep in mind that real-life domain objects will be more complex than those but our main goal is to concentrate on a small section.
In this post we’ll connect the dependent objects with Loadtest. We’ll also look at two new concepts from DDD: associations and standalone classes.
Associations
An association is probably not a DDD-specific term but it’s important to understand what it is and how it affects our domain implementation in code. An association between two objects shows the relationship between them. Associations are very common in database design: one-to-one, one-to-many, many-to-one and many-to-many. Similar relationships can be found in code:
- A customer can have one or more projects and each project belongs to a single customer
- An order can have one or more order lines and each order line will belong to a single order
Another important term related to associations is traversal direction. Consider the following possible solution for the first item in the above list: the Customer object might have a property of type IEnumerable of Project. In turn, a Project will have a property of type Customer. This is an example of a bi-directional traversal direction. We can find the projects of a customer and the customer that belongs to a project:
Customer.Projects Project.Customer
This seems straightforward and logical so we might be tempted to build up our Loadtest object like that, i.e. with direct references to all of the below objects we created in the previous post:
- Agent
- Customer
- Engineer
- Project
- Location
In turn each of the above objects will have a property pointing back at the owning Loadtest object. Or in fact it would be more complex as e.g. an Engineer can work on multiple load tests and an agent can generate the load for multiple load tests.
There’s an inherent difficulty with relationships like that. If Customer references a Project and a Project references a Customer then the one cannot exist without the other. They are tightly coupled with each other. There’s no way a programmer can look at Customer without having to view Project as well. We’ll face another challenge when we’re trying to load a Customer object from the data store. If we ignore to load the list of projects of that Customer then the Customer object will be in an inconsistent state. Trying to call…
Customer.Projects
…will fail with a null reference exception. The problem is aggravated if a Project object references other domain objects. You’ll need to populate them all as well.
I’ve already told you that the demo for this series is based on a real-world load testing domain at the company I currently work for. It’s also clear that the scope of the demo domain is very limited compared to the real one so that we can focus on a small section of the domain. However, let me share with you just how complex a domain can be, based on the full Loadtest domain:
- A Customer can have one or more Projects
- A Project can have one or more TestInstances – don’t worry what test instances are, that’s not relevant in this context
- Each test instance can have one or more TestResults – again, don’t worry about what test results are
- Each test result can have one or more UrlResults
- Each test result can also have on or more UrlErrors
…and so on and so forth, I won’t give you the full list of associations. You can see where we’re heading if we try to reflect all of that in code. Loading a single customer by ID will load a large amount of other domains so that we keep the state of the objects valid. Imagine loading all customers at once. It would mean loading the entire database and keeping it in memory…
You probably get the point. Eric Evans states another difficulty with associations:
“Associations in the OBJECT MODEL allow us to find an object based on its relationship to another. But we must have a means of finding the starting point for such a traversal for an ENTITY or VALUE in the middle of its lifecycle.”
What does that mean? When implementing associations in code it can be difficult to find the “correct” direction. An object somewhere in the middle, such as a TestResult in the above example, is especially problematic. If we load a test result then we also have to load all the objects below and above it in the object graph.
All of the above information leads us towards limiting the scope of associations and traversal directions in code. Let’s say we only want to keep one side of the association, e.g. we’ll reference the list of projects in the Customer objects but not vice versa. Here’s a relevant quote on this subject from Eric Evans:
“The United States has had many presidents, as have many other countries. This is a bi-directional, one-to many relationship. Yet, we seldom would start out with the name ‘George Washington’ and ask the question, ‘Which country was he president of?’ Pragmatically, we can reduce it unidirectional association, traversable from country to president. This actually reflects insight into the domain as well as making a more practical design. It captures the understanding that one direction of the association is much more meaningful and important than the other. It keeps the ‘Person’ class independent of the far less fundamental concept of ‘President’.”
So, how do we decide which direction to keep? There are no well-defined rules here I think. One can argue about the importance of both directions: we must be able to find all projects of a customer. However, finding the owning customer of a project might be just as important. In some cases, like the one quoted above with the US presidents, you’ll be able to make a sound judgement based on some basic reasoning or the business case. In other cases you can discuss what the end users expects to see, i.e. what the UI specifications look like. You might discover that one of the directions is not as crucial as the other one.
In our case being able to view the “components” of a load test is more important than the way around. It’s more important to know which load test agent carries out a specific agent than which load tests a single agent has executed. Also, it’s more important to know the steps of a load test, i.e. the scenario, than which load tests were executed for a given scenario.
Now we know that we’ll go for a unidirectional traversal. Next we need to decide the degree of coupling.
Standalone classes
“Interdependencies make models and designs hard to understand. They also make them hard to test and maintain. And interdependencies pile up easily. Every association is, of course, a dependency, and understanding a class requires understanding what it is attached to. Those attached things will be attached to still more things, and they have to be understood too. The type of every argument of every method is also a dependency. So is every return value.”
Furthermore:
“Low coupling is a fundamental to object design. When you can, go all the way. Eliminate all other concepts from the picture. Then the class will be completely self-contained and can be studied and understood alone. Every such self-contained class significantly eases the burden of understanding a MODULE.”
Those are quite clear guidelines from Eric Evans. Keep the number of tightly coupled dependencies at a minimum.
In the case of Loadtest we’d like to have some reference to the constituent objects such as the Customer and the Agent. However, we don’t want to have a direct object reference to them in order to keep the coupling at a minimum. One way out of this dilemma is to only preserve the ID references to the dependencies. Therefore add the following class to the Domain layer:
public class Loadtest : EntityBase<Guid> { public Guid AgentId { get; private set; } public Guid CustomerId { get; private set; } public Guid? EngineerId { get; private set; } public Guid LoadtestTypeId { get; private set; } public Guid ProjectId { get; private set; } public Guid ScenarioId { get; private set; } public LoadtestParameters Parameters { get; private set; } public Loadtest(Guid guid, LoadtestParameters parameters, Guid agentId, Guid customerId, Guid? engineerId , Guid loadtestTypeId, Guid projectId, Guid scenarioId) : base(guid) { RaiseIfDefaultGuid(guid); AssignParameters(parameters, agentId, customerId, engineerId, loadtestTypeId, projectId, scenarioId); } public void Update(LoadtestParameters parameters, Guid agentId, Guid customerId, Guid? engineerId , Guid loadtestTypeId, Guid projectId, Guid scenarioId) { AssignParameters(parameters, agentId, customerId, engineerId, loadtestTypeId, projectId, scenarioId); } private void AssignParameters(LoadtestParameters parameters, Guid agentId, Guid customerId, Guid? engineerId , Guid loadtestTypeId, Guid projectId, Guid scenarioId) { RaiseIfDefaultGuid(agentId); RaiseIfDefaultGuid(customerId); if (engineerId.HasValue) RaiseIfDefaultGuid(engineerId.Value); RaiseIfDefaultGuid(loadtestTypeId); RaiseIfDefaultGuid(projectId); RaiseIfDefaultGuid(scenarioId); AgentId = agentId; CustomerId = customerId; EngineerId = engineerId; LoadtestTypeId = loadtestTypeId; ProjectId = projectId; ScenarioId = scenarioId; Parameters = parameters; } private void RaiseIfDefaultGuid(Guid guid) { if (guid == default(Guid)) { throw new ArgumentException("Default GUID not acceptable"); } } }
…where we have 2 new objects. LoadtestParameters is a new value object that includes a limited number of the load test parameters:
public class LoadtestParameters : ValueObjectBase<LoadtestParameters> { private LoadtestParameters() { } public DateTime StartDateUtc { get; private set; } public int UserCount { get; private set; } public int DurationSec { get; private set; } public LoadtestParameters(DateTime startDateUtc, int userCount, int durationSec) { if (userCount < 1) throw new ArgumentException("User count cannot be less than 1"); if (durationSec < 30) throw new ArgumentException("Test duration cannot be less than 30 seconds."); if (durationSec > 3600) throw new ArgumentException("Test duration cannot be more than 3600 seconds, i.e. 1 hour."); if (startDateUtc < DateTime.UtcNow) startDateUtc = DateTime.UtcNow; StartDateUtc = startDateUtc; UserCount = userCount; DurationSec = durationSec; } public DateTime GetEndDateUtc() { return StartDateUtc.AddSeconds(DurationSec); } public LoadtestParameters WithStartDateUtc(DateTime newStartDate) { return new LoadtestParameters(newStartDate, UserCount, DurationSec); } public LoadtestParameters WithUserCount(int userCount) { return new LoadtestParameters(StartDateUtc, userCount, DurationSec); } public LoadtestParameters WithDuration(int durationSec) { return new LoadtestParameters(StartDateUtc, UserCount, durationSec); } public override bool Equals(LoadtestParameters other) { return other.UserCount == this.UserCount && other.StartDateUtc == this.StartDateUtc && other.DurationSec == this.DurationSec; } public override bool Equals(object obj) { if (obj == null) return false; if (!(obj is LoadtestParameters)) return false; return this.Equals((LoadtestParameters)obj); } public override int GetHashCode() { return this.DurationSec.GetHashCode() + this.StartDateUtc.GetHashCode() + this.UserCount.GetHashCode(); } }
…and the property LoadtestTypeId refers to a new entity that I forgot to introduce in the previous post:
public class LoadtestType : EntityBase<Guid> { public Description Description { get; private set; } public LoadtestType(Guid guid, string shortDescription, string longDescription) : base(guid) { Description = new Description(shortDescription, longDescription); } }
Here are a couple of things to note:
- LoadtestParameters introduces a couple of new rules: a minimum user count of 1, a minimum load test duration of 30 seconds and a maximum load test duration of 3600 seconds
- If load test start date lies in the past then it’s automatically overruled so that start date will be the current UTC date
- LoadtestType reuses the Description value object we saw in the case of the Project entity
- Loadtest doesn’t accept default GUIDs, i.e. where all characters are 0’s
Just to recap we have the following structure in our project currently:
We have sorted out an important central object in our domain model. However, we haven’t seen the aggregate root yet. The Loadtest entity is a good candidate but there’s in fact another object that lies higher up. We’ve already mentioned the timetable which holds all load tests. We’ll discuss that object and its role in the next post.
View the list of posts on Architecture and Patterns here.
The fields in Loadtest class are specified as Guids in order to assure loose coupling:
public Guid CustomerId { get; private set; }
How would I then refer to multiple customers? Would IList be in terms of DDD?
public IList CustomerId { get; private set; }
Couple of typos fixed… The fields in Loadtest class are specified as Guids in order to assure loose coupling:
public Guid CustomerId { get; private set; }
How would I then refer to multiple customers? Would IList be acceptable in terms of DDD?
public IList CustomerId { get; private set; }