In "Implementing Domain Driven Design", Vaugh Vernon uses the "specification pattern" to handle entity/aggregate validation. Without quoting the entire chapter, you have different possibilities : (Java is used in my example, I hope you get the overall idea)
Validating Attributes / Properties
If it is a simple validation process field by field, then validate each attribute separately inside the setter method.
class Product {
String name;
public Product(String name) {
setName(name);
}
public void setName(String name) {
if(name == null) {
throw new IllegalArgumentException("name cannot be null");
}
if(name.length() == 0) {
throw new IllegalArgumentException("name cannot be empty");
}
this.name = name;
}
}
Validating Whole Object
If you have to validate the whole object, you can use a kind of specification to help you. To avoid having the entity having too much responsibilities (dealing with the state, and validate it), you can use a Validator.
a. Create a generic Validator class, and implement it for your Product Validator. Use a NotificationHandler to deal with your validation error (exception, event, accumulating errors and then sending them ? up to you) :
public abstract class Validator {
private ValidationNotificationHandler notificationHandler;
public Validator(ValidationNotificationHandler aHandler) {
super();
this.setNotificationHandler(aHandler);
}
public abstract void validate();
protected ValidationNotificationHandler notificationHandler() {
return this.notificationHandler;
}
private void setNotificationHandler(ValidationNotificationHandler aHandler) {
this.notificationHandler = aHandler;
}
}
NotificationHandler
is an interface, that you could implement given your requirements in term of validation error handling. Here is the interface proposed by Vaugh Vernon :
public interface ValidationNotificationHandler {
public void handleError(String aNotificationMessage);
public void handleError(String aNotification, Object anObject);
public void handleInfo(String aNotificationMessage);
public void handleInfo(String aNotification, Object anObject);
public void handleWarning(String aNotificationMessage);
public void handleWarning(String aNotification, Object anObject);
}
b. Implements this class with a specific validator ProductValidator
:
public class ProductValidator extends Validator {
private Product product;
public ProductValidator(Product product, ValidationNotificationHandler aHandler) {
super(aHandler);
this.setProduct(product);
}
private void setProduct(Product product) {
this.product = product;
}
@Override
public void validate() {
this.checkForCompletness();
}
private void checkForCompletness() {
if(product.getName().equals("bad name") && anotherCondition()) {
notificationHandler().handleError("This specific validation failed");
}
...
}
}
And then, you can update your entity, with a validate method, that will call this validator to validate the whole object:
public class Product {
private String name;
public Product(String name) {
setName(name);
}
private void setName(String name) {
if (name == null) {
throw new IllegalArgumentException("Name cannot be null");
}
if (name.length() == 0) {
throw new IllegalArgumentException("Name cannot be empty");
}
this.name = name;
}
// Here is the new method to validate your object
public void validate(ValidationNotificationHandler aHandler) {
(new ProductValidator(this, aHandler)).validate();
}
}
Validating multiple aggregates
And finally, which is your direct concern, if you want to validate multiple aggregates to have something coherent, the recommendation is to create a Domain Service and a specific validator. The domain services can either have injected the repositories to look up for the different aggregates, or I everything is created by the application layers, then inject the different aggregates as method parameter:
public class ProductCategoryValidator extends Validator {
private Product product;
private Category category;
public ProductCategoryValidator(Product product, Category category, ValidationNotificationHandler aHandler) {
super(aHandler);
this.setProduct(product);
this.setCategory(category);
}
private void setCategory(Category category) {
this.category = category;
}
private void setProduct(Product product) {
this.product = product;
}
@Override
public void validate() {
this.checkForCompletness();
}
private void checkForCompletness() {
// Count number of attributes, check for correctness...
}
}
And the domain service that will call the Validator
public class ProductService {
// Use this is you can pass the parameters from the client
public void validateProductWithCategory(Product product, Category category, ValidationNotificationHandler handler) {
(new ProductCategoryValidator(product, category, handler)).validate();
}
// Use This is you need to retrieve data from persistent layer
private ProductRepository productRepository;
private CategoryReposiory categoryReposiory;
public ProductService(ProductRepository productRepository, CategoryReposiory categoryReposiory) {
this.productRepository = productRepository;
this.categoryReposiory = categoryReposiory;
}
public void validate(String productId, ValidationNotificationHandler handler) {
Product product = productRepository.findById(productId);
Category category = categoryReposiory.categoryOfProductId(productId);
(new ProductCategoryValidator(product, category, handler)).validate();
}
}
Like I said, I think you might be interested into the solution 3. As you have guessed it, you can use a Domain Service. But, add a specific validator to ensure the "responsibilities" are not mixed.