Jak byłoby to zaprogramowane w trybie innym niż OO? [Zamknięte]

11

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.

Od: http://www.smashcompany.com/technology/object-oriented-programming-is-an-expensive-disaster-which-must-end

// 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)?

Danny Jarosławski
źródło
44
Nie można racjonalnie oczekiwać, że porówna się jakiś paradygmat programowania na tak krótkich i wymyślonych przykładach. Każdy tutaj może wymyślić wymagania dotyczące kodu, które sprawią, że jego preferowany paradygmat będzie wyglądał lepiej niż odpoczynek, szczególnie jeśli niewłaściwie wdrażają inne. Tylko wtedy, gdy masz prawdziwy, duży, zmieniający się projekt, możesz uzyskać wgląd w mocne i słabe strony różnych paradygmatów.
Euforyczny
20
Nie ma nic w programowaniu OO, które nakazuje, aby te 3 metody pasowały do ​​siebie w tej samej klasie; podobnie nie ma nic w programowaniu OO, które nakazuje takie zachowanie w tej samej klasie co dane. Innymi słowy, dzięki OO Programming możesz umieścić dane w tej samej klasie co zachowanie lub możesz podzielić je na osobny byt / model. tak czy inaczej, OO tak naprawdę nie ma nic do powiedzenia na temat tego, jak dane powinny odnosić się do obiektu, ponieważ pojęcie obiektu zasadniczo dotyczy zachowania modelowania poprzez pogrupowanie logicznie powiązanych metod w klasę.
Ben Cottrell,
20
Mam 10 zdań w tym artykule i poddałem się. Nie zwracaj uwagi na mężczyznę za tą zasłoną. W innych wiadomościach nie miałem pojęcia, że ​​Prawdziwi Szkoci byli przede wszystkim programistami OOP.
Robert Harvey,
11
Jeszcze jedno zdanie kogoś, kto pisze kod proceduralny w języku OO, potem zastanawia się, dlaczego OO nie pracuje dla niego.
TheCatWhisperer
11
Chociaż niewątpliwie jest prawdą, że OOP to katastrofa pomyłek w projektowaniu od początku do końca - i jestem dumny, że jestem jego częścią! - ten artykuł jest nieczytelny, a podany przez ciebie przykład jest po prostu argumentem, że źle zaprojektowana hierarchia klas jest źle zaprojektowana.
Eric Lippert,

Odpowiedzi:

42

W stylu FP Productbyłby niezmienną klasą, product.setPricenie mutowałby Productobiektu, ale zamiast tego zwraca nowy obiekt, a increasePricefunkcja 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:

 public List increasePrice(List products, int percentage) {
    if (products != null) {
        return products.Select(product => {
                double newPrice = product.getPrice().doubleValue() *
                    (100 + percentage)/100;
                return product.setPrice(newPrice);     
               });
    }
    else return null;
}

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.

Doktor Brown
źródło
7
Sposoby uczynienia tego „bardziej FP”: 1) Użyj typów Może / Opcjonalne zamiast zerowalności, aby ułatwić pisanie funkcji całkowitych zamiast funkcji częściowych i użyj funkcji pomocniczych wyższego rzędu do wyodrębnienia „if (x! = Null)” logika. 2) Użyj soczewek, aby zdefiniować rosnącą cenę za jeden produkt pod względem zastosowania procentowego wzrostu w kontekście soczewki w stosunku do ceny produktu. 3) Użyj częściowej aplikacji / kompozycji / curry, aby uniknąć jawnej lambda dla połączenia map / Select.
Jack
6
Muszę powiedzieć, że nienawidzę pomysłu na kolekcję, która mogłaby być zerowa zamiast po prostu pusta z założenia. Działają w ten sposób języki funkcjonalne z natywną obsługą krotek / kolekcji. Nawet w OOP nie znoszę powrotu, nullgdy kolekcja jest rodzajem zwrotu. / rant over
Berin Loritsch
Ale może to być metoda statyczna, jak w klasie narzędzi w językach OOP, takich jak Java lub C #. Ten kod jest krótszy, częściowo dlatego, że prosisz o przekazanie listy i nie trzymasz jej sam. Oryginalny kod zawiera również strukturę danych, a samo przeniesienie go skróciłoby oryginalny kod bez zmiany pojęć.
Mark
@ Mark: jasne i myślę, że OP już to wie. Rozumiem pytanie jako „jak wyrazić to w sposób funkcjonalny”, nieobowiązkowe w języku innym niż OOP.
Doc Brown
@Mark FP i OO nie wykluczają się nawzajem.
Pieter B,
17

W szczególności pytam, w jaki sposób ten przykład zostałby modelowany / zaprogramowany w języku FP (rzeczywisty kod, nie teoretycznie)?

W 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ę”.

(mapcar (lambda (x) (* x 2)) '(1 2 3))

Proszę bardzo. Inne języki będą podobne, z tą różnicą, że zyskasz wyraźne typy ze zwykłą funkcjonalną semantyką „dopasowywania”. Sprawdź Haskell:

incPrice :: (Num) -> [Num] -> [Num]  
incPrice _ [] = []  
incPrice percentage (x:xs) = x*percentage : incPrice percentage xs  

(Lub coś takiego, to było wieki ...)

Chcę być otwarty na argumenty autora,

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.

AnoE
źródło
4
„całkowicie jasne, że nie ma zainteresowania dyskusją, jest na krucjacie”. Mam głos za tym klejnotem.
Euforyczny
Czy nie potrzebujesz ograniczenia Num do kodu Haskell? jak możesz zadzwonić (*) w inny sposób?
jk.
@jk., od dawna robiłem Haskell, aby spełnić ograniczenia OP dotyczące odpowiedzi, której szuka. ;) Jeśli ktoś chce naprawić mój kod, nie krępuj się. Ale jasne, zmienię to na Num.
AnoE
7

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 Productklasę, 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 CategoryManagerdo śledzenia wszystkich rodzajów kategorii, czy też odpowiedzialność ta należy do Categoryklasy, 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 Productklasie, Categoryklasie, DiscountRuleklasie, CategoryManagerklasie, czy potrzebujemy czegoś nowego? W ten sposób powstaje coś takiego DiscountRuleProductCategoryFactoryBuilder.

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 mapPricesw poniższym przykładzie Scala:

def mapPrices(f: Int => Int)(products: Traversable[Product]): Traversable[Product] =
  products map {x => x.copy(price = f(x.price))}

def increasePrice(percentage: Int)(price: Int): Int =
  price * (percentage + 100) / 100

mapPrices(increasePrice(25))(products)

Prawdopodobnie mógłbym dodać inne funkcje związane z cenami tutaj jak decreasePrice, applyBulkDiscountitp

Ponieważ 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 productsczł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.

Karl Bielefeldt
źródło
2

Po prostu rozdzielenie danych i funkcji, o której wspominał autor, może wyglądać tak w języku F # („język FP”).

module Product =

    type Product = {
        Price : decimal
        ... // other properties not mentioned
    }

    let increasePrice ( percentage : int ) ( product : Product ) : Product =
        let newPrice = ... // calculate

        { product with Price = newPrice }

W ten sposób możesz wykonać podwyżkę ceny na liście produktów.

let percentage = 10
let products : Product list = ...  // load?

products
|> List.map (Product.increasePrice percentage)

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 returnprzed 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ża mapna 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).

Kasey Speakman
źródło
1

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.

export class Product extends Object {
    name: string;
    price: number;
    category: string;
}

products: Product[] = [
    new Product( { name: "Tablet", "price": 20.99, category: 'Electronics' } ),
    new Product( { name: "Phone", "price": 500.00, category: 'Electronics' } ),
    new Product( { name: "Car", "price": 13500.00, category: 'Auto' } )
];

// find all electronics and double their price
let newProducts = products
    .filter( ( product: Product ) => product.category === 'Electronics' )
    .map( ( product: Product ) => {
        product.price *= 2;
        return product;
    } );

console.log( newProducts );

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.

Conor Mancone
źródło
2
To nie jest tak naprawdę przykład OOP, w klasycznym sensie. W prawdziwym OOP dane byłyby łączone z zachowaniem; tutaj rozdzieliłeś oba. Niekoniecznie jest to zła rzecz (uważam, że jest czystsza), ale nie jest to coś, co nazwałbym klasycznym OOP.
Robert Harvey,
0

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:

class SimpleProductManager extends ProductManager {
    ...
}

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ć:

public void increasePrice(int number) {
    if (products != null) {
        for (Product product : products) {
            double newPrice = product.getPrice() + number;
            product.setPrice(newPrice);
        }
    }
}

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.

Fis
źródło