Validating business rules which require a call to a repository - one approach

posted @ Thursday, March 08, 2007 12:00 AM

 

Ayende has a couple of recent posts which relate to entities, services and repositories.  In some of the comments, it was briefly discussed about if/how you should validate dynamic business rules that require a call to a repository.  Ayende mentioned this in one of his comments:

About business logic that needs talking to the database.
I would strive very hard to avoid doing it. Entities validate business logic by traversing the object graph they have, not by calling to the database.
Calling to the database makes it a lot harder to just start a new entity and run business logic.

I would agree it's not ideal and it does certainly make it harder to deal with the situation where you've changed the domain object but it's invalid, so you don't want to persist it.  It just so happens I'm having to tackle this problem right now in one of my projects.  Here's one way I've approached this so far, though I'm sure there are better ways.  So please let me know what you think...

One Approach

First off, one of my current approaches to business rules in general is similar to what Jean-Paul Boodhoo demonstrates in his posts on Validation In The Domain Layer.  I just really like how clean this kind of approach plays out.  Anyways, here goes...

So a simple rule might be something like a product cannot be saved if it's name is a duplicate of another product (this is an actual rule, though not for products, that a handful of my domain objects have in my current project).  So here's what a service might look like:

public class ProductService : IProductService
{
private readonly IProductRepository repository;

public ProductService(IProductRepository repository) // injected
{
this.repository = repository;
}

public IBusinessRuleSet SaveProduct(ProductDTO productDTO)
{
try
{
repository.BeginTransaction();

Product product = repository.FindById(productDTO.Id) ?? new Product();
product.Name = productDTO.Name;

IBusinessRuleSet brokenRules = GetBrokenRulesFor(product);

if (!brokenRules.IsEmpty)
{
repository.RollBackTransaction();
return brokenRules;
}

repository.Save(product);
repository.CommitTransaction();

return brokenRules;
}
catch (Exception)
{
repository.RollBackTransaction();
throw;
}
}

private IBusinessRuleSet GetBrokenRulesFor(Product productToSave)
{
productToSave.AddBusinessRule(InitializeDuplicateRuleUsing(productToSave.Name));

return productToSave.Validate();
}

private IBusinessRule<Product> InitializeDuplicateRuleUsing(string productName)
{
IBusinessRule<Product> duplicateRule =
Product.Rules.DuplicateProduct(delegate(Product productToValidate)
{
return
new DuplicateProductSpecification(
repository.FindByName(productName)).IsSatisfiedBy(
productToValidate);
});
return duplicateRule;
}
}

And here's an example of the Product domain object:

public class Product : DomainObject  // base domain object has Validate and other validation properties and methods
{
private string name;

public string Name
{
get { return name; }
set { name = value; }
}

public Product() : this(null)
{
}

public Product(string name)
{
this.name = name;
}

public static class Rules
{
public static IBusinessRule<Product> DuplicateProduct(
Predicate<Product> rulePredicate)
{
return
new BusinessRule<Product>("DuplicateProduct",
"A product with this name already exists.",
rulePredicate);
}
}
}

I like this because the rule itself including its description is still declared inside the domain object, but the logic for validating it is passed in via a Predicate.  This keeps the domain object from directly referencing the repository, though I have thought about trying that approach as well. 

Challenges with NHibernate

I did run into a couple challenges with NHibernate which I resolved by specifically managing the transaction and setting FlushMode to Commit instead of Auto.  This is needed because, as Jeffrey Palermo points out, queries using ICriteria and IQuery may flush changes to the database when set to Auto which is not good news when the result of that query may determine whether or not you want to persist the object in the first place.

Conclusion

I'm still looking for a more elegant solution, but this is getting me by for now.  One minor change might be to extract out the initialization of the duplicate rule into a class implementing the IBusinessRule<Product> interface to get rid of the private methods in the ProductService class.  I'm sure I'll make other changes as needed and as I learn better ways to structure entities, services and repositories. 

Looking forward to "getting my learn on" in Jean-Paul Boodhoo's upcoming class down here in Richmond, VA.  :)

Comments
Jan Limpens - 9/19/2008 11:11 AM
# re: Validating business rules which require a call to a repository - one approach
I would not do validation outside of the actually validated entity. If the entity is in a fine state it is valid, for itself. In your case what might not be invalid is another entity, that might not exist in you design: the ProductStore (or similar) - the entity that holds all products. It has to keep track, that no product is added to it with a duplicated name and is in an invalid state if this is not the case.

This is a bit similar to the active record pattern, where an entity can save itself by Entity.Save(). Save where? Save what? A word document cannot save itself, only the word app can convince windows to save it.

I guess this leak in the abstraction comes from the fact, that most data storages won't have such a 1 row table, so it is often missing in the domain as well. One has to ask oneself: where _are_ my products? In outer space? Where do they appear from, when called? Do they just manifest out of thin air? A ProductGroup or a Category are no places, they are just tags, so they don't count.

Probably you found a solution for this, a long time ago
gfxrakista - 10/28/2008 9:48 PM
# re: Validating business rules which require a call to a repository - one approach
I would prefer doing the validation outside of the entity and create some kind of business rules validation engine.. that some kind of ProductController uses.. You see in the real world, business/laws are not inside an entity, its a system that each entity should obey and they are subject for change.. You don't change an entity because the rules changes. You submit to it and someone checks if you are align with the rules, not the entity itself..

So its a combination of Jan and your approach.....
Post Comment






Please add 2 and 6 and type the answer here: