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 .
Odpowiedzi:
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.
źródło
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 :)
źródło
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.
źródło
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ć.
źródło
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
Map
s iSet
s 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.
źródło
Ś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.
źródło