Czytając wredny artykuł na temat wad OOP na rzecz innego paradygmatu natknąłem się na przykład, z którym nie mogę znaleźć zbyt wiele winy.
Chcę być otwarty na argumenty autora i chociaż teoretycznie rozumiem ich argumenty, szczególnie w jednym przykładzie trudno mi sobie wyobrazić, jak można by to lepiej zaimplementować, powiedzmy, w języku FP.
// Consider the case where “SimpleProductManager” is a child of
// “ProductManager”:
public class SimpleProductManager implements ProductManager {
private List products;
public List getProducts() {
return products;
}
public void increasePrice(int percentage) {
if (products != null) {
for (Product product : products) {
double newPrice = product.getPrice().doubleValue() *
(100 + percentage)/100;
product.setPrice(newPrice);
}
}
}
public void setProducts(List products) {
this.products = products;
}
}
// There are 3 behaviors here:
getProducts()
increasePrice()
setProducts()
// Is there any rational reason why these 3 behaviors should be linked to
// the fact that in my data hierarchy I want “SimpleProductManager” to be
// a child of “ProductManager”? I can not think of any. I do not want the
// behavior of my code linked together with my definition of my data-type
// hierarchy, and yet in OOP I have no choice: all methods must go inside
// of a class, and the class declaration is also where I declare my
// data-type hierarchy:
public class SimpleProductManager implements ProductManager
// This is a disaster.
Zauważ, że nie szukam obalenia argumentów autora przeciwko „Czy istnieje jakiś racjonalny powód, dla którego te 3 zachowania powinny być powiązane z hierarchią danych?”.
W szczególności pytam, w jaki sposób ten przykład zostałby modelowany / zaprogramowany w języku FP (rzeczywisty kod, nie teoretycznie)?
object-oriented
functional-programming
Danny Jarosławski
źródło
źródło
Odpowiedzi:
W stylu FP
Product
byłby niezmienną klasą,product.setPrice
nie mutowałbyProduct
obiektu, ale zamiast tego zwraca nowy obiekt, aincreasePrice
funkcja byłaby funkcją „samodzielną”. Przy użyciu podobnej składni, takiej jak Twoja (C # / Java like), równoważna funkcja może wyglądać następująco:Jak widać, rdzeń nie różni się tak naprawdę tutaj, z wyjątkiem tego, że pominięto kod „płyty grzewczej” z wymyślonego przykładu OOP. Jednak nie widzę tego jako dowodu, że OOP prowadzi do rozdętego kodu, tylko jako dowód na to, że jeśli zbudujemy przykład kodu, który jest wystarczająco sztuczny, możliwe jest udowodnienie czegokolwiek.
źródło
null
gdy kolekcja jest rodzajem zwrotu. / rant overW języku „a” FP? Jeśli coś wystarczy, wybieram seplenienie Emacsa. Ma pojęcie typów (rodzaj, rodzaj), ale tylko te wbudowane. Zatem twój przykład ogranicza się do „jak pomnożyć każdy element na liście przez coś i zwrócić nową listę”.
Proszę bardzo. Inne języki będą podobne, z tą różnicą, że zyskasz wyraźne typy ze zwykłą funkcjonalną semantyką „dopasowywania”. Sprawdź Haskell:
(Lub coś takiego, to było wieki ...)
Czemu? Próbowałem przeczytać artykuł; Musiałem się poddać po stronie i szybko zeskanowałem resztę.
Problemem tego artykułu nie jest to, że jest on przeciwko OOP. Nie jestem też ślepo „pro OOP”. Programowałem z logiką, paradygmatami funkcjonalnymi i OOP, dość często w tym samym języku, jeśli to możliwe, i często bez żadnego z trzech, czysto imperatywnych, a nawet na poziomie asemblera. Nigdy nie powiedziałbym, że którykolwiek z tych paradygmatów jest pod każdym względem znacznie nadrzędny. Czy twierdziłbym, że bardziej lubię język X niż Y? Oczywiście że tak! Ale nie o tym jest ten artykuł.
Problemem tego artykułu jest to, że używa on mnóstwa narzędzi retorycznych (błędów) od pierwszego do ostatniego zdania. Nie ma sensu nawet opisywać wszystkich zawartych w nim błędów. Autor wyraźnie stwierdza, że nie ma zainteresowania dyskusją, jest na krucjacie. Dlaczego więc zawracać sobie głowę?
W końcu wszystkie te rzeczy są tylko narzędziami do wykonania pracy. Mogą istnieć miejsca pracy, w których OOP jest lepszy, i mogą istnieć inne miejsca pracy, w których FP jest lepszy lub w których oba są nadmierne. Ważne jest, aby wybrać odpowiednie narzędzie do pracy i wykonać to.
źródło
Autor wysunął bardzo dobry punkt, a następnie wybrał bezbłędny przykład, aby spróbować wykonać kopię zapasową. Skarga nie dotyczy implementacji klasy, lecz idei, że hierarchia danych jest nierozerwalnie połączona z hierarchią funkcji.
Wynika z tego, że aby zrozumieć punkt autora, nie pomogłoby tylko zobaczyć, jak zaimplementowałby tę pojedynczą klasę w funkcjonalnym stylu. Musisz zobaczyć, jak zaprojektowałby cały kontekst danych i funkcji w tej klasie w funkcjonalnym stylu.
Pomyśl o potencjalnych typach danych związanych z produktami i cenami. Aby przeprowadzić burzę mózgów kilka: nazwa, kod upc, kategoria, waga przesyłki, cena, waluta, kod rabatowy, reguła rabatowa.
Jest to łatwa część projektowania obiektowego. Po prostu tworzymy klasę dla wszystkich powyższych „obiektów” i jesteśmy dobrzy, prawda? Zrób
Product
klasę, aby połączyć kilka z nich razem?Ale poczekaj, możesz mieć kolekcje i agregaty niektórych z tych typów: Ustaw [kategoria], (kod rabatowy -> cena), (ilość -> kwota rabatu) i tak dalej. Gdzie się one mieszczą? Czy tworzymy oddzielne narzędzie
CategoryManager
do śledzenia wszystkich rodzajów kategorii, czy też odpowiedzialność ta należy doCategory
klasy, którą już stworzyliśmy?A co z funkcjami, które dają zniżkę cenową, jeśli masz pewną ilość produktów z dwóch różnych kategorii? Czy to idzie w
Product
klasie,Category
klasie,DiscountRule
klasie,CategoryManager
klasie, czy potrzebujemy czegoś nowego? W ten sposób powstaje coś takiegoDiscountRuleProductCategoryFactoryBuilder
.W kodzie funkcjonalnym hierarchia danych jest całkowicie ortogonalna względem funkcji. Możesz sortować swoje funkcje w dowolny sposób, który ma sens semantyczny. Na przykład możesz pogrupować wszystkie funkcje, które zmieniają ceny produktów, w takim przypadku sensowne byłoby wyróżnienie wspólnej funkcjonalności, takiej jak
mapPrices
w poniższym przykładzie Scala:Prawdopodobnie mógłbym dodać inne funkcje związane z cenami tutaj jak
decreasePrice
,applyBulkDiscount
itpPonieważ używamy również kolekcji
Products
, wersja OOP musi zawierać metody zarządzania tą kolekcją, ale nie chciałeś, aby ten moduł dotyczył wyboru produktu, chciałeś, aby dotyczył on cen. Sprzężenie funkcji z danymi zmusiło cię również do wrzucenia tam płyty zarządzającej kolekcjonowaniem.Możesz spróbować rozwiązać ten problem, umieszczając
products
członka w osobnej klasie, ale wtedy kończy się to bardzo ściśle powiązanymi klasami. Programiści OO uważają łączenie funkcji z danymi za bardzo naturalne, a nawet korzystne, ale wiąże się z tym duży koszt utraty elastyczności. Za każdym razem, gdy tworzysz funkcję, musisz przypisać ją do jednej i tylko jednej klasy. Za każdym razem, gdy chcesz skorzystać z funkcji, musisz znaleźć sposób na doprowadzenie jej połączonych danych do punktu użycia. Te ograniczenia są ogromne.źródło
Po prostu rozdzielenie danych i funkcji, o której wspominał autor, może wyglądać tak w języku F # („język FP”).
W ten sposób możesz wykonać podwyżkę ceny na liście produktów.
Uwaga: jeśli nie znasz FP, każda funkcja zwraca wartość. Pochodząc z języka podobnego do C, możesz traktować ostatnią instrukcję w funkcji tak, jakby miała
return
przed nią.Zawarłem kilka adnotacji typu, ale powinny być niepotrzebne. getter / setter nie są tutaj potrzebne, ponieważ moduł nie jest właścicielem danych. Ma strukturę danych i dostępne operacje. Można to również zobaczyć za pomocą
List
, który narażamap
na uruchomienie funkcji na każdym elemencie na liście i zwraca wynik na nowej liście.Zauważ, że moduł Product nie musi wiedzieć nic o zapętlaniu, ponieważ ta odpowiedzialność spoczywa na module List (który stworzył potrzebę zapętlenia).
źródło
Pozwólcie, że poprzedzę to faktem, że nie jestem ekspertem od programowania funkcjonalnego. Jestem bardziej osobą OOP. Więc chociaż jestem prawie pewien, że poniżej można uzyskać taką samą funkcjonalność z FP, mogę się mylić.
To jest w maszynopisie (stąd wszystkie adnotacje typu). Maszynopis (jak javascript) to język wielu domen.
Szczegółowo (i znowu, nie ekspert FP), należy zrozumieć, że nie ma zbyt wielu predefiniowanych zachowań. Nie ma metody „podwyższenia ceny”, która stosuje wzrost ceny na całej liście, ponieważ oczywiście nie jest to OOP: nie ma klasy, w której można by zdefiniować takie zachowanie. Zamiast tworzyć obiekt przechowujący listę produktów, wystarczy utworzyć tablicę produktów. Następnie możesz użyć standardowych procedur FP do manipulowania tą tablicą w dowolny sposób: filtruj, aby wybrać określone elementy, mapuj, aby dostosować elementy wewnętrzne itp. Kończysz z bardziej szczegółową kontrolą nad listą produktów bez konieczności ograniczania się do Interfejs API zapewniany przez SimpleProductManager. Niektórzy mogą to uznać za zaletę. Prawdą jest również to, że nie nie musisz martwić się o bagaż związany z klasą ProductManager. Wreszcie, nie ma obaw o „SetProducts” lub „GetProducts”, ponieważ nie ma obiektu, który ukrywa twoje produkty: zamiast tego masz tylko listę produktów, z którymi pracujesz. Ponownie może to być zaletą lub wadą w zależności od okoliczności / osoby, z którą rozmawiasz. Poza tym oczywiście nie ma hierarchii klas (na co narzekał), ponieważ w ogóle nie ma klas. może to być zaletą lub wadą w zależności od okoliczności / osoby, z którą rozmawiasz. Poza tym oczywiście nie ma hierarchii klas (na co narzekał), ponieważ w ogóle nie ma klas. może to być zaletą lub wadą w zależności od okoliczności / osoby, z którą rozmawiasz. Poza tym oczywiście nie ma hierarchii klas (na co narzekał), ponieważ w ogóle nie ma klas.
Nie poświęciłem czasu na przeczytanie całego jego zdania. Używam praktyk FP, gdy jest to wygodne, ale zdecydowanie jestem bardziej typem faceta od OOP. Pomyślałem więc, że odkąd odpowiedziałem na twoje pytanie, chciałbym również krótko skomentować jego opinie. Myślę, że to bardzo wymyślny przykład, który podkreśla „wady” OOP. W tym konkretnym przypadku, dla pokazanej funkcjonalności, OOP prawdopodobnie nadmiernie zabija, a FP prawdopodobnie lepiej by pasowało. Z drugiej strony, gdyby dotyczyło to czegoś w rodzaju koszyka na zakupy, ochrona listy produktów i ograniczenie dostępu do niej jest (myślę) bardzo ważnym celem programu, a FP nie ma możliwości egzekwowania takich rzeczy. Znowu być może nie jestem ekspertem od FP, ale po wdrożeniu koszyków na systemy e-commerce wolałbym raczej OOP niż FP.
Osobiście trudno mi brać na poważnie każdego, kto tak silnie argumentuje za „X jest po prostu okropny. Zawsze używaj Y”. Programowanie ma wiele narzędzi i paradygmatów, ponieważ istnieje wiele różnych problemów do rozwiązania. FP ma swoje miejsce, OOP ma swoje miejsce i nikt nie będzie świetnym programistą, jeśli nie będzie w stanie zrozumieć wad i zalet wszystkich naszych narzędzi i kiedy z nich korzystać.
** uwaga: Oczywiście w moim przykładzie jest jedna klasa: klasa produktu. W tym przypadku jest to jednak po prostu głupi pojemnik na dane: nie sądzę, że użycie go narusza zasady FP. Jest bardziej pomocnikiem w sprawdzaniu typu.
** uwaga: nie pamiętam z głowy i nie sprawdziłem, czy sposób, w jaki korzystam z funkcji mapy, zmodyfikuje produkty w miejscu, tj. czy przypadkowo podwoiłem cenę produktów w oryginalnych produktach szyk. To oczywiście jest pewien efekt uboczny, którego FP próbuje uniknąć, a przy odrobinie więcej kodu z pewnością mogę się upewnić, że tak się nie stanie.
źródło
Nie wydaje mi się, że SimpleProductManager jest dzieckiem (rozszerza lub dziedziczy) czegoś.
Jest to po prostu implementacja interfejsu ProductManager, który jest w zasadzie umową określającą, jakie działania (zachowania) musi wykonać dany obiekt.
Jeśli byłoby to dziecko (lub, mówiąc lepiej, dziedziczona klasa lub klasa rozszerzająca funkcjonalność innej klasy), zapisano by to jako:
Zasadniczo autor mówi:
Mają jakiś obiekt, którym jest zachowanie: setProducts, wzrost ceny, getProdukty. I nie obchodzi nas, czy obiekt ma również inne zachowanie lub jak to zachowanie jest implementowane.
Implementuje go klasa SimpleProductManager. Zasadniczo wykonuje akcje.
Może być również nazywany Procentową Ceną Zwiększenia, ponieważ jego głównym zachowaniem jest podwyższanie ceny o pewną wartość procentową.
Ale możemy również zaimplementować inną klasę: ValuePriceIncreaser, która będzie się zachowywać:
Z zewnętrznego punktu widzenia nic się nie zmieniło, interfejs jest taki sam, nadal mają te same trzy metody, ale zachowanie jest inne.
Ponieważ w FP nie ma czegoś takiego jak interfejsy, wdrożenie byłoby trudne. Na przykład w C możemy trzymać wskaźniki do funkcji i wywoływać odpowiedni w zależności od naszych potrzeb. Ostatecznie w OOP działa w bardzo bardzo podobny sposób, ale jest „zautomatyzowany” przez kompilator.
źródło