Wiem, że myślisz (a może krzyczysz): „nie ma innego pytania, gdzie należy sprawdzić poprawność w architekturze warstwowej?!?” Cóż, tak, ale mam nadzieję, że będzie to trochę inne spojrzenie na ten temat.
Jestem głęboko przekonany, że sprawdzanie poprawności przybiera wiele form, jest oparte na kontekście i różni się na każdym poziomie architektury. To jest podstawa do postu - pomaga określić, jaki rodzaj walidacji powinien zostać wykonany na każdej warstwie. Ponadto często pojawia się pytanie, gdzie należą kontrole autoryzacji.
Przykładowy scenariusz pochodzi z aplikacji dla firmy cateringowej. Okresowo w ciągu dnia kierowca może zwrócić się do biura z nadwyżką gotówki zgromadzonej podczas transportu ciężarówki z miejsca na miejsce. Aplikacja pozwala użytkownikowi zarejestrować „wypłatę gotówki” poprzez zebranie identyfikatora kierowcy i kwoty. Oto szkielet kodu ilustrujący zaangażowane warstwy:
public class CashDropApi // This is in the Service Facade Layer
{
[WebInvoke(Method = "POST")]
public void AddCashDrop(NewCashDropContract contract)
{
// 1
Service.AddCashDrop(contract.Amount, contract.DriverId);
}
}
public class CashDropService // This is the Application Service in the Domain Layer
{
public void AddCashDrop(Decimal amount, Int32 driverId)
{
// 2
CommandBus.Send(new AddCashDropCommand(amount, driverId));
}
}
internal class AddCashDropCommand // This is a command object in Domain Layer
{
public AddCashDropCommand(Decimal amount, Int32 driverId)
{
// 3
Amount = amount;
DriverId = driverId;
}
public Decimal Amount { get; private set; }
public Int32 DriverId { get; private set; }
}
internal class AddCashDropCommandHandler : IHandle<AddCashDropCommand>
{
internal ICashDropFactory Factory { get; set; } // Set by IoC container
internal ICashDropRepository CashDrops { get; set; } // Set by IoC container
internal IEmployeeRepository Employees { get; set; } // Set by IoC container
public void Handle(AddCashDropCommand command)
{
// 4
var driver = Employees.GetById(command.DriverId);
// 5
var authorizedBy = CurrentUser as Employee;
// 6
var cashDrop = Factory.CreateCashDrop(command.Amount, driver, authorizedBy);
// 7
CashDrops.Add(cashDrop);
}
}
public class CashDropFactory
{
public CashDrop CreateCashDrop(Decimal amount, Employee driver, Employee authorizedBy)
{
// 8
return new CashDrop(amount, driver, authorizedBy, DateTime.Now);
}
}
public class CashDrop // The domain object (entity)
{
public CashDrop(Decimal amount, Employee driver, Employee authorizedBy, DateTime at)
{
// 9
...
}
}
public class CashDropRepository // The implementation is in the Data Access Layer
{
public void Add(CashDrop item)
{
// 10
...
}
}
Wskazałem 10 lokalizacji, w których widziałem sprawdzanie poprawności umieszczone w kodzie. Moje pytanie dotyczy tego, jakie kontrole przeprowadzilibyście przy każdej z następujących reguł biznesowych (wraz ze standardowymi kontrolami długości, zakresu, formatu, typu itp.):
- Kwota zrzutu gotówki musi być większa od zera.
- Upuszczenie gotówki musi mieć ważnego Kierowcę.
- Bieżący użytkownik musi być upoważniony do dodawania zrzutów gotówki (bieżący użytkownik nie jest kierowcą).
Proszę podzielić się swoimi przemyśleniami, jak masz lub podejmiesz ten scenariusz i powody swoich wyborów.
źródło
CashDropAmount
obiekt wartości zamiast używaćDecimal
. Sprawdzenie, czy sterownik istnieje, czy nie, zostanie wykonane w module obsługi poleceń i to samo dotyczy reguł autoryzacji. Możesz uzyskać autoryzację za darmo, robiąc coś w taki sposób, wApprover approver = approverService.findById(employeeId)
jaki wyrzuca, jeśli pracownik nie pełni roli osoby zatwierdzającej.Approver
byłby tylko obiektem wartości, a nie bytem. Można też pozbyć się fabryki lub użyć metody fabryki na AR zamiast:cashDrop = driver.dropCash(...)
.Odpowiedzi:
Zgadzam się, że to, co zatwierdzasz, będzie różne w każdej warstwie aplikacji. Zazwyczaj sprawdzam tylko to, co jest wymagane do wykonania kodu w bieżącej metodzie. Staram się traktować podstawowe komponenty jako czarne skrzynki i nie sprawdzam ich poprawności w oparciu o sposób ich implementacji.
Na przykład w klasie CashDropApi sprawdziłbym tylko, czy „kontrakt” nie jest zerowy. Zapobiega to NullReferenceExceptions i jest wszystkim, co jest potrzebne do zapewnienia prawidłowego działania tej metody.
Nie wiem, czy sprawdziłbym cokolwiek w klasie usługi lub polecenia, a moduł obsługi sprawdzałby tylko, czy „polecenie” nie ma wartości zerowej z tych samych powodów, co w klasie CashDropApi. Widziałem (i zrobiłem) walidację w obie strony w odniesieniu do klas fabryki i encji. Jedno lub drugie to miejsce, w którym chcesz zweryfikować wartość „kwoty”, a pozostałe parametry nie mają wartości zerowej (reguły biznesowe).
Repozytorium powinno sprawdzać tylko, czy dane zawarte w obiekcie są zgodne ze schematem zdefiniowanym w bazie danych, a operacja daa zakończy się powodzeniem. Na przykład, jeśli masz kolumnę, która nie może być pusta lub ma maksymalną długość itp.
Jeśli chodzi o kontrolę bezpieczeństwa, myślę, że to naprawdę kwestia zamiaru. Ponieważ reguła ma zapobiegać nieautoryzowanemu dostępowi, chciałbym przeprowadzić tę kontrolę tak wcześnie, jak to możliwe, aby zmniejszyć liczbę niepotrzebnych kroków, które podjąłem, jeśli użytkownik nie jest autoryzowany. Prawdopodobnie umieściłbym to w CashDropApi.
źródło
Twoja pierwsza reguła biznesowa
wygląda jak niezmiennik twojej
CashDrop
istoty i twojejAddCashDropCommand
klasy. Istnieje kilka sposobów egzekwowania niezmiennika takiego jak ten:Twoja druga zasada ma szerszy charakter (w świetle szczegółów w pytaniu): czy ważna oznacza, że jednostka Kierowcy ma flagę wskazującą, że może prowadzić (tj. Nie zawieszono jej prawa jazdy), czy oznacza to, że kierowca był faktycznie działa tego dnia, czy oznacza to po prostu, że driverId, przekazany do CashDropApi, jest ważny w sklepie trwałości.
W każdym z tych przypadków będziesz musiał nawigować w swoim modelu domeny i pobrać
Driver
instancję z własnegoIEmployeeRepository
, tak jak wlocation 4
przykładzie kodu. Tak więc tutaj musisz upewnić się, że wywołanie do repozytorium nie zwraca null, w którym to przypadku sterownik nie był prawidłowy i nie możesz kontynuować przetwarzania.W przypadku pozostałych 2 (moich hipotetycznych) kontroli (czy kierowca ma ważne prawo jazdy, czy kierowca pracuje dzisiaj) przestrzegasz reguł biznesowych.
To, co zwykle robię, to użycie kolekcji klas walidacyjnych, które działają na jednostkach (podobnie jak wzorzec specyfikacji z książki Erica Evansa - Domain Driven Design). Użyłem FluentValidation do zbudowania tych reguł i walidatorów. Mogę następnie skomponować (a zatem ponownie wykorzystać) bardziej złożone / pełniejsze reguły z prostszych reguł. I mogę zdecydować, które warstwy w mojej architekturze je uruchomić. Ale mam je wszystkie zakodowane w jednym miejscu, a nie rozrzucone po całym systemie.
Twoja trzecia zasada dotyczy zagadnienia przekrojowego: autoryzacji. Ponieważ używasz już kontenera IoC (zakładając, że twój kontener IoC obsługuje przechwytywanie metod), możesz zrobić AOP . Napisz apkę, która wykonuje autoryzację i możesz użyć kontenera IoC do wstrzyknięcia tego zachowania autoryzacji tam, gdzie musi. Wielką wygraną jest to, że raz napisałeś logikę, ale możesz jej ponownie użyć w całym systemie.
Aby korzystać z przechwytywania za pośrednictwem dynamicznego serwera proxy (Castle Windsor, Spring.NET, Ninject 3.0 itp.), Klasa docelowa musi zaimplementować interfejs lub odziedziczyć po klasie podstawowej. Przechwycisz przed wywołaniem metody docelowej, sprawdzisz autoryzację użytkownika i zapobiegniesz przejściu wywołania do rzeczywistej metody (rzucisz wykrzyknik, zarejestrujesz, zwrócisz wartość wskazującą błąd lub coś innego), jeśli użytkownik nie ma odpowiednie role do wykonania operacji.
W twoim przypadku możesz przechwycić połączenie do obu
Problemy tutaj
CashDropService
mogą być nie do przechwycenia, ponieważ nie ma interfejsu / klasy bazowej. LubAddCashDropCommandHandler
nie jest tworzony przez IoC, dlatego Twój IoC nie może utworzyć dynamicznego proxy do przechwycenia połączenia. Spring.NET ma przydatną funkcję, w której można kierować metodę na klasę w zespole za pomocą wyrażenia regularnego, więc może to działać.Mam nadzieję, że daje to kilka pomysłów.
źródło
Do zasad:
Zrobiłbym sprawdzanie poprawności w lokalizacji (1) dla reguły biznesowej (1) i upewnienie się, że identyfikator nie jest zerowy lub ujemny (zakładając, że zero jest prawidłowe) jako wstępne sprawdzenie reguły (2). Powodem jest moja zasada: „Nie przekraczaj granicy warstwy z niewłaściwymi danymi, które możesz sprawdzić za pomocą dostępnych informacji”. Wyjątkiem jest sytuacja, w której usługa dokonuje walidacji w ramach obowiązku wobec innych dzwoniących. W takim przypadku walidacja będzie wystarczająca tylko tam.
W przypadku reguł (2) i (3) należy to zrobić tylko w warstwie dostępu do bazy danych (lub w samej warstwie bazy danych), ponieważ wymaga to dostępu do bazy danych. Nie trzeba celowo podróżować między warstwami.
W szczególności można uniknąć zasady (3), jeśli pozwolimy, aby GUI uniemożliwiał nieautoryzowanym użytkownikom naciśnięcie przycisku włączającego ten scenariusz. Chociaż kodowanie jest trudniejsze, jest lepsze.
Dobre pytanie!
źródło