2013-05-28 10 views
25

Mam obecnie warstwę usługi opartą na artykule Validating with a service layer z witryny ASP.NET.Oddzielanie warstwy usługi od warstwy sprawdzania poprawności

Zgodnie z odpowiedzią this jest to złe podejście, ponieważ logika usługi jest mieszana z logiką walidacji, która narusza zasadę jednej odpowiedzialności.

Bardzo podoba mi się dostarczona alternatywa, ale podczas ponownego obliczania mojego kodu natknąłem się na problem, którego nie jestem w stanie rozwiązać.

Rozważmy następujący interfejs usługi:

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

z następującym konkretnej implementacji opartej na połączonych odpowiedź:

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(); 
    } 
} 

The PurchaseOrder obiekt, który jest przekazywany do walidatora wymaga również dwa inne podmioty, Part i Supplier (załóżmy dla tego przykładu, że PO ma tylko jedną część).

Obiekty mogą mieć wartość zerową, jeśli szczegóły dostarczone przez użytkownika nie odpowiadają obiektom w bazie danych, które wymagają od weryfikatora zgłoszenia wyjątku.

Problem polega na tym, że na tym etapie weryfikator utracił informacje kontekstowe (numer części i nazwę dostawcy), więc nie jest w stanie zgłosić użytkownikowi dokładnego błędu. Najlepszy błąd, jaki mogę podać, to: "Zamówienie musi mieć przypisaną część", które nie ma sensu dla użytkownika, ponieważ podał numer części (po prostu nie istnieje w bazie danych).

Korzystanie z klasy usług z artykułu ASP.NET robię coś takiego:

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(); 
} 

To pozwala mi zapewnić znacznie lepszą informację weryfikacyjną dla użytkownika, ale oznacza, że ​​logika walidacja jest zawarta bezpośrednio w klasa usług, naruszająca zasadę jednej odpowiedzialności (kod jest również duplikowany między klasami usług).

Czy istnieje sposób na uzyskanie najlepszych z obu światów? Czy mogę oddzielić warstwę usługi od warstwy sprawdzania poprawności, zachowując jednocześnie ten sam poziom informacji o błędach?

Odpowiedz

42

Krótka odpowiedź:

Jesteś walidacji źle.

Bardzo długa odpowiedź:

Próbujesz sprawdzić poprawność PurchaseOrder ale to szczegółów wdrażania. Zamiast tego należy sprawdzić samą operację, w tym przypadku parametry partNumber i supplierName.

Samo walidowanie tych dwóch parametrów byłoby niezręczne, ale jest to spowodowane przez Twój projekt - brakuje Ci abstrakcji.

Krótko mówiąc, problem występuje w interfejsie IPurchaseOrderService.Nie powinno przyjmować dwóch argumentów, ale jednego pojedynczego argumentu (Parameter Object). Nazwijmy ten obiekt parametru: CreatePurchaseOrder. W tym przypadku interfejs będzie wyglądać następująco:

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

interface IPurchaseOrderService 
{ 
    void CreatePurchaseOrder(CreatePurchaseOrder command); 
} 

Przedmiot Parametr CreatePurchaseOrder owija oryginalne argumenty. Ten obiekt parametru to komunikat opisujący zamiar utworzenia zamówienia zakupu. Innymi słowy: to polecenie.

Za pomocą tego polecenia można utworzyć implementację IValidator<CreatePurchaseOrder>, która może wykonywać wszystkie poprawne sprawdzenia, w tym sprawdzanie istnienia właściwego dostawcy części i zgłaszanie przyjaznych komunikatów o błędach.

Ale dlaczego IPurchaseOrderService jest odpowiedzialny za walidację? Walidacja jest problemem przekrojowym nr i powinieneś starać się nie mieszać go z logiką biznesową. Zamiast tego można zdefiniować dekorator na to:

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); 
    } 
} 

ten sposób możemy dodać walidację po prostu owijania prawdziwe PurchaseOrderService:

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

Problem oczywiście z tego podejścia jest to, że byłoby to bardzo niewygodne zdefiniuj taką klasę dekoratora dla każdej usługi w systemie. Byłoby to poważne naruszenie zasady DRY.

Ale problem jest spowodowany wadą. Zdefiniowanie interfejsu dla konkretnej usługi (takiej jak IPurchaseOrderService) jest zazwyczaj problematyczne. Ponieważ zdefiniowaliśmy CreatePurchaseOrder, mamy już taką definicję. Możemy teraz zdefiniować pojedynczą abstrakcję dla wszystkich operacji gospodarczych w systemie:

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

Dzięki tej abstrakcji możemy teraz byłaby PurchaseOrderService na następujące kwestie:

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

     unitOfWork.Savechanges(); 
    } 
} 

z tym wzorem, możemy teraz zdefiniować jedną pojedynczy generyczny dekorator do obsługi walidacji dla każdej operacji biznesowej w systemie:

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); 
    } 
} 

Zobacz, jak ten dekorator jest prawie taki sam jak wcześniej zdefiniowane ValidationPurchaseOrderServiceDecorator, ale teraz jako klasa generyczna. Ten dekorator można owinąć wokół naszej nowej klasy usług:

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

Ale ponieważ dekorator jest nazwą rodzajową, możemy owinąć go wokół każdego obsługi poleceń w naszym systemie. Łał! Jak to jest być SUCHEM?

Dzięki temu projektowi można w prosty sposób dodawać zagadnienia przekrojowe. Na przykład, twoja usługa wydaje się obecnie odpowiedzialna za wywołanie SaveChanges na jednostce pracy. Można to również uznać za zagadnienie przekrojowe i można je łatwo wyekstrahować do dekoratora. W ten sposób Twoje klasy usług stają się znacznie prostsze, a mniej kodu pozostaje do przetestowania.

CreatePurchaseOrder walidator może wyglądać następująco:

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."); 
     } 
    } 
} 

A twój obsługi poleceń tak:

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); 
    } 
} 

Należy pamiętać, że polecenia wiadomości staną częścią domeny. Istnieje odwzorowanie typu "jeden do jednego" między przypadkami użycia i poleceniami, a zamiast jednostek sprawdzających te szczegóły będą szczegółami implementacji. Polecenia stają się umową i zostaną zatwierdzone.

Pamiętaj, że prawdopodobnie twoje życie będzie o wiele łatwiejsze, jeśli twoje polecenia zawierają jak najwięcej identyfikatorów. Więc twój system będzie mógł skorzystać z definiowania polecenia w następujący sposób:

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

Gdy zrobisz to nie będziesz musiał sprawdzić, czy część o podanej nazwie nie istnieje. Warstwa prezentacji (lub system zewnętrzny) przekazała ci identyfikator, więc nie musisz już potwierdzać istnienia tej części. Program obsługi poleceń powinien oczywiście zawieść, gdy nie ma części przez ten identyfikator, ale w takim przypadku wystąpił błąd programowania lub konflikt współbieżności. W obu przypadkach nie trzeba przekazywać klientowi ekspresywnych, przyjaznych dla użytkownika błędów weryfikacji.

To jednak powoduje przeniesienie problemu z uzyskaniem właściwych identyfikatorów do warstwy prezentacji. W warstwie prezentacji użytkownik będzie musiał wybrać część z listy, aby uzyskać identyfikator tej części. Ale wciąż doświadczyłem tego, aby system był łatwiejszy i skalowalny.

rozwiązuje też większość problemów, które są określone w sekcji komentarzy artykułu do którego się odnosimy, takich jak:

  • Od poleceń może być łatwo odcinkach i model wiążą problem z jednostki serializacji odchodzi.
  • Atrybuty DataAnnotation można z łatwością zastosować do poleceń, co umożliwia sprawdzanie poprawności strony klienta (Javascript).
  • Dekorator można zastosować do wszystkich procedur obsługi komend, które otaczają całą operację w transakcji bazy danych.
  • Usuwa odwołanie cykliczne między kontrolerem a warstwą usługi (za pośrednictwem elementu ModelState kontrolera), co eliminuje konieczność, aby kontroler wprowadzał nową klasę usług.

Jeśli chcesz dowiedzieć się więcej o tym typie projektu, koniecznie sprawdź: this article.

+1

+1 dziękuję, to jest bardzo doceniane. Będę musiał odejść i ocenić informacje, ponieważ jest dużo do strawienia. Tak przy okazji, aktualnie szukam przejścia od Ninject do Simple Injector. Czytałem dobre rzeczy o występie, ale rzeczą, która mi to sprzedała, było to, że dokumentacja prostego wtryskiwacza jest znacznie lepsza. –

+0

Czy mógłbyś rozwinąć różnice między 'PurchaseOrderCommandHandler' i' PurchActOrderCommandValidator' przekazanymi do dekoratora, ponieważ wydają się robić to samo? Czy intencją walidatora jest potraktowanie instancji obiektu jako parametru, a nie obiektu komendy? –

+0

Funkcja 'PurchaseOrderCommandValidator' sprawdza warunki wstępne dla' PurchaseOrderCommandHandler' do wykonania. W razie potrzeby prześle zapytanie do bazy danych, aby dowiedzieć się, czy program obsługi może działać poprawnie, sprawdzając, czy istnieje część i dostawca. – Steven