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
Container
domain 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
IsNotPlaced
andIsBeingFilled
. 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!