Here is how I would structure your problem. I believe this is also the recommended DDD way of doing it.
public class ProductService : IProductService // Application Service class, used by outside components like UI, WCF, HTTP Services, other Bounded Contexts, etc.
{
private readonly IProductRepository _prodRepository;
private readonly IStoreRepository _storeRepository;
public ProductService(IProductRepository prodRepository, IStoreRepository storeRepository) // Injected dependencies DI
{
if(prodRepository == null) throw new NullArgumentException("Prod Repo is required."); // guard
if(storeRepository == null) throw new NullArgumentException("Store Repo is required."); // guard
_prodRepository = prodRepository;
_storeRepository = storeRepository;
}
public void AddProductToStore(string name, Address address, StoreId storeId) //An exposed API method related to Product that is a part of your Application Service. Address and StoreId are value objects.
{
Store store = _storeRepository.GetBy(storeId);
IProductIdGenerator productIdGenerator = new ProductIdGenerator(_prodRepository);
Product product = Product.MakeNew(name, address, productIdGenerator);
}
... // Rest of API
}
public class Product : Entity
{
public static MakeNew(string name, Address address, IProductIdGenerator productIdGenerator) // Factory to make construction behaviour more explicit
{
return new Product(name, address, productIdGenerator);
}
protected Product(string name, Address address, IProductIdGenerator productIdGenerator)
: base(productIdGenerator.GetNextProductId())
{
Name = name;
Address = address;
}
... // Rest of Product methods, properties and fields
}
public class ProductIdGenerator : IProductIdGenerator
{
private IProductRepository _repository;
public ProductIdGenerator(IProductRepository repository)
{
_repository = repository;
}
public long GetNextProductId()
{
return _repository.GetNextProductId();
}
}
public interface IProductIdGenerator
{
long GetNextProductId();
}
Basically, ProductService is part of your Application Service, that is, the entry and exit point of everything that needs to use your domain or cross its boundary. It is responsible for delegating each use cases to appropriate components that can deal with it, and coordinating between all these components if many is required to fulfill the use case.
Product is your AggregateRoot and an Entity in your Domain. It is responsible for dictating the contract of the UbiquitousLanguage that capture the Domain of your enterprise. So, in itself, it means that your domain has a concept of a Product, which contains Data and Behaviour, whichever data and behaviour you expose publicly must be a concept of the UbiquitousLanguage. It's field should not have external dependencies outside the domain model, so no services. But, it's methods can take Domain Services as parameters to help it perform behaviour logic.
ProductIdGenerator is an example of such a Domain Service. Domain Services encapsulate behaviour logic that crosses outside of an Entity's own boundary. So if you have logic that requires other aggregate roots, or external services like Repository, File System, Cryptography, etc. Basically, any logic that you can not workout from inside your Entity, without needing anything else, you might need a Domain Service. If the logic is to englobing and seems like conceptually might not really belong as a method on your Entity, it's a sign you might need an entirely new Application Service use case just for it, or you have missed a Entity in your design. It is also possible to use Domain Service from the Application Service directly, in a non double dispatch way. This is a bit similar to C# extension methods versus normal static method.
=========== To Answer your Edit questions ===============
I also doubted if the service should be called from the Product Class.
Domain Services can be called from the product class if they are passed as a temporary reference through a method parameter. Application Services should never be called from the Product class.
I have not used a factory pattern (yet) as the construction of the
object is still simple. I dont feel it warrants a factory method yet?
It depends what you expect will take you more time, making a factory now, even though you don't have multiple construction logic, or refactoring later when you do. I think that it's not worth it for entities who do not need to be constructed in more than one way. As wikipedia explains, factory is used to make what each constructor do more explicit and differentiable. In my example, the MakeNew factory explains what this particular construction of the Entity serves as purpose: to create a new Product. You could have more factory like MakeExisting, MakeSample, MakeDeprecated, etc. Each of these factory would create a Product but for different purposes and in slightly different ways. Without a Factory, all these constructors would be named Product() and it would be hard to know which one is for what and does what. The downside is that Factory are harder to work with when you extend your entity, the child entity can't use the parent Factory to create a child, that's why I tend to do all the construction logic inside the constructors anyways, and only use Factory to have a pretty name for them.
I' am confused...Putting the ProductId aside if my Product class
needed some other data from a Service e.g GetSystemDateTime() (i know,
bad example but trying to demonstrate a non db call) where would this
service method be called?
Say you thought the Date implementation was a detail of the infrastructure. You would create an abstraction around it to use in your application. It would start with an interface, maybe something like IDateTimeProvider. This interface would have a method GetSystemDateTime().
Your Application Services would be free to instantiate an IDateTimeProvider and call its methods at any time, than it could pass the result to Aggregates, Entities, Domain Services, or whatever else that would need it.
Your Domain Services would be free to hold a reference to IDateTimeProvider as a class field, but it should not create the instance itself. Either it receives it through dependency injection, or it asks for it through Service Locator.
Finally, your Entites and Aggregate Roots and Value Objects would be free to call GetSystemDateTime() and other methods of IDateTimeProvider, but not directly. It would need to go through double dispatch, where you'd give it a Domain Service as a parameter of one of it's methods, and it would use that Domain Service to query the info it wants, or perform the behaviour it needs. It could also pass itself back to the Domain Service, where the domain service would do the querying and setting.
If you consider your IDateTimeProvider to actually be a Domain Service, as a part of the Ubiquitous Language, than your Entities and Aggregate Roots can just call methods on it directly, it just can not hold a reference to it as a class field, but local variables of method parameters are fine.
Services in DDD are logic dumps where the logic is not natrual to the
domain object, right? So How does it glue together?
I think my entire answer has made this pretty clear already. Basically, you have 3 possibilities for gluing it all (that I can think of as of now at least).
1) An Application Service instantiates a Domain Service, calls a method on it, and passes the resulting return values to something else that needed it (repo, entity, aggregate root, value object, another domain service, factories, etc.).
2) A Domain Service is instantiated by the Application Domain and passed as a parameter to a method of something that will use it. Whatever uses it, does not keep a permanent reference to it, it's a local variable only.
3) A Domain Service is instantiated by the Application Domain and passed as a parameter to a method of something that will use it. Whatever uses it, uses double dispatch to use the Domain Service in a non dependent way. This means it passes to the method of the Domain Service an reference to itself, as in DomainService.DoSomething(this, name, Address).
Hope this helps.
Comments are welcomed if I did anything wrong or that goes against DDD's best practices.