Here's a persistence idiom I don't like:
Only save entities if and when they are valid. Invalid entities don't get saved.
In case you're wondering what I'm talking about, here is an example of it in code.
public class Order
{
public void Save()
{
if( this.OrderItems.Count == 0 )
throw new BusinessRuleViolation("Must have order lines!")
if( this.TotalPrice > 1000000 && ! this.IsApproved )
throw new BusinessRuleViolation("Orders for over £1,000,000 have to be approved");
orderDAO.Save(this);
}
//insert more code here...
}
Here, we only save if the business thinks it's a valid thing to save.
One reason for this approach is to stop crap data making it into the database. Garbage in means garbage out, so don't put garbage in. I've used this approach many times in the past, but I'm starting to think it's a bad idea. Perhaps we should allow invalid entities to be saved?
Saving something has nothing to do with it being valid or not. When we put the two things together, aren't we're mixing persistence concerns with business rule concerns? Why should I let business rules block me from saving something? Isn't that simply ridiculous!? They're unrelated!
Back to the garbage analogy, can't we just put garbage in and ignore it? At least until it we've made it into something useful.
This approach seems to be gaining acceptance. Google Docs and Microsoft Office always save work in progress, even if we decide not to keep the document. Keeping the document is now a different concern than saving it.
Going back to the order example, separating persistence from business-rule validity might look like this:
public class Order
{
public void Save()
{
orderDAO.Save(this);
}
public void PlaceOrder()
{
if( this.OrderItems.Count == 0 )
throw new BusinessRuleViolation("You cant save an order with no order lines")
if( this.TotalPrice > 1000000 && ! this.IsApproved )
throw new BusinessRuleViolation("Orders for over £1,000,000 have to be approved");
this.State = OrderState.Placed;
}
//insert more code here...
}
So, we can save an order regardless of it's state, and we can execute the PlaceOrder() business logic regardless without having to save the object. Concerns have been separated. Notes - An improvement on this would be if the Save() method lived on a Repository of course.
####Persistence, Business Logic and Validation
This fits in with some other patterns/idioms I like:
- Business objects are totally allowed to get into a "bad" state. In fact, they are often born in an invalid state.
- Business objects can be persisted (saved) no-matter what state they're in.
- Business objects only raise exceptions when you try and do something illegal (like call PlaceOrder() on an order that isn't ready).
- Business objects are packed with behavior and business rules.
- Business objects can let you know what's up with them - they can be Validated()
- Business objects might have many states, each with it's own Validate() method and logic for transitioning between states.
What are the side-effects of this?
- You can save objects anytime and anywhere.
- Invalid objects are stored in the same place as valid ones.
- You don't have to worry about storing objects in different places (such as the session) just because they're not "ready".
- You have a record of what people started, not just what they finished (find all orders over 1 month old that were under construction but never placed).
- Objects are always work in process. They are always in some lifecycle state.
- Other business processes have to be careful to only work with objects that are in a valid state (don't try to ship orders that are "under construction")
- You might need to do some housekeeping to make sure that old, incomplete and invalid items are deleted. There will be many of them hanging around.
- On large systems, you might need to do aggressive housekeeping.
- Database constraints are virtually a no no, the database won't be enforcing business rules, at least not with standard mechanisms.
- You might want to use the state pattern, which could result in a slight object-graph explosion
My heart isn't totally set on all this, I'm really just toying with ideas here. I do, however, feel there's something good about all this.
Feel free to shoot me down, but in the meantime I'm definately gonna play with this in a sandbox environment :)