2013-05-28 13 views
25

Ich habe derzeit eine Serviceebene basierend auf dem Artikel Validating with a service layer von der ASP.NET-Site.Trennen der Serviceebene von der Validierungsschicht

Nach this Antwort, dies ist ein schlechter Ansatz, weil die Service-Logik mit der Validierungslogik gemischt ist, die das Prinzip der einheitlichen Verantwortung verletzt.

Ich mag wirklich die Alternative, die geliefert wird, aber während der Umrechnung meines Codes bin ich auf ein Problem gestoßen, das ich nicht lösen kann.

Betrachten Sie die folgende Service-Schnittstelle:

interface IPurchaseOrderService 
{ 
    void CreatePurchaseOrder(string partNumber, string supplierName); 
} 

mit folgendem Beton auf der verlinkten Antwort basierte Implementierung:

public class PurchaseOrderService : IPurchaseOrderService 
{ 
    public void CreatePurchaseOrder(string partNumber, string supplierName) 
    { 
     var po = new PurchaseOrder 
     { 
      Part = PartsRepository.FirstOrDefault(p => p.Number == partNumber), 
      Supplier = SupplierRepository.FirstOrDefault(p => p.Name == supplierName), 
      // Other properties omitted for brevity... 
     }; 

     validationProvider.Validate(po); 
     purchaseOrderRepository.Add(po); 
     unitOfWork.Savechanges(); 
    } 
} 

Das PurchaseOrder Objekt, das auch an den Validator übergeben wird von zwei anderen Einrichtungen erfordert, Part und Supplier (nehmen wir für dieses Beispiel an, dass eine Bestellung nur einen einzigen Teil hat).

Die Objekte Part und Supplier können null sein, wenn die vom Benutzer angegebenen Details nicht mit Entitäten in der Datenbank übereinstimmen, für die der Validator eine Ausnahme auslösen muss.

Das Problem, das ich habe, ist, dass in diesem Stadium der Validator die kontextuellen Informationen (die Teilenummer und den Lieferantennamen) verloren hat, so dass er dem Benutzer keinen genauen Fehler melden kann. Der beste Fehler, den ich liefern kann, ist in den Zeilen "Eine Bestellung muss ein zugeordnetes Teil haben", die für den Benutzer keinen Sinn ergeben würde, weil sie eine Teilenummer lieferten (es existiert nur nicht in der Datenbank).

die Serviceklasse aus dem ASP.NET Artikel verwendet ich so etwas wie dies tue:

public void CreatePurchaseOrder(string partNumber, string supplierName) 
{ 
    var part = PartsRepository.FirstOrDefault(p => p.Number == partNumber); 
    if (part == null) 
    { 
     validationDictionary.AddError("", 
      string.Format("Part number {0} does not exist.", partNumber); 
    } 

    var supplier = SupplierRepository.FirstOrDefault(p => p.Name == supplierName); 
    if (supplier == null) 
    { 
     validationDictionary.AddError("", 
      string.Format("Supplier named {0} does not exist.", supplierName); 
    } 

    var po = new PurchaseOrder 
    { 
     Part = part, 
     Supplier = supplier, 
    }; 

    purchaseOrderRepository.Add(po); 
    unitOfWork.Savechanges(); 
} 

Dies ermöglicht es mir viel besser Validierungsinformationen für den Benutzer zur Verfügung zu stellen, bedeutet aber, dass die Validierungslogik direkt enthalten ist, in die Serviceklasse verletzt das Prinzip der einheitlichen Verantwortlichkeit (Code wird auch zwischen Serviceklassen dupliziert).

Gibt es eine Möglichkeit, das Beste aus beiden Welten zu erhalten? Kann ich die Serviceschicht von der Validierungsschicht trennen und gleichzeitig die gleiche Fehlerinformation bereitstellen?

Antwort

42

Kurze Antwort:

Sie sind die falsche Sache zu validieren.

Sehr lange Antwort:

Sie versuchen, eine PurchaseOrder zu validieren, aber das ist eine Implementierung Detail. Stattdessen sollten Sie die Operation selbst validieren, in diesem Fall die Parameter partNumber und supplierName.

Die Überprüfung dieser beiden Parameter wäre zwar umständlich, aber das liegt an Ihrem Entwurf - Sie haben eine Abstraktion verloren.

Lange Rede kurzer Sinn, das Problem ist in Ihrer IPurchaseOrderService Schnittstelle.Es sollte nicht zwei String-Argumente, sondern ein einziges Argument (a Parameter Object) nehmen. Nennen wir dieses Parameterobjekt: CreatePurchaseOrder. In diesem Fall würde die Schnittstelle wie folgt aussehen:

public class CreatePurchaseOrder 
{ 
    public string PartNumber; 
    public string SupplierName; 
} 

interface IPurchaseOrderService 
{ 
    void CreatePurchaseOrder(CreatePurchaseOrder command); 
} 

Der Parameter Objekt CreatePurchaseOrder die ursprünglichen Argumente wickelt. Dieses Parameterobjekt ist eine Nachricht, die die Absicht der Erstellung einer Bestellung beschreibt. Mit anderen Worten: ist ein Befehl.

Mit diesem Befehl können Sie eine IValidator<CreatePurchaseOrder> Implementierung erstellen, die alle geeigneten Validierungen durchführen kann, einschließlich der Überprüfung des Vorhandenseins des richtigen Teilezulieferers und der Meldung benutzerfreundlicher Fehlermeldungen.

Aber warum ist die IPurchaseOrderService verantwortlich für die Validierung? Validierung ist eine bereichsübergreifende Angelegenheit und Sie sollten versuchen, es mit Geschäftslogik zu mischen. Stattdessen könnten Sie einen Dekorateur für diese definieren:

public class ValidationPurchaseOrderServiceDecorator : IPurchaseOrderService 
{ 
    private readonly IPurchaseOrderService decoratee; 
    private readonly IValidator<CreatePurchaseOrder> validator; 

    ValidationPurchaseOrderServiceDecorator(IPurchaseOrderService decoratee, 
     IValidator<CreatePurchaseOrder> validator) 
    { 
     this.decoratee = decoratee; 
     this.validator = validator; 
    } 

    public void CreatePurchaseOrder(CreatePurchaseOrder command) 
    { 
     this.validator.Validate(command); 
     this.decoratee.CreatePurchaseOrder(command); 
    } 
} 

diese Weise können wir die Validierung durch einfaches Umwickeln eines echten PurchaseOrderService hinzufügen:

var service = 
    new ValidationPurchaseOrderServiceDecorator(
     new PurchaseOrderService(), 
     new CreatePurchaseOrderValidator()); 

Problem natürlich mit diesem Ansatz ist, dass es wirklich schwierig sein würde, Definieren Sie eine Decorator-Klasse für jeden Service im System. Das wäre eine schwere Verletzung des DRY-Prinzips.

Aber das Problem wird durch einen Fehler verursacht. Das Definieren einer Schnittstelle pro spezifischem Dienst (z. B. IPurchaseOrderService) ist typischerweise problematisch. Seit wir die CreatePurchaseOrder definiert haben, haben wir bereits eine solche Definition. Wir können nun eine einzige Abstraktion für alle Geschäftsvorgänge im System definieren:

public interface ICommandHandler<TCommand> 
{ 
    void Handle(TCommand command); 
} 

Mit dieser Abstraktion wir jetzt PurchaseOrderService an folgenden Refactoring können:

public class CreatePurchaseOrderHandler : ICommandHandler<CreatePurchaseOrder> 
{ 
    public void Handle(CreatePurchaseOrder command) 
    { 
     var po = new PurchaseOrder 
     { 
      Part = ..., 
      Supplier = ..., 
     }; 

     unitOfWork.Savechanges(); 
    } 
} 

Mit diesem Entwurf können wir nun eine definieren Einzel generic Dekorateur Validierungen für jeden Geschäftsbetrieb im System zu handhaben:

public class ValidationCommandHandlerDecorator<T> : ICommandHandler<T> 
{ 
    private readonly ICommandHandler<T> decoratee; 
    private readonly IValidator<T> validator; 

    ValidationCommandHandlerDecorator(
     ICommandHandler<T> decoratee, IValidator<T> validator) 
    { 
     this.decoratee = decoratee; 
     this.validator = validator; 
    } 

    void Handle(T command) 
    { 
     var errors = this.validator.Validate(command).ToArray(); 

     if (errors.Any()) 
     { 
      throw new ValidationException(errors); 
     } 

     this.decoratee.Handle(command); 
    } 
} 

Beachten Sie, wie dieser Dekorateur ist fast das gleiche wie die zuvor definierte ValidationPurchaseOrderServiceDecorator, aber jetzt als eine generische Klasse. Dieser Dekorateur kann um unsere neue Serviceklasse eingewickelt werden:

var service = 
    new ValidationCommandHandlerDecorator<PurchaseOrderCommand>(
     new CreatePurchaseOrderHandler(), 
     new CreatePurchaseOrderValidator()); 

Aber da dieser Dekorateur generisch ist, können wir es um jeden Befehlshandler in unserem System wickeln. Beeindruckend! Wie ist das für DRY?

Dieses Design macht es auch sehr einfach, später Querschnitte hinzuzufügen. Zum Beispiel scheint Ihr Dienst derzeit dafür verantwortlich zu sein, SaveChanges auf der Arbeitseinheit aufzurufen. Dies kann auch als Querschnittsaufgabe betrachtet werden und kann leicht einem Dekorateur entnommen werden. Auf diese Weise werden Ihre Serviceklassen viel einfacher, da weniger Code zum Testen vorhanden ist.

Der CreatePurchaseOrder Validator könnte wie folgt aussehen:

public sealed class CreatePurchaseOrderValidator : IValidator<CreatePurchaseOrder> 
{ 
    private readonly IRepository<Part> partsRepository; 
    private readonly IRepository<Supplier> supplierRepository; 

    public CreatePurchaseOrderValidator(IRepository<Part> partsRepository, 
     IRepository<Supplier> supplierRepository) 
    { 
     this.partsRepository = partsRepository; 
     this.supplierRepository = supplierRepository; 
    } 

    protected override IEnumerable<ValidationResult> Validate(
     CreatePurchaseOrder command) 
    { 
     var part = this.partsRepository.Get(p => p.Number == command.PartNumber); 

     if (part == null) 
     { 
      yield return new ValidationResult("Part Number", 
       $"Part number {partNumber} does not exist."); 
     } 

     var supplier = this.supplierRepository.Get(p => p.Name == command.SupplierName); 

     if (supplier == null) 
     { 
      yield return new ValidationResult("Supplier Name", 
       $"Supplier named {supplierName} does not exist."); 
     } 
    } 
} 

Und Ihre Befehlshandler wie folgt aus:

public class CreatePurchaseOrderHandler : ICommandHandler<CreatePurchaseOrder> 
{ 
    private readonly IUnitOfWork uow; 

    public CreatePurchaseOrderHandler(IUnitOfWork uow) 
    { 
     this.uow = uow; 
    } 

    public void Handle(CreatePurchaseOrder command) 
    { 
     var order = new PurchaseOrder 
     { 
      Part = this.uow.Parts.Get(p => p.Number == partNumber), 
      Supplier = this.uow.Suppliers.Get(p => p.Name == supplierName), 
      // Other properties omitted for brevity... 
     }; 

     this.uow.PurchaseOrders.Add(order); 
    } 
} 

Beachten Sie, dass Nachrichten befehle Teil Ihrer Domain werden. Es gibt eine Eins-zu-Eins-Zuordnung zwischen Anwendungsfällen und Befehlen, und anstelle von Entitäten werden diese Entitäten ein Implementierungsdetail sein. Die Befehle werden zum Vertrag und werden validiert.

Beachten Sie, dass es wahrscheinlich Ihr Leben viel einfacher macht, wenn Ihre Befehle so viele IDs wie möglich enthalten. So würde Ihr System könnte von der Definition eines Befehls profitieren wie folgt:

public class CreatePurchaseOrder 
{ 
    public int PartId; 
    public int SupplierId; 
} 

Wenn Sie das tun Sie nicht zu überprüfen haben, ob ein Teil von dem angegebenen Namen existiert. Die Präsentationsebene (oder ein externes System) hat Ihnen eine ID übergeben, so dass Sie die Existenz dieses Teils nicht mehr validieren müssen. Der Befehlshandler sollte natürlich fehlschlagen, wenn von dieser ID kein Teil vorhanden ist. In diesem Fall liegt jedoch ein Programmierfehler oder ein Parallelitätskonflikt vor. In beiden Fällen müssen keine ausdrucksvollen benutzerfreundlichen Validierungsfehler an den Client übermittelt werden.

Dies verschiebt jedoch das Problem, die richtigen IDs für die Präsentationsebene zu erhalten. In der Präsentationsschicht muss der Benutzer einen Teil aus einer Liste auswählen, um die ID dieses Teils zu erhalten. Trotzdem habe ich das erlebt, um das System viel einfacher und skalierbarer zu machen.

Es löst auch die meisten der Probleme, die in den Kommentaren des Artikels angegeben werden Sie sich beziehen, wie zum Beispiel:

  • Da Befehle können leicht serialisiert und Modell binden sein, das Problem mit Entity-Serialisierung geht weg.
  • DataAnnotation-Attribute können leicht auf Befehle angewendet werden und dies ermöglicht Client-seitige (Javascript) Validierung.
  • Ein Decorator kann auf alle Befehlshandler angewendet werden, die den gesamten Vorgang in eine Datenbanktransaktion einbinden.
  • Es entfernt den Zirkelverweis zwischen dem Controller und der Serviceschicht (über den ModelState des Controllers), sodass der Controller die Serviceklasse nicht neu erstellen muss.

Wenn Sie mehr über diese Art von Design erfahren möchten, sollten Sie unbedingt this article überprüfen.

+1

+1 danke, das wird sehr geschätzt. Ich muss weggehen und die Informationen auswerten, da es viel zu verdauen gibt. Übrigens suche ich gerade den Wechsel von Ninject zu Simple Injector. Ich habe gute Dinge über die Leistung gelesen, aber das, was es mir verkauft hat, war, dass die Dokumentation für den einfachen Injector viel besser ist. –

+0

Könnten Sie die Unterschiede zwischen dem 'PurchaseOrderCommandHandler' und dem' PurchaseOrderCommandValidator', die an den Dekorateur weitergegeben werden, näher erläutern, da sie das gleiche zu tun scheinen? Soll der Validator eine Instanz der Entität als Parameter und nicht als Befehlsobjekt verwenden? –

+0

Der 'PurchaseOrderCommandValidator' überprüft die Vorbedingungen für die Ausführung des' PurchaseOrderCommandHandler'. Bei Bedarf wird die Datenbank abgefragt, ob der Handler korrekt ausgeführt werden kann, indem überprüft wird, ob das Teil und der Lieferant existieren. – Steven