I have a question regarding business logic validation in my domain. Im not talking about validation in the sense of simple data validation such as checking for null values or specific data ranges. These days it seems like most people are handling these types of validations with a validation framework and checking an IsValid operation or something similar. I refer to these as data validations. It may be fine in some domains for an object to exist and be persisted when that object is in an invalid data state. I am faced with an issue in which I cannot allow certain objects to ever be in an invalid state.
In my domain I have PromoCode and Account. PromoCodes can be redeemed by accounts, but there are rules regarding redemption. If any of these rules are violated, redemption cannot succeed and the application must prevent the redemption. These rule violations are not fatal and are to be expected, however I must notify the user as to why the redemption did not succeed. So my question is, what is the preferred way of communicating business rule violations to the UI when an action cannot be performed because it would put the object into an invalid state that cannot be persisted?
I think there are essentialy 4 ways:
1) Throw an exception describing the first violation
2) Use Notification pattern to communicate information about all violations.
3) Throw an exception that encapsulates the Notification object as described in Sergio's blog post, A Notification Strategy for Business Errors
4) Provide a Check operation which returns a a boolean or a Notification object such as Notification CanRedeem(Account); or bool CanRedeem(Account); If the check operation returns any violations, Redeem would throw an exception.
The thought of throwing exceptions doesn't sit real well with me but it is the easiest way to get the information to any interested parties because they just have to try...catch it. The Notification pattern has value but it seems more difficult to get the information to the interested parties. Granted, the Redeem operation on PromoCode is a void operation and I could just return a Notification object from that call. However, in my research for this problem Im trying to comeup with a solution that would also work with operations that had a return value. #4 is an instersting option but im a little turned off by it because the check would have to be called multiple times, once from the client and once from the redeem operation. Here is a sample
public class PromoCode{
string code;
DateTime expirationDate;
bool isRedeemed;
bool isOnlyForNewAccounts;
public Notification CanRedeem(Account account){
Notification violations = new Notification();
if(this.isRedeemed){
violations.Add(PromoCodeViolation.AlreadyRedeemed);
}
if(this.expirationDate > DateTime.Now){
violations.Add(PromoCodeViolation.Expired);
}
if(this.isOnlyForNewAccounts && account.IsNewAccount == false){
violations.Add(PromoCodeViolation.NewAccountsOnly);
}
return violations;
}
public void Redeem(Account account){
Notification violations = this.CanRedeem(account);
if(violations.HasErrors){
throw new BusinessRuleException(violations);
}
this.DoRedeem(account);
}
protected void DoRedeem(Account account){
...
}
}
public class PromoCodeTest{
public void RunCanRedeem(){
Account testAccount = Mock.GetMock<Account>();
PromoCode code = CreateValidPromoCode();
if(code.CanRedeem(testAccount).HasErrors == false){
code.Redeem(testAccount);
Assert.IsTrue(code.IsRedeemed);
}
}
}
Any thoughts or advice on this? Any ways to improve it? Or is the a better strategy that I havent seen?
Thanks,
Jesse