There has been a lot of discussion recently about how to handle aggregates that contain other aggregates when building models using DDD. The discussion started a couple weeks ago when Udi posted his proposed solution. Udi suggested that we should add a .Save() method to our domain object that does nothing other then fire an event that will allow any interested "parties" to catch the event and do the appropriate processing. I don't like the idea of adding methods to our domain objects that only fire events. Having a .Save() method on an object implies that calling that method will save the domain object to the data store. Additionally if we're following DDD we try to avoid adding data access like behavior to any of our domain objects. We leave the data access "logic" to our repositories. Let's quickly review the scenario that Udi presented (which came from a comment on one of my earlier DDD posts). We have a Customer object which has an associated address.
public class Customer {
// ... other props...
public Address Address {
get { return _address; }
set { _address = value; }
}
}
When we save the customer we also want to save the associated address. Now if we're following DDD we know that Repositories will play a large role in the saving of our domain objects. Each of our Aggregates should have a Repository that is responsible for handling all data related tasks for all the objects in the aggregate. If address is part of the Customer aggregate we have nothing to worry about since the CustomerRepository would then be responsible for saving the address itself. For arguments sake let's continue with the assumption that the Address class is the root of it's own aggregate and has it's very own Repository. How should the CustomerRepository handle the address when the customer is saved?
Since we're following DDD we should embrace the fact that we're going to be using repositories to save our domain objects.
public class CustomerRepository : DomainRepository {
public bool Save(Customer customer) {
SaveCustomerToDataStore(customer);
AddressRepository addressRepository = new AddressRepository();
addressRepository.Save(customer.Address);
return true;
}
}
As Steve Maine pointed out in one of his follow up posts, including the AddressRepository directly in the CustomerRepository creates a dependency between our repositories that we don't want. What if all the sudden the address needs to be saved by a different repository? How do we introduce a mock address repository into the equation during testing? Rather then hard coding the repository, we should implement a RepositoryFactory class. The RepositoryFactory will have the responsibility of knowing what repository should be used for each type of domain object. The knowledge will either be provided during initialization, via a configuration file, or perhaps will be covered by a framework such as PicoContainer. By introducing a factory into our design we decouple the AddressRepository from the CustomerRepository.
public class CustomerRepository : DomainRepository {
public bool Save(Customer customer) {
SaveCustomerToDataStore(customer);
DomainRepository addressRepository = RepositoryFactory.GetRepository(typeof(Address));
addressRepository.Save(customer.Address);
return true;
}
}
Since we may not always want to use the default RepositoryFactory we should use dependeny injection to allow users of the CustomerRepository to change the factory that is used.
public class CustomerRepository : DomainRepository {
public CustomerRepository(IRepositoryFactory factory) {
this.repositoryFactory = factory;
}
public IRepositoryFactory RepositoryFactory {
get { return factory; }
}
public bool Save(Customer customer) {
SaveCustomerToDataStore(customer);
DomainRepository addressRepository = this.RepositoryFactory.GetRepository(typeof(Address));
addressRepository.Save(customer.Address);
return true;
}
}
With our constructor in place we can very easily inject the proper repository factory into our CustomerRepository. This will allow us to swap out our repository during the testing of our components and will allow us to keep our repositories decoupled, both good things. Next on the plate is the loading of our customer and its associated address, which will come in another post....