Gdzie powinniśmy umieścić weryfikację modelu domeny

38

Nadal szukam sprawdzonych metod sprawdzania poprawności modelu domeny. Czy dobrze jest umieścić walidację w konstruktorze modelu domeny? mój przykład sprawdzania poprawności modelu domeny w następujący sposób:

public class Order
 {
    private readonly List<OrderLine> _lineItems;

    public virtual Customer Customer { get; private set; }
    public virtual DateTime OrderDate { get; private set; }
    public virtual decimal OrderTotal { get; private set; }

    public Order (Customer customer)
    {
        if (customer == null)
            throw new  ArgumentException("Customer name must be defined");

        Customer = customer;
        OrderDate = DateTime.Now;
        _lineItems = new List<LineItem>();
    }

    public void AddOderLine //....
    public IEnumerable<OrderLine> AddOderLine { get {return _lineItems;} }
}


public class OrderLine
{
    public virtual Order Order { get; set; }
    public virtual Product Product { get; set; }
    public virtual int Quantity { get; set; }
    public virtual decimal UnitPrice { get; set; }

    public OrderLine(Order order, int quantity, Product product)
    {
        if (order == null)
            throw new  ArgumentException("Order name must be defined");
        if (quantity <= 0)
            throw new  ArgumentException("Quantity must be greater than zero");
        if (product == null)
            throw new  ArgumentException("Product name must be defined");

        Order = order;
        Quantity = quantity;
        Product = product;
    }
}

Dziękuję za wszystkie sugestie.

adisembiring
źródło

Odpowiedzi:

47

Jest ciekawy artykuł Martina Fowlera na ten temat, który podkreśla aspekt, który większość osób (w tym ja) często pomija:

Ale jedną rzeczą, która - jak sądzę - nieustannie potrząsa ludźmi jest to, że myślą, że ważność obiektu jest niezależna od kontekstu, na przykład metoda isValid.

Myślę, że o wiele bardziej przydatne jest myślenie o walidacji jako o czymś związanym z kontekstem - zazwyczaj działaniem, które chcesz zrobić. Czy to zamówienie jest ważne do wypełnienia, czy klient może zameldować się w hotelu. Więc zamiast metod takich jak isValid, mają metody takie jak isValidForCheckIn.

Wynika z tego, że konstruktor nie powinien sprawdzać poprawności, z wyjątkiem być może bardzo podstawowego sprawdzania poprawności poczytności współdzielonego przez wszystkie konteksty.

Ponownie z artykułu:

W About About Alan Cooper opowiadał się za tym, aby nie dopuścić, aby nasze pomysły dotyczące prawidłowych stanów uniemożliwiały użytkownikowi wprowadzanie (i zapisywanie) niepełnych informacji. Przypomniało mi to kilka dni temu, kiedy czytałem szkic książki, nad którą pracuje Jimmy Nilsson. Stwierdził zasadę, że zawsze powinieneś być w stanie zapisać obiekt, nawet jeśli zawiera on błędy. Chociaż nie jestem przekonany, że powinna to być absolutna zasada, myślę, że ludzie mają tendencję do zapobiegania oszczędzaniu bardziej niż powinni. Myślenie o kontekście walidacji może temu zapobiec.

Michael Borgwardt
źródło
Dzięki Bogu, ktoś to powiedział. Formularze zawierające 90% danych, ale niczego nie zapisujące, są niesprawiedliwe dla użytkowników, którzy często stanowią pozostałe 10% po prostu, aby nie utracić danych, więc cała weryfikacja, którą wykonano, zmusza system do utraty śledzenia, z czego 10% został wymyślony. Podobne problemy mogą wystąpić na zapleczu - powiedzmy import danych. Przekonałem się, że zwykle lepiej jest próbować działać poprawnie z nieprawidłowymi danymi niż zapobiegać temu.
psr
2
@psr Czy potrzebujesz nawet logiki zaplecza, jeśli Twoje dane nie zostaną utrwalone? Możesz pozostawić wszystkie manipulacje po stronie klienta, jeśli Twoje dane nie mają znaczenia w modelu biznesowym. Byłoby również marnotrawstwem zasobów do wysyłania wiadomości tam iz powrotem (klient - serwer), jeśli dane są bez znaczenia. Wracamy więc do idei „nigdy nie pozwalać, aby obiekty domeny wchodziły w niepoprawny stan!” .
Geo C.
2
Zastanawiam się, dlaczego tak wiele głosów na tak niejednoznaczną odpowiedź. Podczas korzystania z DDD czasami istnieją pewne reguły, które po prostu sprawdzają, czy niektóre dane są INT lub są w zakresie. Na przykład, gdy pozwalasz użytkownikowi aplikacji wybrać pewne ograniczenia dotyczące jego produktów (ile razy ktoś może wyświetlić podgląd mojego produktu i w jakich dniach miesiąca). Tutaj oba ograniczenia powinny być int, a jedno z nich powinno mieścić się w przedziale 0–31. Wydaje się, że sprawdzanie poprawności formatu danych, które w środowisku innym niż DDD zmieściłoby się w usłudze lub kontrolerze. Ale w DDD jestem za utrzymaniem walidacji w domenie (90%).
Geo C.
2
Wymuszanie na górnych warstwach zbyt dużej wiedzy na temat domeny, aby utrzymać ją w prawidłowym stanie, pachnie źle. Domena powinna być tą, która gwarantuje jej prawidłowość. Zbyt duże przesuwanie się po ramionach górnych warstw może sprawić, że Twoja domena będzie anemiczna, a Ty możesz znieść pewne ważne ograniczenia, które mogą zaszkodzić Twojej firmie. Teraz zdaję sobie sprawę, że właściwym uogólnieniem byłoby utrzymanie walidacji jak najbliżej uporczywości lub jak najbliżej kodu manipulacji danymi (gdy jest manipulowany w celu osiągnięcia stanu końcowego).
Geo C.
PS Nie mieszam autoryzacji (wolno coś zrobić), uwierzytelnienia (czy wiadomość pochodzi z właściwej lokalizacji, czy została wysłana przez właściwego klienta, oba identyfikowane za pomocą klucza API / tokena / nazwy użytkownika lub czegokolwiek innego) z weryfikacją formatu lub reguły biznesowe. Kiedy mówię 90%, mam na myśli te reguły biznesowe, że większość z nich obejmuje również sprawdzanie formatu. Walidacja formatu oczywiście może odbywać się na wyższych warstwach, ale większość z nich będzie w domenie (nawet format adresu e-mail, który zostanie zweryfikowany w obiekcie wartości EmailAddress).
Geo C.
5

Pomimo tego, że to pytanie jest trochę nieaktualne, chciałbym dodać coś wartościowego:

Chciałbym się zgodzić z @MichaelBorgwardt i rozszerzyć, podnosząc testowalność. W „Skutecznej pracy ze starszym kodem” Michael Feathers dużo mówi o przeszkodach w testowaniu, a jedną z tych przeszkód jest „trudny do zbudowania” obiekt. Konstrukcja niepoprawnego obiektu powinna być możliwa i, jak sugeruje Fowler, kontrole ważności zależne od kontekstu powinny być w stanie zidentyfikować te warunki. Jeśli nie możesz dowiedzieć się, jak skonstruować obiekt w uprzęży testowej, będziesz mieć problemy z testowaniem swojej klasy.

Jeśli chodzi o ważność, lubię myśleć o systemach sterowania. Systemy sterowania działają poprzez ciągłą analizę stanu wyjścia i stosowanie działań naprawczych, gdy wyjście odbiega od wartości zadanej, co nazywa się kontrolą w pętli zamkniętej. Kontrola w zamkniętej pętli z natury oczekuje odchyleń i działa na ich skorygowanie, i tak działa rzeczywisty świat, dlatego wszystkie rzeczywiste systemy sterowania zazwyczaj używają kontrolerów w zamkniętej pętli.

Myślę, że zastosowanie sprawdzania poprawności zależnej od kontekstu i łatwych do zbudowania obiektów sprawi, że Twój system będzie łatwiejszy w pracy na drodze.

Paweł
źródło
1
Wiele razy obiekty wydają się trudne do zbudowania. Na przykład w tym przypadku można pominąć konstruktor publiczny, tworząc klasę opakowania, która dziedziczy z testowanej klasy i umożliwia utworzenie instancji obiektu podstawowego w niepoprawnym stanie. W tym przypadku zastosowanie poprawnych modyfikatorów dostępu do klas i konstruktorów wchodzi w grę i może być naprawdę szkodliwe dla testowania, jeśli jest używane niewłaściwie. Dodatkowo unikanie „zapieczętowanych” klas i metod, z wyjątkiem przypadków, w których jest to właściwe, znacznie ułatwi testowanie kodu.
P. Roe
4

Jestem pewien, że już wiesz ...

W programowaniu obiektowym konstruktor (czasami skracany do ctor) w klasie jest specjalnym rodzajem podprogramu wywoływanym przy tworzeniu obiektu. Przygotowuje nowy obiekt do użycia, często akceptując parametry używane przez konstruktora do ustawiania dowolnych zmiennych składowych wymaganych przy pierwszym tworzeniu obiektu. Nazywa się to konstruktorem, ponieważ konstruuje wartości członków danych klasy.

Sprawdzanie poprawności danych przekazywanych jako parametry c'tora jest zdecydowanie poprawne w konstruktorze - w przeciwnym razie prawdopodobnie pozwalasz na budowę nieprawidłowego obiektu.

Jednak (i ​​to tylko moja opinia, nie mogę znaleźć na tym etapie żadnych dobrych dokumentów) - jeśli sprawdzanie poprawności danych wymaga złożonych operacji (takich jak operacje asynchroniczne - być może sprawdzanie poprawności na serwerze, jeśli tworzysz aplikację komputerową), to lepiej umieścić jakąś funkcję inicjalizacji lub jawnej walidacji, a członkowie ustawiają wartości domyślne (takie jak null) w c'tor.


Ponadto, tak jak uwaga dodatkowa, jak to uwzględniono w przykładowym kodzie ...

Jeśli nie dokonasz dalszej weryfikacji (lub innej funkcjonalności) AddOrderLine, najprawdopodobniej ujawnię ten obiekt List<LineItem>jako właściwość, niż Orderdziałam jako fasada .

Demian Brecht
źródło
Po co wystawiać pojemnik? Co ma znaczenie dla górnych warstw, czym jest pojemnik? Jest całkowicie uzasadnione, aby mieć AddLineItemmetodę. W rzeczywistości w przypadku DDD jest to preferowane. W przypadku List<LineItem>zmiany na niestandardowy obiekt kolekcji właściwość narażona i wszystko, co zależało od List<LineItem>właściwości, mogą ulec zmianie, błędowi i wyjątkowi.
IAbstract
4

Walidacja powinna zostać przeprowadzona jak najszybciej.

Walidacja w dowolnym kontekście, bez względu na model domeny lub inny sposób pisania oprogramowania, powinna służyć temu, CO chcesz zweryfikować i na jakim poziomie jesteś w tej chwili.

Wydaje mi się, że w oparciu o twoje pytanie podzielimy walidację.

  1. Sprawdzanie poprawności właściwości sprawdza, czy wartość tej właściwości jest poprawna, np. Gdy spodziewany jest zakres od 1 do 10.

  2. Sprawdzanie poprawności obiektu gwarantuje, że wszystkie właściwości obiektu są poprawne w połączeniu ze sobą. np. BeginDate jest przed EndDate. Załóżmy, że odczytujesz wartość ze składnicy danych, a zarówno BeginDate, jak i EndDate są domyślnie inicjowane na DateTime.Min. Podczas ustawiania BeginDate nie ma powodu, aby wymuszać regułę „musi być przed EndDate”, ponieważ nie dotyczy to YET. Tę regułę należy sprawdzić PO ustawieniu wszystkich właściwości. Można to wywołać na poziomie zagregowanego katalogu głównego

  3. Walidacja powinna być również przeprowadzona na jednostce agregującej (lub agregującej root). Obiekt zamówienia może zawierać prawidłowe dane, podobnie jak linie zamówień. Ale wtedy reguła biznesowa mówi, że żadne zamówienie nie może przekroczyć 1000 USD. Jak egzekwowałbyś tę regułę w niektórych przypadkach, jest to dozwolone. nie możesz po prostu dodać właściwości „nie sprawdzaj kwoty”, ponieważ prowadziłoby to do nadużyć (prędzej czy później, być może nawet ciebie, by usunąć tę „nieprzyjemną prośbę”).

  4. następnie następuje walidacja na warstwie prezentacji. Czy naprawdę zamierzasz wysłać obiekt przez sieć, wiedząc, że zawiedzie? A może oszczędzisz użytkownikowi tego burdona i poinformujesz go, gdy tylko wprowadzi nieprawidłową wartość. np. przez większość czasu środowisko DEV będzie wolniejsze niż produkcja. Czy chciałbyś poczekać 30 sekund, zanim zostaniesz poinformowany o „PONOWNIE zapomniałeś tego pola podczas KOLEJNEGO uruchomienia testowego”, zwłaszcza gdy występuje błąd produkcyjny do naprawienia, gdy szef oddycha ci po szyi?

  5. Walidacja na poziomie trwałości powinna być jak najbardziej zbliżona do walidacji wartości właściwości. Pomoże to uniknąć wyjątków związanych z odczytywaniem błędów „null” lub „niepoprawna wartość” przy korzystaniu z jakichkolwiek maperów lub zwykłych starych czytników danych. Korzystanie z procedur przechowywanych rozwiązuje ten problem, ale wymaga napisania tej samej logiki waloryzacji PONOWNIE i wykonania jej PONOWNIE. Procedury składowane są domeną administracyjną DB, więc nie próbuj wykonywać JEGO zadania (lub gorzej: „wybieranie nitty, za które mu nie płaci”).

żeby to powiedzieć znanymi słowami „to zależy”, ale przynajmniej teraz wiesz, DLACZEGO to zależy.

Chciałbym umieścić to wszystko w jednym miejscu, ale niestety nie da się tego zrobić. Spowodowałoby to uzależnienie od „obiektu Boga” zawierającego WSZYSTKIE sprawdzanie poprawności WSZYSTKICH warstw. Nie chcesz iść tą ciemną ścieżką.

Z tego powodu zgłaszam wyjątki sprawdzania poprawności tylko na poziomie właściwości. Wszystkie pozostałe poziomy używam ValidationResult z metodą IsValid, aby zebrać wszystkie „złamane reguły” i przekazać je użytkownikowi w jednym AggregateException.

Podczas propagowania stosu wywołań, zbieram je ponownie w AggregateExceptions, aż dojdę do warstwy prezentacji. Warstwa usługi może zgłosić ten wyjątek bezpośrednio do klienta w przypadku WCF jako wyjątek błędu.

To pozwala mi wziąć wyjątek i podzielić go, aby wyświetlić poszczególne błędy przy każdym elemencie sterującym wejściowym, lub spłaszczyć go i wyświetlić na pojedynczej liście. Wybór nalezy do ciebie.

dlatego wspomniałem również o sprawdzaniu poprawności prezentacji, aby je jak najbardziej zwierać.

Jeśli zastanawiasz się, dlaczego mam również sprawdzanie poprawności na poziomie agregacji (lub na poziomie usługi, jeśli chcesz), to dlatego, że nie mam kryształowej kuli, która mówi mi, kto będzie korzystać z moich usług w przyszłości. Będziesz miał dość problemów ze znalezieniem własnych błędów, aby inni nie popełnili Twoich błędów :) wprowadzając nieprawidłowe dane. Np. Administrujesz aplikacją A, ale aplikacja B podaje pewne dane za pomocą Twojej usługi. Zgadnij, kogo pytają najpierw, kiedy pojawia się błąd? Administrator aplikacji B z przyjemnością poinformuje użytkownika „na moim końcu nie ma błędu, po prostu wprowadzam dane”.

Wesley Kenis
źródło