Dlaczego anemiczny model domeny jest uważany za zły w C # / OOP, ale bardzo ważny w F # / FP?

46

W poście na blogu na F # dla zabawy i zysku napisano:

W funkcjonalnym projekcie bardzo ważne jest oddzielenie zachowania od danych. Typy danych są proste i „głupie”. A następnie osobno masz wiele funkcji, które działają na te typy danych.

Jest to dokładne przeciwieństwo projektowania obiektowego, w którym zachowanie i dane mają być łączone. W końcu taka właśnie jest klasa. W naprawdę zorientowanym obiektowo projekcie nie powinieneś zachowywać się inaczej - dane są prywatne i można uzyskać do nich dostęp tylko metodami.

W rzeczywistości w przypadku OOD niewystarczające zachowanie wokół typu danych jest uważane za złą rzecz, a nawet ma nazwę: „ anemiczny model domeny ”.

Biorąc pod uwagę, że w C # wydaje się, że nadal pożyczamy od F # i staramy się pisać bardziej funkcjonalny kod; dlaczego nie zapożyczamy pomysłu oddzielenia danych / zachowania, a nawet uważamy to za złe? Czy to po prostu, że definicja nie jest zgodna z OOP, czy jest konkretny powód, że jest zły w C #, który z jakiegoś powodu nie ma zastosowania w F # (i w rzeczywistości jest odwrócony)?

(Uwaga: szczególnie interesują mnie różnice w C # / F #, które mogą zmienić opinię na temat tego, co jest dobre / złe, niż osoby, które mogą nie zgadzać się z którąkolwiek opinią w blogu).

Danny Tuppeny
źródło
1
Cześć Dan! Twoja postawa jest inspirująca. Oprócz platformy .NET (i haskell) zachęcam do obejrzenia scala. Debashish ghosh napisał kilka blogów o modelowaniu domen za pomocą funkcjonalnych narzędzi, było to dla mnie wnikliwe, mam nadzieję, że również dla ciebie, proszę bardzo: debasishg.blogspot.com/2012/01/...
AndreasScheinert
2
Kolega przysłał mi dzisiaj ciekawy post na blogu: blog.inf.ed.ac.uk/sapm/2014/02/04/... Wygląda na to, że ludzie zaczynają kwestionować ideę anemicznych modeli domen; co moim zdaniem może być dobrą rzeczą!
Danny Tuppeny,
1
Odnośnik do wpisu na blogu opiera się na błędnym pomyśle: „Zwykle dane mogą zostać ujawnione bez hermetyzacji. Dane są niezmienne, więc nie można ich„ uszkodzić ”przez niewłaściwie zachowującą się funkcję.” Nawet niezmienne typy mają niezmienniki, które należy zachować, a to wymaga ukrywania danych i kontrolowania sposobu ich tworzenia. Na przykład nie można ujawnić implementacji niezmiennego czerwono-czarnego drzewa, ponieważ wtedy ktoś mógłby stworzyć drzewo składające się tylko z czerwonych węzłów.
Doval,
4
@Doval, aby być uczciwym, to znaczy powiedzieć, że nie można ujawnić programu piszącego system plików, ponieważ ktoś może zapełnić dysk. Ktoś tworzący drzewo tylko z czerwonymi węzłami absolutnie nie uszkadza czerwono-czarnego drzewa, z którego zostały sklonowane, ani żadnego kodu w całym systemie, który akurat używa tej dobrze uformowanej instancji. Jeśli napiszesz kod, który aktywnie tworzy nowe wystąpienia śmieci lub robi niebezpieczne rzeczy, niezmienność cię nie uratuje, ale uratuje innych przed tobą . Ukrywanie implementacji nie powstrzyma ludzi przed pisaniem nonsensownych kodów, które ostatecznie dzielą się przez zero.
Jimmy Hoffa
2
@ JimmyHoffa Zgadzam się, ale to nie ma nic wspólnego z tym, co skrytykowałem. Autor twierdzi, że zwykle ujawnienie danych jest w porządku, ponieważ jest niezmienne, i mówię, że niezmienność nie usuwa magicznie potrzeby ukrywania szczegółów implementacji.
Doval,

Odpowiedzi:

37

Głównym powodem, dla którego FP ma na to cel, a C # OOP nie jest to, że w PR skupiono się na przejrzystości odniesienia; to znaczy, dane przechodzą do funkcji i dane wychodzą, ale oryginalne dane nie są zmieniane.

W C # OOP istnieje koncepcja przekazania odpowiedzialności, w której delegujesz zarządzanie obiektem, a zatem chcesz, aby zmienił on swoje wewnętrzne elementy.

W FP nigdy nie chcesz zmieniać wartości w obiekcie, dlatego posiadanie funkcji osadzonych w obiekcie nie ma sensu.

Dalej w FP masz wyższy rodzaj polimorfizmu, dzięki czemu twoje funkcje są znacznie bardziej uogólnione niż pozwala C # OOP. W ten sposób możesz napisać funkcję, która działa dla dowolnej a, a zatem umieszczenie jej w bloku danych nie ma sensu; które szczelnie para sposób tak, że działa tylko z danego rodzaju z a. Takie zachowanie jest powszechne w C # OOP, ponieważ i tak nie masz możliwości abstrakcyjnego generowania funkcji, ale w FP jest to kompromis.

Największym problemem, jaki widziałem w modelach anemicznych domen w C # OOP, jest to, że kończysz się duplikatem kodu, ponieważ masz DTO x i 4 różne funkcje, które zatwierdzają działanie f do DTO x, ponieważ 4 różne osoby nie widziały innej implementacji . Kiedy umieścisz metodę bezpośrednio w DTO x, wtedy te 4 osoby zobaczą implementację f i ponownie go użyją.

Anemiczne modele danych w C # OOP utrudniają ponowne użycie kodu, ale nie dzieje się tak w FP, ponieważ pojedyncza funkcja jest uogólniona na tak wiele różnych typów, że uzyskuje się większe ponowne użycie kodu, ponieważ ta funkcja jest użyteczna w o wiele większej liczbie scenariuszy niż funkcja, którą napisałbym dla jednego DTO w C #.


Jak wskazano w komentarzach , wnioskowanie o typach jest jedną z korzyści, na których opiera się FP, umożliwiając tak znaczący polimorfizm, a konkretnie można prześledzić to z powrotem do systemu typów Hindley Milner z wnioskowaniem o typie Algorytm W. takiego wnioskowania typu w systemie typu C # OOP udało się uniknąć, ponieważ czas kompilacji po dodaniu wnioskowania opartego na ograniczeniach staje się wyjątkowo długi ze względu na konieczne wyczerpujące wyszukiwanie, szczegóły tutaj: https://stackoverflow.com/questions/3968834/generics-why -cant-the-compiler-infer-the-type-arguments-in-this-case

Jimmy Hoffa
źródło
Jakie funkcje w F # ułatwiają pisanie tego rodzaju kodu wielokrotnego użytku? Dlaczego kod w C # nie może być tak wielokrotnego użytku? (
Wydaje
7
@DannyTuppeny szczerze mówiąc F # jest kiepskim przykładem dla porównania, to tylko lekko ubrany C #; jest to zmienny język imperatywny, podobnie jak C #, ma kilka udogodnień FP, których C # nie ma, ale niewiele. Spójrz na haskell, aby zobaczyć, gdzie FP naprawdę się wyróżnia, a takie rzeczy stają się znacznie bardziej możliwe dzięki klasom typów, a także ogólnym ADT
Jimmy Hoffa
@MattFenwick Mówię wprost o C #, ponieważ o to pytał plakat. Tam, gdzie w mojej odpowiedzi odnoszę się do OOP, mam na myśli C # OOP, zmienię, aby wyjaśnić.
Jimmy Hoffa
2
Wspólną cechą języków funkcjonalnych, pozwalającą na tego rodzaju ponowne użycie, jest pisanie dynamiczne lub wnioskowanie. Chociaż sam język może wykorzystywać dobrze zdefiniowane typy danych, typowa funkcja nie dba o to, jakie są dane, o ile wykonywane są na nich operacje (inne funkcje lub arytmetyka). Jest to również dostępne w paradygmatach OO (Go, na przykład, ma niejawną implementację interfejsu pozwalającą obiektowi być Kaczką, ponieważ może latać, pływać i kwakać, ale obiekt nie został jawnie zadeklarowany jako Kaczka), ale jest prawie niezbędny do programowania funkcjonalnego.
KeithS
4
@KeithS Przez przeciążenie oparte na wartościach w Haskell Myślę, że masz na myśli dopasowanie wzorca. Zdolność Haskella do posiadania wielu funkcji najwyższego poziomu o tej samej nazwie z różnymi wzorami natychmiast wyłącza 1 funkcję najwyższego poziomu + dopasowanie wzorca.
jozefg
6

Dlaczego anemiczny model domeny jest uważany za zły w C # / OOP, ale bardzo ważny w F # / FP?

Twoje pytanie ma duży problem, który ograniczy użyteczność otrzymywanych odpowiedzi: sugerujesz / zakładasz, że F # i FP są podobne. FP to ogromna rodzina języków, w tym symboliczne przepisywanie terminów, dynamiczne i statyczne. Nawet wśród statycznie typowanych języków FP istnieje wiele różnych technologii wyrażania modeli domen, takich jak moduły wyższego rzędu w OCaml i SML (które nie istnieją w F #). F # jest jednym z tych języków funkcjonalnych, ale jest szczególnie zauważalny, ponieważ jest szczupły, a w szczególności nie zapewnia modułów wyższego rzędu ani typów bardziej dobranych.

W rzeczywistości nie mogłem zacząć mówić, jak wyrażane są modele domen w FP. Inna odpowiedź tutaj mówi bardzo konkretnie o tym, jak to się robi w Haskell i w ogóle nie ma zastosowania do Lisp (matki wszystkich języków FP), rodziny języków ML ani innych języków funkcjonalnych.

dlaczego nie zapożyczamy pomysłu oddzielenia danych / zachowania, a nawet uważamy to za złe?

Generyczne można uznać za sposób na rozdzielenie danych i zachowania. Generyczne pochodzą z rodziny funkcjonalnych języków programowania ML nie są częścią OOP. C # ma oczywiście generyczne. Można więc argumentować, że C # powoli zapożycza pomysł oddzielenia danych i zachowania.

Czy to po prostu, że definicja nie pasuje do OOP,

Uważam, że OOP opiera się na zupełnie innym założeniu, w związku z czym nie zapewnia narzędzi potrzebnych do oddzielenia danych i zachowania. Do wszystkich praktycznych celów potrzebne są typy produktów i sumy oraz wysyłka nad nimi. W ML oznacza to typy związków i rekordów oraz dopasowanie wzorców.

Sprawdź przykład, który podałem tutaj .

czy jest jakiś konkretny powód, że jest zły w C #, który z jakiegoś powodu nie ma zastosowania w F # (i w rzeczywistości jest odwrócony)?

Uważaj na przeskakiwanie z OOP do C #. C # nie jest tak purytaniczny w OOP, jak inne języki. .NET Framework jest teraz pełen ogólnych, statycznych metod, a nawet lambdas.

(Uwaga: szczególnie interesują mnie różnice w C # / F #, które mogą zmienić opinię na temat tego, co jest dobre / złe, niż osoby, które mogą nie zgadzać się z którąkolwiek opinią w blogu).

Brak typów unii i dopasowywania wzorców w języku C # sprawia, że ​​jest to prawie niemożliwe. Gdy masz tylko młotek, wszystko wygląda jak gwóźdź ...

Jon Harrop
źródło
2
... kiedy wszystko, co masz, to młotek, ludzie z OOP utworzą fabryki młotków; P +1 za naprawdę sprecyzowanie sedna tego, czego brakuje w C #, aby umożliwić sobie pełne pozbywanie się danych i zachowań: typy Unii i dopasowanie wzorca.
Jimmy Hoffa
-4

Myślę, że w aplikacji biznesowej często nie chcesz ukrywać danych, ponieważ dopasowanie wzorca niezmiennych wartości jest świetne, aby zapewnić, że obejmiesz wszystkie możliwe przypadki. Ale jeśli implementujesz złożone algorytmy lub struktury danych, lepiej ukryj szczegóły implementacji przekształcając ADT (algebraiczne typy danych) w ADT (abstrakcyjne typy danych).

Giacomo Citi
źródło
4
Czy mógłbyś wyjaśnić nieco więcej, w jaki sposób odnosi się to do programowania obiektowego a programowania funkcjonalnego?