Comment gérez-vous la validation sur des agrégats complexes dans une conception axée sur le domaine? Consolidez-vous vos règles commerciales/votre logique de validation?
Je comprends la validation des arguments. Et je comprends la validation de propriété qui peut être attachée aux modèles eux-mêmes et vérifie, par exemple, qu’une adresse électronique ou un code postal est valide ou que le prénom a une longueur minimale et maximale.
Mais qu'en est-il de la validation complexe impliquant plusieurs modèles? Où placez-vous généralement ces règles et méthodes dans votre architecture? Et quels modèles utilisez-vous, le cas échéant, pour les implémenter?
J'aime la solution de Jimmy Bogard à ce problème. Il a publié sur son blog un article intitulé "Validation d'entité avec visiteurs et méthodes d'extension" dans lequel il présente une approche très élégante de la validation d'entité, qui suggère la mise en œuvre d'une classe distincte pour stocker le code de validation.
public interface IValidator<T>
{
bool IsValid(T entity);
IEnumerable<string> BrokenRules(T entity);
}
public class OrderPersistenceValidator : IValidator<Order>
{
public bool IsValid(Order entity)
{
return BrokenRules(entity).Count() == 0;
}
public IEnumerable<string> BrokenRules(Order entity)
{
if (entity.Id < 0)
yield return "Id cannot be less than 0.";
if (string.IsNullOrEmpty(entity.Customer))
yield return "Must include a customer.";
yield break;
}
}
Au lieu de faire appel à IsValid(xx)
lors de votre candidature, envisagez de prendre conseil auprès de Greg Young:
Ne laissez jamais vos entités entrer dans Dans un état invalide.
Cela signifie essentiellement que vous passez de la pensée des entités à des conteneurs de données purs et davantage aux objets dotés de comportements.
Prenons l'exemple de l'adresse d'une personne:
person.Address = "123 my street";
person.City = "Houston";
person.State = "TX";
person.Zip = 12345;
Entre ces appels, votre entité est invalide (car vous auriez des propriétés qui ne seraient pas en accord. Considérons ceci:
person.ChangeAddress(.......);
tous les appels relatifs au comportement de changement d'adresse sont maintenant une unité atomique. Votre entité n'est jamais invalide ici.
Si vous prenez cette idée de la modélisation des comportements plutôt que de l'état, vous pouvez alors atteindre un modèle qui n'autorise pas les entités non valides.
Pour une bonne discussion à ce sujet, consultez cette interview infoq: http://www.infoq.com/interviews/greg-young-ddd
J'utilise habituellement une classe de spécification, Elle fournit une méthode (c'est C # mais vous pouvez la traduire dans n'importe quel langage):
bool IsVerifiedBy(TEntity candidate)
Cette méthode effectue une vérification complète du candidat et de ses relations. Vous pouvez utiliser des arguments dans la classe de spécification pour la paramétrer, comme un niveau de vérification ...
Vous pouvez également ajouter une méthode pour savoir pourquoi le candidat n'a pas vérifié la spécification:
IEnumerable<string> BrokenRules(TEntity canditate)
Vous pouvez simplement décider d'implémenter la première méthode comme ceci:
bool IsVerifiedBy(TEntity candidate)
{
return BrokenRules(candidate).IsEmpty();
}
Pour les règles non respectées, j’écris habituellement un itérateur:
IEnumerable<string> BrokenRules(TEntity candidate)
{
if (someComplexCondition)
yield return "Message describing cleary what is wrong...";
if (someOtherCondition)
yield return
string.Format("The amount should not be {0} when the state is {1}",
amount, state);
}
Pour la localisation, vous devez utiliser des ressources et pourquoi ne pas transmettre une culture à la méthode BrokenRules. Je place cette classe dans l’espace de nom du modèle avec des noms qui en suggèrent l’utilisation.
La validation de plusieurs modèles doit passer par votre racine agrégée. Si vous devez valider plusieurs racines agrégées, vous avez probablement un défaut de conception.
Pour valider des agrégats, je renvoie une interface de réponse indiquant si la validation a réussi/échoué, ainsi que tout message expliquant la raison de son échec.
Vous pouvez valider tous les sous-modèles sur la racine agrégée afin qu'ils restent cohérents.
// Command Response class to return from public methods that change your model
public interface ICommandResponse
{
CommandResult Result { get; }
IEnumerable<string> Messages { get; }
}
// The result options
public enum CommandResult
{
Success = 0,
Fail = 1
}
// My default implementation
public class CommandResponse : ICommandResponse
{
public CommandResponse(CommandResult result)
{
Result = result;
}
public CommandResponse(CommandResult result, params string[] messages) : this(result)
{
Messages = messages;
}
public CommandResponse(CommandResult result, IEnumerable<string> messages) : this(result)
{
Messages = messages;
}
public CommandResult Result { get; private set; }
public IEnumerable<string> Messages { get; private set; }
}
// usage
public class SomeAggregateRoot
{
public string SomeProperty { get; private set; }
public ICommandResponse ChangeSomeProperty(string newProperty)
{
if(newProperty == null)
{
return new CommandResponse(CommandResult.Fail, "Some property cannot be changed to null");
}
SomeProperty = newProperty;
return new CommandResponse(CommandResult.Success);
}
}