I think your best chance in this is to use the Chain Of Responsiblity pattern, also know as pipeline.
Your class hierarchy is good, however i'd add an extra property to the base class
public bool IsValid {get; set;}
Then you would have your pipeline defined like this
List<BaseValidator> validationPipeline;
in this collection you have to add all the validators you require for a specific scenario, with the following rule, if an object cannot validate the input (because it's not its reponsibilty) then it treats the input as valid and passes the input to the next object in the pipeline, if at the end of the iterarion you have a valid result, then the input is ok, otherwise you can break the validation cycle at the first invalid result
the validation sequence code should be something like this
bool valid = true;
foreach (var validator in validationPipeline)
{
validator.Validate();
valid = validator.IsValid;
if(!valid)
break;
}
//If valid == true at this point, the input is valid
In this way you can split your validation logic in several classes without compromising the desing or coupling it so much, the important things to keep in mind are:
- The order of the validators is important (the most likely to fail should go first)
- If a validator doesn't have to handle the input it should treat it as valid and leave it for another validator down the chain
A quick & dirty example would be a credit card number validator, with the following validation sequence
- Validate credit card number (with the standard algortihm)
- If the credit card is VISA, check something with VISA's system
- If the credit card is Mastercard, check something with Mastercard's systems
And four scenarios can occur
1- The credit card number is invalid, the first validator runs and stops the iteration because if the number isn't right you shouldn't waste time doing anything else.
2- The credit card is valid, and its a visa card, so the second validator runs against some services and it is successful control is passed to the mastercard validator that sould return that its valid because its not its resposiblity to handle visa cards.
3- The credit card is valid, and its a mastercard, so the second validator returns its valid because it only handles visa cards, and the third validator runs and determines the outcome of the validation process
4- Suppose its a valid credit card number, but it's neither visa nor mastercard, lets say its an American Express card, you could add a fourth validator that would return always false because you only accept visa or mastercard.
Hope it's clear and that it's helpful to you or anyone out there