Czy nadal można mówić o modelu anemicznym w kontekście programowania funkcjonalnego?

40

Większość wzorców projektowania taktycznego DDD należy do paradygmatu zorientowanego obiektowo, a model anemiczny opisuje sytuację, w której cała logika biznesowa jest umieszczana w usługach, a nie w obiektach, co czyni je rodzajem DTO. Innymi słowy model anemiczny jest synonimem stylu proceduralnego, który nie jest zalecany w przypadku modelu złożonego.

Nie mam dużego doświadczenia w czystym programowaniu funkcjonalnym, ale chciałbym wiedzieć, jak DDD pasuje do paradygmatu FP i czy w tym przypadku nadal istnieje termin „model anemiczny”.

Aktualizacja : Ostatnio opublikowane książki i wideo na ten temat.

Pavel Voronin
źródło
1
Jeśli mówisz to, co myślę, że tutaj mówisz, DTO są niedokrwiste, ale pierwszorzędne obiekty w DDD i że istnieje naturalny rozdział między DTO a usługami, które je przetwarzają. Zgadzam się co do zasady. Tak samo ten blogu , widocznie.
Robert Harvey
2
Silnie powiązane, jeśli nie wręcz duplikat: Dlaczego anemiczny model domeny jest uważany za zły w OOP, ale bardzo ważny w FP?
Robert Harvey
1
„czy termin„ model anemiczny ”nadal istnieje w tym przypadku” Krótka odpowiedź, termin model anemiczny został ukuty w kontekście OO. Mówienie o modelu anemicznym w kontekście PR nie ma żadnego sensu. Mogą istnieć odpowiedniki w znaczeniu opisującym, czym jest idiomatyczny FP, ale nie ma to nic wspólnego z modelami anemicznymi.
plalx
5
Eric Evans został kiedyś zapytany, co mówi ludziom, którzy go oskarżają, że to, co opisuje w swojej książce, to po prostu dobry projekt obiektowy, i odpowiedział, że to nie jest oskarżenie, to prawda, DDD jest po prostu dobry OOD, właśnie napisał zanotować niektóre przepisy i wzory oraz nadać im nazwy, aby łatwiej było ich przestrzegać i rozmawiać o nich. Nic więc dziwnego, że DDD jest powiązany z OOD. Szerszym pytaniem byłoby, jakie są skrzyżowania i różnice między OOD a FPD, chociaż najpierw trzeba by zdefiniować, co rozumiesz przez „programowanie funkcjonalne”.
Jörg W Mittag
2
@ JörgWMittag: Masz na myśli inną niż zwykła definicja? Istnieje wiele ilustracyjnych platform, z których najbardziej oczywistym jest Haskell.
Robert Harvey

Odpowiedzi:

24

Sposób, w jaki opisany jest problem „modelu anemicznego”, nie przekłada się dobrze na FP. Najpierw należy go odpowiednio uogólnić. Sercem modelu anemicznego jest model, który zawiera wiedzę o tym, jak właściwie go używać, który nie jest zamknięty w samym modelu. Zamiast tego wiedza ta rozprzestrzenia się na stos powiązanych usług. Usługi te powinny być tylko klientami tego modelu, ale z powodu anemii ponoszą za to odpowiedzialność . Rozważmy na przykład Accountklasę, której nie można użyć do aktywacji lub dezaktywacji kont, a nawet wyszukiwania informacji o koncie, chyba że są obsługiwane przez AccountManagerklasę. Konto powinno być odpowiedzialne za podstawowe operacje na nim, a nie niektóre zewnętrzne klasy menedżerów.

W programowaniu funkcjonalnym podobny problem występuje, gdy typy danych nie odzwierciedlają dokładnie tego, co powinny modelować. Załóżmy, że musimy zdefiniować typ reprezentujący identyfikatory użytkownika. Definicja „anemiczna” oznaczałaby, że identyfikatory użytkowników są łańcuchami. Jest to technicznie wykonalne, ale napotyka ogromne problemy, ponieważ identyfikatory użytkowników nie są używane jak ciągi arbitralne. Nie ma sensu ich łączenia ani wycinania podciągów, Unicode nie powinien mieć większego znaczenia i powinny być łatwe do osadzenia w adresach URL i innych kontekstach o ściśle ograniczonych znakach i formatach.

Rozwiązanie tego problemu zwykle przebiega w kilku etapach. Prostym pierwszym cięciem jest powiedzenie: „Cóż, a UserIDjest reprezentowane równorzędnie z łańcuchem, ale są to różne typy i nie można użyć jednego tam, gdzie oczekujesz drugiego”. Haskell (i niektóre inne wpisane języki funkcjonalne) udostępnia tę funkcję poprzez newtype:

newtype UserID = UserID String

To definiuje UserIDfunkcję, która gdy dali Stringkonstruuje wartość, która jest traktowana jakUserID przez system typu, ale jest nadal tylko Stringprzy starcie. Teraz funkcje mogą zadeklarować, że wymagają UserIDzamiast ciągu znaków; używając UserIDs, gdzie wcześniej używałeś łańcuchów chroniących przed kodem UserIDłączącym dwa s razem. System typów gwarantuje, że to się nie stanie, nie są wymagane żadne testy.

Słabością jest to, że kod może nadal brać dowolnej Stringjak "hello"i zbudować UserIDz niego. Dalsze kroki obejmują utworzenie „inteligentnego konstruktora”, który po podaniu łańcucha sprawdza niektóre niezmienniki i zwraca tylko, UserIDjeśli są spełnione. Następnie „głupi” UserIDkonstruktor zostaje ustawiony na prywatny, więc jeśli klient chce UserID, musi użyć inteligentnego konstruktora, zapobiegając w ten sposób powstaniu zniekształconych identyfikatorów użytkowników.

Nawet dalsze kroki definiują UserIDtyp danych w taki sposób, że niemożliwe jest zbudowanie takiego, który jest zniekształcony lub „niewłaściwy”, po prostu z definicji. Na przykład zdefiniowanie UserIDjako listy cyfr:

data Digit = Zero | One | Two | Three | Four | Five | Six | Seven | Eight | Nine
data UserID = UserID [Digit]

Aby zbudować UserIDlistę cyfr, należy podać. Biorąc pod uwagę tę definicję, trywialne jest pokazanie, że UserIDistnienie, którego nie można przedstawić w adresie URL, jest niemożliwe . Definiowanie modeli danych, jak to w Haskell jest często wspomagane przez zaawansowanych funkcji systemowych, takich jak typ Rodzaje danych i typy uogólnione dane (GADTs algebraiczne) , które pozwalają na system typu zdefiniować i udowodnić więcej niezmienników o kodzie. Gdy dane są oddzielone od zachowania, definicja danych jest jedynym sposobem wymuszenia zachowania.

Jacek
źródło
2
A co z agregatami i zagregowanymi pierwiastkami chroniącymi niezmienniki, czy te ostatnie również mogą zostać wyrażone i łatwo zrozumiane przez programistów? Dla mnie najcenniejszą częścią DDD jest bezpośrednie mapowanie modelu biznesowego na kod. I odpowiedź jest dokładnie o tym.
Pavel Voronin
2
Fajna mowa, ale brak odpowiedzi na pytanie OP.
SerG
10

Niezmienność w dużym stopniu sprawia, że ​​nie jest konieczne ścisłe łączenie funkcji z danymi, jak zaleca OOP. Możesz wykonać dowolną liczbę kopii, nawet tworząc pochodne struktury danych, w kodzie daleko odbiegającym od oryginalnego kodu, bez obawy, że oryginalna struktura danych nieoczekiwanie zmieni się spod ciebie.

Jednak lepszym sposobem na dokonanie tego porównania jest prawdopodobnie sprawdzenie, które funkcje przypisujesz do warstwy modelu w porównaniu z warstwą usług . Mimo, że nie wygląda tak samo jak w OOP, w FP często popełniany jest błąd polegający na wtłoczeniu wielu funkcji abstrakcji w jedną funkcję.

O ile mi wiadomo, nikt nie nazywa go modelem anemicznym, ponieważ jest to termin OOP, ale efekt jest taki sam. Możesz i powinieneś ponownie używać funkcji ogólnych tam, gdzie ma to zastosowanie, ale w przypadku bardziej złożonych lub specyficznych dla aplikacji operacji powinieneś również zapewnić bogaty zestaw funkcji tylko do pracy z twoim modelem. Tworzenie odpowiednich warstw abstrakcji to dobry projekt w każdym paradygmacie.

Karl Bielefeldt
źródło
2
„W dużej mierze niezmienność sprawia, że ​​niepotrzebne jest ścisłe łączenie funkcji z danymi, jak zaleca OOP.”: Innym powodem łączenia danych i procedur jest wdrażanie polimorfizmu poprzez dynamiczną wysyłkę.
Giorgio
2
Główną zaletą łączenia zachowań z danymi w kontekście DDD jest dostarczenie sensownego interfejsu biznesowego; jest zawsze pod ręką. Mamy naturalny sposób na samodokumentowanie kodu (przynajmniej do tego jestem przyzwyczajony) i jest to klucz do udanej komunikacji z ekspertami biznesowymi. Jak zatem osiąga się to w FP? Rurociąg prawdopodobnie pomaga, ale co jeszcze? Czy ogólny charakter FP nie sprawia, że ​​wymagania biznesowe są trudniejsze do wycofania z kodu?
Pavel Voronin
7

Podczas korzystania z DDD w OOP jednym z głównych powodów umieszczania logiki biznesowej w samych obiektach domeny jest to, że logika biznesowa jest zwykle stosowana przez mutowanie stanu obiektu. Jest to związane z enkapsulacją: Employee.RaiseSalaryprawdopodobnie mutuje salarypole Employeeinstancji, które nie powinno być publicznie ustawiane.

W FP unika się mutacji, więc zaimplementowałbyś to zachowanie, tworząc RaiseSalaryfunkcję, która pobiera istniejące Employeewystąpienie i zwraca nowe Employee wystąpienie z nową pensją. Zatem nie jest wymagana żadna mutacja: tylko czytanie z oryginalnego obiektu i tworzenie nowego obiektu. Z tego powodu taka RaiseSalaryfunkcja nie musi być zdefiniowana jako metoda w Employeeklasie, ale może żyć w dowolnym miejscu.

W takim przypadku naturalne staje się oddzielanie danych od zachowania: jedna struktura reprezentuje Employeedane jako (całkowicie anemiczne), podczas gdy jeden (lub kilka) modułów zawiera funkcje, które działają na tych danych (zachowując niezmienność).

Zauważ, że łącząc dane i zachowanie, tak jak w DDD, zasadniczo naruszasz Zasadę Jednej Odpowiedzialności (SRP): Employeemoże być konieczna zmiana, jeśli zmienią się zasady dotyczące zmian wynagrodzeń; ale może również wymagać zmiany, jeśli zasady obliczania premii EOY ulegną zmianie. W przypadku podejścia oddzielonego od produkcji nie ma to miejsca, ponieważ możesz mieć kilka modułów, każdy z jedną odpowiedzialnością.

Jak zwykle podejście FP zapewnia większą modułowość / kompozycyjność.

la-yumba
źródło
-1

Myślę, że istotą sprawy jest to, że anemiczny model z całą logiką domen w usługach działających na tym modelu jest zasadniczo programowaniem proceduralnym - w przeciwieństwie do „prawdziwego” programowania OO, w którym masz obiekty „inteligentne” i zawierające nie tylko dane ale także logikę, która jest najbardziej związana z danymi.

Ten sam kontrast występuje z programowaniem funkcjonalnym: „rzeczywisty” FP oznacza używanie funkcji jako elementów pierwszej klasy, które są przekazywane jako parametry, a także budowane w locie i zwracane jako wartość zwracana. Ale kiedy nie wykorzystasz całej tej mocy i masz tylko funkcje działające na strukturach danych, które są między nimi przekazywane, to kończysz w tym samym miejscu: zasadniczo programujesz procedury.

Michael Borgwardt
źródło
5
Tak, w zasadzie tak mówi OP w swoim pytaniu. Wydaje się, że oboje nie zauważyliście punktu, w którym wciąż można mieć funkcjonalną kompozycję.
Robert Harvey
-3

Chciałbym wiedzieć, jak DDD pasuje do paradygmatu FP

Myślę, że tak, ale głównie jako taktyczne podejście do przechodzenia między niezmiennymi obiektami wartości lub jako sposób na uruchomienie metod na bytach. (Gdzie większość logiki wciąż żyje w bycie).

oraz czy w tym przypadku nadal istnieje pojęcie „model anemiczny”.

Cóż, jeśli masz na myśli „w sposób analogiczny do tradycyjnego OOP”, to pomaga zignorować zwykłe szczegóły implementacji i wrócić do podstaw: Jakiego języka używają Twoi eksperci w dziedzinie? Jakie masz zamiary przechwytywania od użytkowników?

Załóżmy, że rozmawiają o łączeniu procesów i funkcji razem, to wydaje się, że funkcje (a przynajmniej obiekty „do-er”) są w zasadzie twoimi domenami!

Zatem w tym scenariuszu „model anemiczny” prawdopodobnie wystąpiłby, gdy „funkcje” nie byłyby w rzeczywistości wykonywalne, a byłyby jedynie konstelacjami metadanych, które są interpretowane przez usługę, która naprawdę działa.

Darien
źródło
1
Model anemiczny wystąpiłby, gdy przekazywałbyś abstrakcyjnym typom danych, takim jak krotki, rekordy lub listy, różne funkcje do przetwarzania. Nie potrzebujesz niczego tak egzotycznego jak „funkcja, która nie wykonuje się” (cokolwiek to jest).
Robert Harvey
Stąd cudzysłowy wokół „funkcji”, aby podkreślić, jak niewłaściwa staje się etykieta, gdy są anemiczne.
Darien
Jeśli jesteś ironiczny, to jest trochę subtelne.
Robert Harvey