Dlaczego „ścisłe powiązanie funkcji z danymi” jest złe?

38

Znalazłem ten cytat w „ Radości Clojure'a ” na str. 32, ale ktoś powiedział mi to samo podczas kolacji w zeszłym tygodniu i słyszałem to także w innych miejscach:

[A] wadą programowania obiektowego jest ścisłe powiązanie funkcji i danych.

Rozumiem, dlaczego niepotrzebne sprzężenie jest złe w aplikacji. Nie mam też wątpliwości, że należy unikać stanu zmiennego i dziedziczenia, nawet w programowaniu obiektowym. Ale nie rozumiem, dlaczego przyczepianie funkcji do klas jest z natury złe.

Mam na myśli, że dodanie funkcji do klasy wydaje się oznaczać tagowanie poczty w Gmailu lub umieszczanie pliku w folderze. Jest to technika organizacyjna, która pomaga znaleźć ją ponownie. Wybierasz niektóre kryteria, a następnie łączysz je razem. Przed OOP nasze programy były dość dużymi torbami metod w plikach. To znaczy, musisz gdzieś umieścić funkcje. Dlaczego ich nie uporządkować?

Jeśli jest to zawoalowany atak na typy, dlaczego nie powiedzą po prostu, że ograniczenie typu wejścia i wyjścia do funkcji jest niewłaściwe? Nie jestem pewien, czy mógłbym się z tym zgodzić, ale przynajmniej znam argumenty za i przeciw bezpieczeństwu. To brzmi dla mnie jak osobna sprawa.

Pewnie, czasami ludzie źle to rozumieją i przypisują funkcjonalność niewłaściwej klasie. Ale w porównaniu z innymi błędami wydaje się to bardzo niewielką niedogodnością.

Więc Clojure ma przestrzenie nazw. W jaki sposób naklejanie funkcji na klasę w OOP różni się od naklejania funkcji w przestrzeni nazw w Clojure i dlaczego jest tak źle? Pamiętaj, funkcje w klasie niekoniecznie działają tylko na członków tej klasy. Spójrz na java.lang.StringBuilder - działa na dowolnym typie odwołania lub poprzez automatyczne boxowanie na dowolnym typie.

PS Ten cytat odnosi się do książki, której nie przeczytałem: Multiparadigm Programming in Leda: Timothy Budd, 1995 .

GlenPeterson
źródło
20
Wierzę, że pisarz po prostu nie zrozumiał poprawnie OOP i potrzebował jeszcze jednego powodu, aby powiedzieć, że Java jest zła, a Clojure jest dobra. / rant
Euphoric
6
Metod instancji (w przeciwieństwie do darmowych funkcji lub metod rozszerzeń) nie można dodać z innych modułów. Staje się to bardziej ograniczeniem, gdy weźmie się pod uwagę interfejsy, które można zaimplementować tylko za pomocą metod instancji. Nie można zdefiniować interfejsu i klasy w różnych modułach, a następnie użyć kodu z trzeciego modułu, aby połączyć je ze sobą. Bardziej elastyczne podejście, takie jak klasy typu haskell, powinno być w stanie to zrobić.
CodesInChaos
4
@Euphoric wierzę, że pisarz nie zrozumieć, ale społeczność Clojure wydaje się jak zrobić mężczyznę słomkowy OOP i nagrać go jako podobiznę za wszelkie zło programowania przed mieliśmy dobre zbieranie śmieci, dużo pamięci, szybkie procesory i dużo miejsca na dysku. Chciałbym, żeby przestali bić w OOP i celowali w prawdziwe przyczyny: na przykład architekturę von Neumana.
GlenPeterson
4
Mam wrażenie, że większość krytyki OOP to tak naprawdę krytyka OOP zaimplementowana w Javie. Nie dlatego, że jest to celowy człowiek ze słomy, ale dlatego, że kojarzy się z OOP. Istnieją dość podobne problemy z ludźmi narzekającymi na pisanie statyczne. Większość problemów nie jest nierozerwalnie związana z koncepcją, ale po prostu ma wadę w popularnej implementacji tej koncepcji.
CodesInChaos
3
Twój tytuł nie pasuje do treści pytania. Łatwo jest wyjaśnić, dlaczego ścisłe połączenie funkcji i danych jest złe, ale w tekście zadaje się pytania „Czy OOP to robi?”, „Jeśli tak, to dlaczego?” i „Czy to zła rzecz?”. Do tej pory miałeś szczęście otrzymać odpowiedzi, które dotyczą jednego lub więcej z tych trzech pytań i żadne nie zakładało prostszego pytania w tytule.
itsbruce

Odpowiedzi:

34

Teoretycznie luźne sprzęganie funkcji z danymi ułatwia dodawanie większej liczby funkcji do pracy na tych samych danych. Wadą jest to, że trudniej jest zmienić samą strukturę danych, dlatego w praktyce dobrze zaprojektowany kod funkcjonalny i dobrze zaprojektowany kod OOP mają bardzo podobne poziomy sprzężenia.

Weź przykładowy wykres acykliczny (DAG) jako przykładową strukturę danych. W programowaniu funkcjonalnym nadal potrzebujesz pewnej abstrakcji, aby uniknąć powtarzania się, więc stworzysz moduł z funkcjami do dodawania i usuwania węzłów i krawędzi, znajdowania węzłów dostępnych z danego węzła, tworzenia sortowania topologicznego itp. Te funkcje są skutecznie ściśle powiązane z danymi, nawet jeśli kompilator ich nie wymusza. Możesz dodać węzeł na poważnie, ale dlaczego chcesz? Spójność w ramach jednego modułu zapobiega szczelnemu połączeniu w całym systemie.

I odwrotnie, po stronie OOP wszelkie funkcje inne niż podstawowe operacje DAG będą wykonywane w osobnych klasach „view”, a obiekt DAG zostanie przekazany jako parametr. Tak łatwo można dodać dowolną liczbę widoków, które działają na danych DAG, tworząc taki sam poziom oddzielania funkcji od danych, jak w programie funkcjonalnym. Kompilator nie powstrzyma cię przed wtłoczeniem wszystkiego w jedną klasę, ale zrobią to twoi koledzy.

Zmiana paradygmatów programowania nie zmienia najlepszych praktyk abstrakcji, spójności i łączenia, a jedynie zmiany praktyk kompilatora pomagających w egzekwowaniu. W programowaniu funkcjonalnym, gdy chcesz połączyć funkcję z danymi, wymusza je zgoda dżentelmenów, a nie kompilator. W OOP separacja widoków modelu jest wymuszana przez dżentelmeńską zgodę, a nie kompilator.

Karl Bielefeldt
źródło
13

Jeśli jeszcze tego nie wiesz, weź ten wgląd: pojęcia obiektowe i zamknięcia są dwiema stronami tej samej monety. To powiedziawszy, co to jest zamknięcie? Pobiera zmienne lub dane z otaczającego zakresu i wiąże się z nim wewnątrz funkcji lub z perspektywy OO skutecznie robisz to samo, gdy na przykład przekazujesz coś do konstruktora, aby później móc z niego korzystać kawałek danych w funkcji członka tego wystąpienia. Ale zabranie rzeczy z otaczającego zakresu nie jest przyjemną rzeczą - im większy jest otaczający zakres, tym bardziej jest to złe (chociaż pragmatycznie, pewne zło jest często konieczne do wykonania pracy). Używanie zmiennych globalnych doprowadza to do skrajności, gdzie funkcje w programie używają zmiennych w zakresie programu - naprawdę naprawdę złe. Tam sądobre opisy dotyczące tego, dlaczego zmienne globalne są złe.

Jeśli zastosujesz się do technik OO, w zasadzie już akceptujesz, że każdy moduł w twoim programie będzie miał pewien minimalny poziom zła. Jeśli podejmiesz funkcjonalne podejście do programowania, dążysz do ideału, w którym żaden moduł w twoim programie nie będzie zawierał zła zamknięcia, chociaż możesz nadal mieć pewne, ale będzie to o wiele mniej niż OO.

Jest to wada OO - zachęca do tego rodzaju zła, łączenia danych do działania poprzez ujednolicenie zamknięć (rodzaj teorii programowania w oknie zepsutym ).

Jedyną zaletą jest to, że jeśli wiesz, że na początku użyjesz wielu zamknięć, OO zapewnia przynajmniej idealogiczne ramy, które pomogą uporządkować takie podejście, aby przeciętny programista mógł je zrozumieć. W szczególności zmienne, które są zamykane, są jawne w konstruktorze, a nie tylko domyślnie brane w zamknięciu funkcji. Programy funkcjonalne, które używają wielu zamknięć, są często bardziej tajemnicze niż równoważny program OO, choć niekoniecznie mniej elegancki :)

Benedykt
źródło
8
Cytat dnia: „często konieczne jest zło, aby wykonać pracę”
GlenPeterson,
5
Naprawdę nie wyjaśniłeś, dlaczego rzeczy, które nazywasz złem, są złe; po prostu nazywacie ich złymi. Wyjaśnij, dlaczego są źli, a możesz uzyskać odpowiedź na pytanie dżentelmena.
Robert Harvey
2
Ostatni akapit zapisuje jednak odpowiedź. Według ciebie może to być jedyny plus, ale to nie jest mała rzecz. My, tak zwani „przeciętni programiści”, w rzeczywistości witamy pewną ilość ceremonii, z pewnością wystarczającą, aby dać nam znać, co się do cholery dzieje.
Robert Harvey
Jeśli OO i zamknięcia są synonimami, dlaczego tak wiele języków OO nie zapewnia ich wyraźnego wsparcia? Cytowana strona wiki C2 ma jeszcze więcej sporów (i mniej konsensusu) niż jest to normalne dla tej strony.
itsbruce
1
@itsbruce Są one w dużej mierze niepotrzebne. Zmienne, które byłyby „zamknięte”, zamiast tego stają się zmiennymi klasowymi przekazywanymi do obiektu.
Izkata,
7

Chodzi o sprzężenie typu :

Funkcja wbudowana w obiekt do pracy na tym obiekcie nie może być używana na innych typach obiektów.

W Haskell piszesz funkcje do pracy z klasami typów - więc istnieje wiele różnych typów obiektów, z którymi każda dana funkcja może działać, o ile jest to typ danej klasy, na którym działa funkcja.

Funkcje wolnostojące pozwalają na takie oddzielenie, którego nie można uzyskać, koncentrując się na pisaniu funkcji do pracy wewnątrz typu A, ponieważ wtedy nie można ich użyć, jeśli nie ma instancji typu A, nawet jeśli funkcja może w przeciwnym razie powinien być na tyle ogólny, aby można go było używać w instancji typu B lub instancji typu C.

Jimmy Hoffa
źródło
3
Czy nie o to chodzi w interfejsach? Aby zapewnić rzeczy, które pozwalają typowi B i typowi C wyglądać tak samo dla twojej funkcji, aby mogła działać na więcej niż jednym typie?
Random832,
2
@ Random832 absolutnie, ale po co osadzać funkcję w typie danych, jeśli nie ma pracować z tym typem danych? Odpowiedź: To jedyny powód, aby osadzić funkcję w typie danych. Możesz napisać tylko klasy statyczne i sprawić, że wszystkie funkcje nie dbają o typ danych, w których są zawarte, aby całkowicie oddzielić od własnego typu, ale po co więc w ogóle wkładać je w typ? Podejście funkcjonalne mówi: nie przejmuj się, pisz swoje funkcje, aby pracować w kierunku interfejsów, a wtedy nie ma powodu, aby łączyć je z danymi.
Jimmy Hoffa,
Nadal musisz wdrożyć interfejsy.
Random832,
2
@ Random832 interfejsy są typami danych; nie potrzebują w nich żadnych funkcji. Dzięki bezpłatnym funkcjom wszystkie interfejsy, które muszą wychwalać, to dane, które udostępniają, aby funkcje działały.
Jimmy Hoffa,
2
@ Random832, aby odnosić się do obiektów w świecie rzeczywistym, tak powszechnych w OO, pomyśl o interfejsie książki: przedstawia informacje (dane), to wszystko. Masz bezpłatną funkcję przewracania strony, która działa przeciwko klasie typów, które mają strony, ta funkcja działa na wszelkiego rodzaju książkach, gazetach, wrzecionach plakatów w K-Mart, kartkach pocztowych, pocztach, cokolwiek zszytych razem w kąt. Jeśli zaimplementowałeś stronę zwrotną jako członek książki, przegapisz wszystkie rzeczy, których możesz użyć na stronie zwrotnej, jako bezpłatna funkcja nie jest związana; po prostu rzuca wyjątek PartyFoulException na piwo.
Jimmy Hoffa
4

W Javie i podobnych wcieleniach OOP metod instancji (w przeciwieństwie do darmowych funkcji lub metod rozszerzeń) nie można dodawać z innych modułów.

Staje się to bardziej ograniczeniem, gdy weźmie się pod uwagę interfejsy, które można zaimplementować tylko za pomocą metod instancji. Nie można zdefiniować interfejsu i klasy w różnych modułach, a następnie użyć kodu z trzeciego modułu, aby połączyć je ze sobą. Bardziej elastyczne podejście, takie jak klasy typu Haskell, powinno być w stanie to zrobić.

CodesInChaos
źródło
Możesz to łatwo zrobić w Scali. Go nie znam, ale AFAIK też możesz to zrobić. W Ruby dość powszechną praktyką jest dodawanie metod do obiektów po fakcie, aby były zgodne z jakimś interfejsem. To, co opisujesz, wydaje się raczej źle zaprojektowanym systemem pisania niż czymkolwiek, nawet zdalnie związanym z OO. Podobnie jak eksperyment myślowy: czym różni się twoja odpowiedź, gdy mówisz o abstrakcyjnych typach danych zamiast o obiektach? Nie sądzę, żeby miało to jakikolwiek wpływ, co dowodziłoby, że twój argument nie ma związku z OO.
Jörg W Mittag,
1
@ JörgWMittag Myślę, że miałeś na myśli algebraiczne typy danych. A CodesInChaos, Haskell bardzo wyraźnie odradza to, co sugerujesz. To się nazywa osierocona instancja i wyświetla ostrzeżenia w GHC.
jozefg
3
@ JörgWMittag Mam wrażenie, że wielu, którzy krytykują OOP, krytykuje formę OOP używaną w Javie i podobnych językach, ze sztywną strukturą klas i metodami instancji. Mam wrażenie, że ten cytat krytykuje skupienie się na metodach instancji i tak naprawdę nie dotyczy innych smaków OOP, takich jak to, czego używa Golang.
CodesInChaos
2
@CodesInChaos W takim razie być może wyjaśniając to jako „OO oparty na klasie statycznej”
jozefg
@ jozefg: Mówię o abstrakcyjnych typach danych. Nie rozumiem nawet, w jaki sposób algebraiczne typy danych są zdalnie istotne w tej dyskusji.
Jörg W Mittag,
3

Orientacja obiektowa zasadniczo dotyczy pozyskiwania danych proceduralnych (lub pozyskiwania danych funkcjonalnych, jeśli usuniesz skutki uboczne, które są kwestią ortogonalną). W pewnym sensie Lambda Calculus jest najstarszym i najczystszym językiem obiektowym, ponieważ zapewnia tylko funkcjonalną abstrakcję danych (ponieważ nie ma żadnych konstrukcji poza funkcjami).

Tylko operacje jednego obiektu mogą sprawdzać reprezentację danych tego obiektu. Nie mogą tego zrobić nawet inne obiekty tego samego typu . (Jest to główna różnica między obiektową abstrakcją danych a abstrakcyjnymi typami danych: w przypadku narzędzi ADT obiekty tego samego typu mogą sprawdzać wzajemnie reprezentację danych, ukryta jest tylko reprezentacja obiektów innych typów).

Oznacza to, że kilka obiektów tego samego typu może mieć różne reprezentacje danych. Nawet ten sam obiekt może mieć różne reprezentacje danych w różnych momentach. (Na przykład w Scali Maps i Sets przełączają się między tablicą a funkcją mieszania w zależności od liczby elementów, ponieważ dla bardzo małych liczb wyszukiwanie liniowe w tablicy jest szybsze niż wyszukiwanie logarytmiczne w drzewie wyszukiwania ze względu na bardzo małe stałe czynniki .)

Z zewnątrz obiektu nie powinieneś, nie możesz znać jego reprezentacji danych. To przeciwieństwo ciasnego połączenia.

Jörg W Mittag
źródło
Mam klasy w OOP, które zmieniają wewnętrzne struktury danych w zależności od okoliczności, więc instancje obiektów tych klas mogą korzystać z bardzo różnych reprezentacji danych w tym samym czasie. Podstawowe ukrywanie danych i enkapsulacja Powiedziałbym? Czym więc Map w Scali różni się od poprawnie zaimplementowanej (ukrywanie i enkapsulacja danych wrt) klasy Map w języku OOP?
Marjan Venema,
W twoim przykładzie enkapsulacja danych za pomocą funkcji akcesora w klasie (a zatem ścisłe połączenie tych funkcji z tymi danymi) faktycznie pozwala ci luźno powiązać instancje tej klasy z resztą twojego programu. Odrzucasz centralny punkt cytatu - bardzo miło!
GlenPeterson
2

Ścisłe sprzężenie między danymi a funkcjami jest złe, ponieważ chcesz mieć możliwość zmiany każdego niezależnie od siebie, a ścisłe sprzężenie utrudnia to, ponieważ nie możesz zmienić jednego bez wiedzy i ewentualnie zmiany drugiego.

Chcesz, aby różne dane prezentowane tej funkcji nie wymagały żadnych zmian w funkcji i podobnie chcesz mieć możliwość wprowadzania zmian w funkcji bez potrzeby wprowadzania zmian w danych, na których działa, w celu obsługi tych zmian funkcji.

Michael Durrant
źródło
1
Tak, chcę to. Ale z mojego doświadczenia wynika, że ​​gdy wysyłasz dane do nietrywialnej funkcji, do której obsługi nie została specjalnie zaprojektowana, funkcja ta ma tendencję do psucia się. Nie odnoszę się tylko do bezpieczeństwa typu, ale raczej do warunków danych, których autor nie przewidział (-y). Jeśli funkcja jest stara i często używana, każda zmiana, która pozwala na przepływ nowych danych, prawdopodobnie spowoduje jej uszkodzenie w przypadku starej formy danych, która nadal musi działać. Chociaż odsprzęganie może być idealnym rozwiązaniem dla funkcji vs. danych, rzeczywistość tego odsprzęgania może być trudna i niebezpieczna.
GlenPeterson