So, I’m developing some software, and trying to keep myself using TDD and other best practices.
I’m trying to write tests to define the classes and repository.
Let’s say I have the classes, Customer, Order, OrderLine.
Now, do I create the Order class as something like
abstract class Entity {
int ID { get; set; }
}
class Order : Entity {
Customer Customer { get; set; }
List<OrderLine> OrderLines { get; set; }
}
Which will serialize nice, but, if I don’t care about the OrderLines, or Customer details is not as lightweight as one would like. Or do I just store IDs to items and add a function for getting them?
class Order : Entity {
int CustomerID { get; set; }
List<OrderLine> GetOrderLines() {};
}
class OrderLine : Entity {
int OrderID { get; set; }
}
And how would you structure the repository for something like this?
Do I use an abstract CRUD repository with methods GetByID(int), Save(entity), Delete(entity) that each items repository inherits from, and adds it’s own specific methods too, something like this?
public abstract class RepositoryBase<T, TID> : IRepository<T, TID> where T : AEntity<TID>
{
private static List<T> Entities { get; set; }
public RepositoryBase()
{
Entities = new List<T>();
}
public T GetByID(TID id)
{
return Entities.Where(x => x.Id.Equals(id)).SingleOrDefault();
}
public T Save(T entity)
{
Entities.RemoveAll(x => x.Id.Equals(entity.Id));
Entities.Add(entity);
return entity;
}
public T Delete(T entity)
{
Entities.RemoveAll(x => x.Id.Equals(entity.Id));
return entity;
}
}
What’s the ‘best practice’ here?
Entities
Let’s start with the
Orderentity. An order is an autonomous object, which isn’t dependent on a ‘parent’ object. In domain-driven design this is called an aggregate root; it is the root of the entire order aggregate. The order aggregate consists of the root and several child entities, which are theOrderLineentities in this case.The aggregate root is responsible for managing the entire aggregate, including the lifetime of the child entities. Other components are not allowed to access the child entities; all changes to the aggregate must go through the root. Also, if the root ceases to exist, so do the children, i.e. order lines cannot exist without a parent order.
The
Customeris also an aggregate root. It isn’t part of an order, it’s only related to an order. If an order ceases to exist, the customer doesn’t. And the other way around, if a customer ceases to exist, you’ll want to keep the orders for bookkeeping purposes. BecauseCustomeris only related, you’ll want to have just theCustomerIdin the order.Repositories
The
OrderRepositoryis responsible for loading the entireOrderaggregate, or parts of it, depending on the requirements. It is not responsible for loading the customer. If you need the customer, load it from theCustomerRepository, using theCustomerIdfrom the order.If you ever need to load the order lines afterwards, you should use the tell, don’t ask principle. Tell the order to load its order lines, and which repository to use. The order will then tell the repository the information it needs to know.
Note that the code uses an
IOrderRepositoryto retrieve the order lines, rather than a separate repository for order lines. Domain-driven design states that there should be a repository for each aggregate root. Methods for retrieving child entities belong in the repository of the root and should only be accessed by the root.Abstract/base repositories
I have written abstract repositories with CRUD operations myself, but I found that it didn’t add any value. Abstraction is useful when you want to pass instances of subclasses around in your code. But what kind of code will accept any
BaseRepositoryimplementation as a parameter?Also, the CRUD operations can differ per entity, making a base implementation useless. Do you really want to delete an order, or just set its status to deleted? If you delete a customer, what will happen to the related orders?
My advice is to keep things simple. Stay away from abstraction and generic base classes. Sure, all repositories share some kind of functionality and generics look cool. But do you actually need it?