Nowadays I am learning Domain Driven Design intensively. I am reading the book called Domain Driven Design by Eric Evans, and at the same time I try to apply the knowledge on a real life project I work on recently. My domain is the following: The web-app I am developing is responsible for tracking containers. These containers can contain different kind of building materials, like bricks or concrete, and they are transferred by trucks from one place to another. A customer can have an order, and an order can have multiple containers placed on the customer's address. Where I am busy right now is to build and design a container entity using DDD. I have done it, but there are several parts of the code where I am doubting and having questions. As a technical info: I am using .Net Core 2.0 with EF Core, and for database I use Azure SQL database.
Let's see first the my Container domain entity itself:
public class Container: Entity<Guid>
{
public int Size {get; }
public string Status { get; private set; }
public int NumberOfChanges { get; private set; }
public int? Price { get; set; }
public string Remark { get; set; }
public DateTime LayDownDate { get; set; }
public DateTime? ChangeDate { get; set; }
public DateTime? TakeUpDate { get; set; }
public long LastTouchedById { get; private set; }
public TruckDriver LastTouchedBy { get; set; }
public long OrderId { get; private set; }
public Order Order { get; set; }
private bool IsNotPlaced => Status.Equals(ContainerStatus.NotPlaced, StringComparison.InvariantCulture);
private bool IsBeingFilled => Status.Equals(ContainerStatus.BeingFilled, StringComparison.InvariantCulture);
public Container(int size)
{
if(!ValidContainerSizes.SizeIsValid(size)) {
throw new InvalidOperationException($"Invalid container size of {size}");
}
Size = size;
Status = ContainerStatus.NotPlaced;
}
public void Place(long byTruckDriverId) {
if (!IsNotPlaced)
{
throw new InvalidOperationException($"Container can only be placed when it is not placed yet. But the current container status is {Status}");
}
LastTouchedById = byTruckDriverId;
Status = ContainerStatus.BeingFilled;
}
public void Change(long byTruckDriverId)
{
if (!IsBeingFilled)
{
throw new InvalidOperationException($"Container can only be changed when it is placed and being filled. But the current container status is {Status}");
}
NumberOfChanges++;
LastTouchedById = byTruckDriverId;
}
public void TakeAway(long byTruckDriverId)
{
if (!IsBeingFilled)
{
throw new InvalidOperationException($"Container can only be taken away when it is placed and being filled. But the current container status is {Status}");
}
NumberOfChanges++;
LastTouchedById = byTruckDriverId;
Status = ContainerStatus.TakenAway;
}
}
Where the ContainerStatus is the following:
public static class ContainerStatus
{
public const string NotPlaced = "NotPlaced";
public const string BeingFilled = "BeingFilled";
public const string TakenAway = "TakenAway";
public static IEnumerable<string> GetAll()
{
yield return NotPlaced;
yield return BeingFilled;
yield return TakenAway;
}
}
And last but not least the ValidContainerSizes looks like:
public static class ValidContainerSizes
{
public static bool SizeIsValid(int size) => GetSizes().Contains(size);
public static IEnumerable<int> GetSizes()
{
yield return 3;
yield return 4;
yield return 5;
yield return 6;
}
}
My design decisions: First of all I tried to use as many as private setters I could and place the all the logics (relating to container) inside the class.
As for the properties Price, Remark, LayDownDate, ChangeDate, TakeUpDate are just plain data holders, so I want to change them outside of the class so I left the properties with public setters. I know it is a so called anti-pattern, but I did not really wanted to add just public setters explicitly for them. As for Status, NumberOfChanges and LastTouchedById, they are used in the Container functions and they are privately set.
As for ContainerStatus I use static string fields instead of enums, because it is easier to persist and maintain them in relational databases.
I also have numerous tests written for the code, but I do not include them since it is irrelevant to the question.
My concerns and questions are the following about my Container domain entity.
- I am coming from the Java world where the constructor is placed after the class fields. I see some examples where the constructor is on the first place in C# codes. To be honest I prefer to place the constructor after the fields so if I read a class first, I can see what the the properties of them are. What is the convention for placing the constructor in C#, using DDD?
- I was thinking that my
Containerdomain entity contains a lot of properties. What do you think, it is too rich for a domain entity, or reasonable? - As you can see I use contract validations in the constructor and in all the methods. Is it a good practice to throw an InvalidOperationEception or shall I use some other contract validation framework? If so, which contract validation framework you would recommend to use?
- I have created two more properties
IsNotPlacedandIsBeingFilled. To be honest it is the first time I use these expressions in C#. Is it a good practice, or it is anti-pattern and better to have just old plain methods for them? I personally like them! - I tried to extract some part of this code to a DDD ValueObject, but I could not really come up with a good idea. What is your opinion, there is room for extracting some properties and logic to a ValueObject?
Furthermore I am open for any other remark, improvement point, refactoring idea. What do you think where I could improve my code using DDD? I include additional code if needed for the question. Thanks!