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 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. –
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? –
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