Modele z bogatą domeną - jak dokładnie mieści się zachowanie?

84

W debacie na temat modeli domen Rich vs. Anemic Internet jest pełen porad filozoficznych, ale brakuje autorytatywnych przykładów. Celem tego pytania jest znalezienie ostatecznych wytycznych i konkretnych przykładów prawidłowych modeli projektowania opartych na domenie. (Idealnie w C #.)

Na przykład w rzeczywistości ta implementacja DDD wydaje się nieprawidłowa:

Poniższe modele domen WorkItem to tylko torby właściwości, używane przez Entity Framework dla bazy danych zawierającej kod. Według Fowlera jest to niedokrwistość .

Warstwa WorkItemService jest najwyraźniej częstym błędnym postrzeganiem usług domenowych; zawiera całą logikę zachowania / biznesu dla WorkItem. Według Jemelyanova i innych jest to postępowanie proceduralne . (str. 6)

Więc jeśli poniższe informacje są nieprawidłowe, jak mogę to naprawić?
Zachowanie, tj. AddStatusUpdate lub Checkout , powinno należeć do klasy WorkItem, prawda?
Jakie zależności powinien mieć model WorkItem?

wprowadź opis zdjęcia tutaj

public class WorkItemService : IWorkItemService {
    private IUnitOfWorkFactory _unitOfWorkFactory;

    //using Unity for dependency injection
    public WorkItemService(IUnitOfWorkFactory unitOfWorkFactory) {
        _unitOfWorkFactory = unitOfWorkFactory;
    }

    public void AddStatusUpdate(int workItemId, int statusId) {

        using (var unitOfWork = _unitOfWorkFactory.GetUnitOfWork<IWorkItemUnitOfWork>()) {
            var workItemRepo = unitOfWork.WorkItemRepository;
            var workItemStatusRepo = unitOfWork.WorkItemStatusRepository;

            var workItem = workItemRepo.Read(wi => wi.Id == workItemId).FirstOrDefault();
            if (workItem == null)
                throw new ArgumentException(string.Format(@"The provided WorkItem Id '{0}' is not recognized", workItemId), "workItemId");

            var status = workItemStatusRepo.Read(s => s.Id == statusId).FirstOrDefault();
            if (status == null)
                throw new ArgumentException(string.Format(@"The provided Status Id '{0}' is not recognized", statusId), "statusId");

            workItem.StatusHistory.Add(status);

            workItemRepo.Update(workItem);
            unitOfWork.Save();
        }
    }
}

(Ten przykład został uproszczony, aby był bardziej czytelny. Kod jest zdecydowanie nieporadny, ponieważ jest to myląca próba, ale zachowanie domeny brzmiało: aktualizacja statusu poprzez dodanie nowego statusu do historii archiwum. Ostatecznie zgadzam się z innymi odpowiedziami, to może po prostu obsłużyć CRUD).

Aktualizacja

@AlexeyZimarev udzielił najlepszej odpowiedzi, doskonałego filmu na ten temat w C # autorstwa Jimmy'ego Bogarda, ale najwyraźniej został przeniesiony do komentarza poniżej, ponieważ nie podał wystarczającej ilości informacji poza linkiem. Mam wstępny szkic notatek podsumowujących wideo w mojej odpowiedzi poniżej. Prosimy o komentarz w odpowiedzi z wszelkimi poprawkami. Film trwa godzinę, ale warto go obejrzeć.

Aktualizacja - 2 lata później

Myślę, że to oznaka rodzącej się dojrzałości DDD, że nawet po studiowaniu przez 2 lata nadal nie mogę obiecać, że znam „właściwy sposób” robienia tego. Wszechobecny język, zagregowane korzenie i jego podejście do projektowania zorientowanego na zachowanie stanowią cenny wkład DDD w przemysł. Trwałość ignorancji i pozyskiwania zdarzeń powoduje zamieszanie i myślę, że taka filozofia powstrzymuje ją przed szerszym przyjęciem. Ale gdybym musiał zrobić ten kod od nowa, z tym, czego się nauczyłem, myślę, że wyglądałby mniej więcej tak:

wprowadź opis zdjęcia tutaj

Nadal z zadowoleniem przyjmuję wszelkie odpowiedzi na ten (bardzo aktywny) post, który zawiera kod najlepszych praktyk dla prawidłowego modelu domeny.

RJB
źródło
6
Wszystkie teorie filozoficzne spadają na ziemię, kiedy im mówisz "I don't want to duplicate all my entities into DTOs simply because I don't need it and it violates DRY, and I also don't want my client application to take a dependency on EntityFramework.dll". „Encje” w żargonie Entity Framework nie są tym samym co „Encje” jak w „Modelu domeny”
Federico Berasategui
Nie mam nic przeciwko powielaniu moich jednostek domeny w DTO przy użyciu zautomatyzowanego narzędzia, takiego jak Automapper, jeśli to wystarczy. Po prostu nie jestem pewien, jak to ma wyglądać pod koniec dnia.
RJB
16
Polecam obejrzeć sesję Jimmy'ego Bogarda NDC 2012 „Crafting Wicked Domain Models” na Vimeo . Wyjaśnia, czym powinna być bogata domena i jak je wdrażać w prawdziwym życiu, zachowując się w swoich bytach. Przykłady są bardzo praktyczne i wszystkie w języku C #.
Alexey Zimarev
Dziękuję, jestem w połowie filmu i jak dotąd jest to idealne. Wiedziałem, że jeśli to było złe, musiała gdzieś tam znaleźć „właściwą” odpowiedź…
RJB
2
Żądam też miłości do Javy: /
uylmz

Odpowiedzi:

59

Najbardziej pomocna odpowiedź została udzielona przez Aleksieja Zimareva i uzyskała co najmniej 7 głosów poparcia, zanim moderator umieścił ją w komentarzu pod moim pierwotnym pytaniem ...

Jego odpowiedź:

Polecam obejrzeć sesję Jimmy'ego Bogarda NDC 2012 „Crafting Wicked Domain Models” na Vimeo. Wyjaśnia, czym powinna być bogata domena i jak je wdrażać w prawdziwym życiu, zachowując się w swoich bytach. Przykłady są bardzo praktyczne i wszystkie w języku C #.

http://vimeo.com/43598193

Zrobiłem notatki, aby podsumować wideo z korzyścią dla mojego zespołu i podać nieco bardziej szczegółowe szczegóły w tym poście. (Film jest godzinny, ale naprawdę warty każdej minuty, jeśli masz czas. Jimmy Bogard zasługuje na wiele zasług za jego wyjaśnienie).

  • „W przypadku większości aplikacji ... nie wiemy, że będą one skomplikowane, gdy zaczniemy. Po prostu takie stają się”.
    • Złożoność rośnie naturalnie wraz z dodawaniem kodu i wymagań. Aplikacje mogą zaczynać się bardzo prosto, jak CRUD, ale zachowanie / reguły mogą zostać wprowadzone.
    • „Fajną rzeczą jest to, że nie musimy zaczynać od złożonego. Możemy zacząć od anemicznego modelu domeny, który jest tylko torbami własności, a przy użyciu standardowych technik refaktoryzacji możemy przejść w kierunku prawdziwego modelu domeny”.
  • Modele domen = obiekty biznesowe. Zachowanie domeny = reguły biznesowe.
  • Zachowanie jest często ukryte w aplikacji - może być w PageLoad, Button1_Click lub często w klasach pomocniczych, takich jak „FooManager” lub „FooService”.
  • Reguły biznesowe niezależne od obiektów domeny „wymagają od nas zapamiętania” tych reguł.
    • W moim osobistym przykładzie powyżej jedną regułą biznesową jest WorkItem.StatusHistory.Add (). Nie zmieniamy tylko statusu, archiwizujemy go do kontroli.
  • Zachowania domenowe „eliminują błędy w aplikacji znacznie łatwiej niż pisanie wielu testów”. Testy wymagają znajomości pisania tych testów. Zachowania domenowe oferują odpowiednie ścieżki do przetestowania .
  • Usługi domenowe to „klasy pomocnicze do koordynowania działań między różnymi jednostkami modelu domeny”.
    • Usługi domenowe = zachowanie domeny. Podmioty zachowują się, usługi domenowe są tylko pośrednikami między nimi.
  • Obiekty domeny nie powinny mieć potrzebnej infrastruktury (tj. IOfferCalculatorService). Usługa infrastruktury powinna zostać przekazana do modelu domeny, który z niej korzysta.
  • Modele domen powinny oferować informacje o tym, co mogą zrobić, i powinny być w stanie robić tylko te rzeczy.
  • Właściwości modeli domen należy chronić za pomocą prywatnych ustawień, aby tylko model mógł określać własne właściwości poprzez własne zachowania . W przeciwnym razie jest „rozwiązła”.
  • Obiekty modelu anemicznego domeny, które są tylko workami właściwości dla ORM, są tylko „cienką okleiną - silnie typowaną wersją bazy danych”.
    • „Jakkolwiek łatwo jest wstawić wiersz bazy danych do obiektu, oto co mamy.”
    • „Najbardziej trwałe modele obiektów są właśnie takie. Tym, co odróżnia anemiczny model domeny od aplikacji, która tak naprawdę nie zachowuje się, jest to, że obiekt ma reguły biznesowe, ale nie można ich znaleźć w modelu domeny.
  • „W przypadku wielu aplikacji nie ma prawdziwej potrzeby budowania jakiejkolwiek warstwy logiki rzeczywistych aplikacji biznesowych, jest to po prostu coś, co może komunikować się z bazą danych i być może jakiś prosty sposób na reprezentację zawartych w niej danych”.
    • Innymi słowy, jeśli wszystko, co robisz, to CRUD bez specjalnych obiektów biznesowych lub reguł zachowania, nie potrzebujesz DDD.

Możesz skomentować wszelkie inne kwestie, które Twoim zdaniem powinny zostać uwzględnione, lub jeśli uważasz, że którakolwiek z tych notatek jest nie na miejscu. Próbowałem zacytować bezpośrednio lub sparafrazować jak najwięcej.

RJB
źródło
Świetne wideo, zwłaszcza aby zobaczyć, jak działa refaktoryzacja w narzędziu. Dużo dotyczy właściwego enkapsulacji obiektów domeny (aby upewnić się, że są spójne). Wykonuje świetną robotę, opowiadając o regułach biznesowych dotyczących ofert, członków itp. Kilka razy wspomina słowo niezmiennik (czyli modelowanie domen oparte na umowie). Chciałbym, żeby kod .net lepiej komunikował się, co jest formalną regułą biznesową, ponieważ te się zmieniają i trzeba je utrzymać.
Fuhrmanator
6

Na twoje pytanie nie można odpowiedzieć, ponieważ twój przykład jest zły. W szczególności, ponieważ nie ma zachowania. Przynajmniej nie w obszarze twojej domeny. Przykładem AddStatusUpdatemetody nie jest logika domeny, ale logika korzystająca z tej domeny. Tego rodzaju logika ma sens, gdy znajduje się w jakiejś usłudze, która obsługuje żądania zewnętrzne.

Na przykład, jeśli istniał wymóg, aby określony element pracy mógł mieć tylko określone statusy lub że mógł mieć tylko N statusów, oznacza to logikę domeny i powinien być częścią jednej WorkItemlub StatusHistoryjednej metody.

Przyczyną tego zamieszania jest to, że próbujesz zastosować wytyczne do kodu, który go nie potrzebuje. Modele domen są istotne tylko wtedy, gdy masz dużo złożonej logiki domeny. Na przykład. logika, która działa na same podmioty i wynika z wymagań. Jeśli kod dotyczy manipulowania jednostkami z danych zewnętrznych, najprawdopodobniej nie jest to logika domeny. Ale w momencie, gdy otrzymasz dużo ifs na podstawie danych i encji, z którymi pracujesz, to jest logika domeny.

Jednym z problemów prawdziwego modelowania domen jest to, że chodzi o zarządzanie złożonymi wymaganiami. W związku z tym jego prawdziwej mocy i zalet nie można pokazać na prostym kodzie. Potrzebujesz dziesiątek podmiotów z mnóstwem wymagań wokół nich, aby naprawdę zobaczyć korzyści. Ponownie twój przykład jest zbyt prosty, aby model domeny mógł naprawdę świecić.

Na koniec chciałbym wspomnieć, że prawdziwy model domeny z prawdziwym projektem OOP byłby naprawdę trudny do utrzymania przy użyciu Entity Framework. Podczas gdy ORM zostały zaprojektowane z odwzorowaniem prawdziwej struktury OOP na relacyjne, wciąż istnieje wiele problemów, a model relacyjny często przecieka do modelu OOP. Nawet z nHibernate, który uważam za znacznie potężniejszy niż EF, może to stanowić problem.

Euforyk
źródło
Słuszne uwagi. Do czego należałaby wówczas metoda AddStatusUpdate w Data lub innym projekcie w infrastrukturze? Jaki jest przykład każdego zachowania, które teoretycznie może należeć do WorkItem? Każdy kod psuedo lub makieta byłyby bardzo mile widziane. Mój przykład został w rzeczywistości uproszczony, aby był bardziej czytelny. Istnieją inne podmioty, na przykład AddStatusUpdate ma pewne dodatkowe zachowanie - w rzeczywistości przyjmuje nazwę kategorii statusu, a jeśli ta kategoria nie istnieje, kategoria jest tworzona.
RJB
@RJB Tak jak powiedziałem, AddStatusUpdate to kod używający domeny. Więc albo jakiś serwis internetowy lub aplikacja korzystająca z klas domen. I jak powiedziałem, nie można oczekiwać żadnego rodzaju makiety lub pseudokodu, ponieważ trzeba by wykonać cały projekt o wystarczająco dużej złożoności, aby pokazać rzeczywistą przewagę modelu domeny OOP.
Euforia
5

Twoje założenie, że enkapsulowanie logiki biznesowej związanej z WorkItem w „grubą usługę” jest nieodłącznym anty-wzorem, który, jak twierdzę, niekoniecznie.

Bez względu na twoje przemyślenia na temat anemicznego modelu domeny, standardowe wzorce i praktyki typowe dla aplikacji .NET z linii biznesowej zachęcają do warstwowego podejścia transakcyjnego składającego się z różnych komponentów. Zachęcają do oddzielenia logiki biznesowej od modelu domeny, aby ułatwić komunikację wspólnego modelu domeny między innymi komponentami .NET, a także komponentami na różnych stosach technologicznych lub na różnych poziomach fizycznych.

Jednym z przykładów może być usługa sieci Web SOAP oparta na .NET, która komunikuje się z aplikacją kliencką Silverlight, która ma bibliotekę DLL zawierającą proste typy danych. Ten projekt jednostki domeny można wbudować w zestaw .NET lub zestaw Silverlight, w którym zainteresowane komponenty Silverlight, które mają tę bibliotekę DLL, nie będą narażone na zachowania obiektów, które mogą zależeć od komponentów dostępnych tylko dla usługi.

Niezależnie od twojego stanowiska w tej debacie, jest to przyjęty i zaakceptowany wzorzec przedstawiony przez Microsoft i moim profesjonalnym zdaniem nie jest to złe podejście, ale model obiektowy, który określa własne zachowanie, niekoniecznie musi być również anty-wzorcem. Jeśli wybierzesz ten projekt, najlepiej jest zrozumieć i zrozumieć niektóre ograniczenia i problemy, na które możesz natknąć się, jeśli chcesz zintegrować się z innymi komponentami, które potrzebują modelu domeny. W tym konkretnym przypadku być może będziesz chciał, aby Translator przekonwertował obiektowy model domeny w stylu obiektowym na proste obiekty danych, które nie ujawniają pewnych metod zachowania.

wałek klonowy
źródło
1
1) Jak oddzielić logikę biznesową od modelu domeny? Jest to domena, w której żyje ta logika biznesowa; podmioty w tej domenie realizują zachowanie związane z tą logiką biznesową. W prawdziwym świecie nie ma usług ani nie istnieją one w głowach ekspertów dziedzinowych. 2) Każdy komponent, który chce się z Tobą zintegrować, musi zbudować własny model domeny, ponieważ jego potrzeby będą się różnić i będzie miał inne spojrzenie na model domeny. To od dawna oszustwo, że możesz stworzyć jeden model domeny, który można udostępniać.
Stefan Billiet
1
@StefanBilliet To są dobre argumenty na temat błędności uniwersalnego modelu domeny, ale jest to możliwe w prostszych komponentach i interakcji komponentów, tak jak wcześniej to robiłem. Moim zdaniem logika translacji między modelami domen może sprawić, że powstanie wiele żmudnych i skomplikowanych kodów, a jeśli można tego bezpiecznie uniknąć, może to być dobry wybór projektu.
wałek klonowy
1
Szczerze mówiąc, uważam, że jedynym dobrym wyborem jest model, o którym ekspert w dziedzinie biznesu może myśleć. Budujesz model domeny, aby firma mogła rozwiązać niektóre problemy w tej domenie. Podział zachowań z jednostek domeny na usługi utrudnia wszystkim zaangażowanym, ponieważ stale trzeba mapować to, co mówią eksperci domeny, na kod usługi, który prawie nie przypomina podobieństwa do bieżącej konwersacji. Z mojego doświadczenia wynika, że ​​tracisz z tym o wiele więcej czasu, niż wpisywanie bojlera. Nie oznacza to, że nie ma możliwości obejścia kodu kursu kotłowni.
Stefan Billiet
@StefanBilliet W idealnym świecie zgadzam się z Tobą, gdzie ekspert biznesowy ma czas na spotkanie z programistami. Rzeczywistość branży oprogramowania polega na tym, że ekspert biznesowy nie ma czasu ani zainteresowania zaangażowaniem się na tym poziomie lub jeszcze gorzej, ale oczekuje się, że programiści po prostu zrozumieją to na podstawie niejasnych wskazówek.
wałek klonowy
To prawda, ale nie jest to powód do zaakceptowania tej rzeczywistości. Kontynuacja takiego dążenia oznacza marnowanie czasu (i być może reputacji) deweloperów oraz pieniędzy klienta. Proces, który opisałem, jest relacją, którą należy budować w miarę upływu czasu; wymaga dużo wysiłku, ale daje znacznie lepsze wyniki. Istnieje powód, dla którego „wszechobecny język” jest często uważany za najważniejszy aspekt DDD.
Stefan Billiet
5

Zdaję sobie sprawę, że to pytanie jest dość stare, więc odpowiedź jest dla potomnych. Chcę odpowiedzieć konkretnym przykładem zamiast opartym na teorii.

Podsumuj „zmianę statusu elementu pracy” w WorkItemklasie w następujący sposób:

public SomeStatusUpdateType Status { get; private set; }

public void ChangeStatus(SomeStatusUpdateType status)
{
    // Maybe we designed this badly at first ;-)
    Status = status;       
}

Teraz twoja WorkItemklasa jest odpowiedzialna za utrzymanie się w stanie prawnym. Implementacja jest jednak dość słaba. Właściciel produktu chce mieć historię wszystkich aktualizacji statusu wprowadzonych do WorkItem.

Zmieniamy to na coś takiego:

private ICollection<SomeStatusUpdateType> StatusUpdates { get; private set; }
public SomeStatusUpdateType Status => StatusUpdates.OrderByDescending(s => s.CreatedOn).FirstOrDefault();

public void ChangeStatus(SomeStatusUpdateType status)
{
    // Better...
    StatusUpdates.Add(status);       
}

Implementacja zmieniła się drastycznie, ale osoba wywołująca ChangeStatusmetodę nie zna podstawowych szczegółów implementacji i nie ma powodu, aby się zmieniać.

To jest przykład bogatego modelu domeny, IMHO.

Don
źródło