Wie würde dies in Nicht-OO programmiert werden? [geschlossen]

11

Beim Lesen eines vernichtenden Artikels über die Nachteile von OOP zugunsten eines anderen Paradigmas bin ich auf ein Beispiel gestoßen, an dem ich nicht allzu viel bemängeln kann.

Ich möchte offen für die Argumente des Autors sein, und obwohl ich ihre Punkte theoretisch verstehen kann, fällt es mir insbesondere schwer, mir vorzustellen, wie es beispielsweise in einer FP-Sprache besser implementiert werden könnte.

Von: 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.

Beachten Sie, dass ich nicht nach einer Widerlegung für oder gegen die Argumente des Autors für "Gibt es einen rationalen Grund, warum diese drei Verhaltensweisen mit der Datenhierarchie verknüpft werden sollten?" Suche.

Ich frage speziell, wie dieses Beispiel in einer FP-Sprache modelliert / programmiert werden soll (tatsächlicher Code, nicht theoretisch).

Danny Yaroslavski
quelle
44
Sie können vernünftigerweise nicht erwarten, Programmierparadigmen mit solch kurzen und erfundenen Beispielen zu vergleichen. Jeder hier kann sich Code-Anforderungen ausdenken, die sein bevorzugtes Paradigma besser aussehen lassen als Ruhe, insbesondere wenn sie andere nicht ordnungsgemäß implementieren. Nur wenn Sie ein echtes, großes, sich veränderndes Projekt haben, können Sie Einblicke in Stärken und Schwächen verschiedener Paradigmen gewinnen.
Euphorisch
20
Es gibt nichts an der OO-Programmierung, was vorschreibt, dass diese drei Methoden in derselben Klasse zusammenpassen. Ebenso gibt es nichts an der OO-Programmierung, was vorschreibt, dass Verhalten in derselben Klasse wie Daten vorhanden sein sollte. Das heißt, mit OO-Programmierung können Sie Daten in dieselbe Klasse wie das Verhalten einordnen oder sie auf eine separate Entität / ein separates Modell aufteilen. In beiden Fällen hat OO nichts zu sagen, wie sich Daten auf ein Objekt beziehen sollen, da sich das Konzept eines Objekts im Wesentlichen mit dem Modellierungsverhalten befasst , indem logisch verwandte Methoden in einer Klasse gruppiert werden.
Ben Cottrell
20
Ich habe 10 Sätze in diesen Artikel aufgenommen und aufgegeben. Achten Sie nicht auf den Mann hinter diesem Vorhang. In anderen Nachrichten hatte ich keine Ahnung, dass True Scotsmen hauptsächlich OOP-Programmierer waren.
Robert Harvey
11
Noch ein Schimpfen von jemandem, der Verfahrenscode in einer OO-Sprache schreibt, fragt sich dann, warum OO nicht für ihn arbeitet.
TheCatWhisperer
11
Obwohl es zweifellos wahr ist, dass OOP von Anfang bis Ende eine Katastrophe von Designfehlern ist - und ich bin stolz, ein Teil davon zu sein! - Dieser Artikel ist nicht lesbar, und das Beispiel, das Sie geben, argumentiert im Grunde, dass eine schlecht gestaltete Klassenhierarchie schlecht gestaltet ist.
Eric Lippert

Antworten:

42

Im FP-Stil Productwäre dies eine unveränderliche Klasse, product.setPricewürde kein ProductObjekt mutieren, sondern stattdessen ein neues Objekt zurückgeben, und die increasePriceFunktion wäre eine "eigenständige" Funktion. Bei Verwendung einer ähnlich aussehenden Syntax wie Ihrer (C # / Java-ähnlich) könnte eine äquivalente Funktion folgendermaßen aussehen:

 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;
}

Wie Sie sehen, unterscheidet sich der Kern hier nicht wirklich, außer dass der "Boilerplate" -Code aus dem erfundenen OOP-Beispiel weggelassen wird. Ich sehe dies jedoch nicht als Beweis dafür, dass OOP zu aufgeblähtem Code führt, sondern nur als Beweis dafür, dass man alles beweisen kann, wenn man ein Codebeispiel konstruiert, das ausreichend künstlich genug ist.

Doc Brown
quelle
7
Möglichkeiten, um dieses "mehr FP" zu machen: 1) Verwenden Sie Vielleicht / Optionale Typen anstelle von Nullfähigkeit, um das Schreiben von Gesamtfunktionen anstelle von Teilfunktionen zu vereinfachen, und verwenden Sie Hilfsfunktionen höherer Ordnung, um "if (x! = Null)" zu abstrahieren. Logik. 2) Verwenden Sie Linsen, um einen steigenden Preis für ein einzelnes Produkt zu definieren, indem Sie eine prozentuale Erhöhung im Zusammenhang mit einer Linse auf den Produktpreis anwenden. 3) Verwenden Sie eine teilweise Anwendung / Komposition / Currying, um ein explizites Lambda für die Karte / den Select-Aufruf zu vermeiden.
Jack
6
Ich muss sagen, ich hasse die Idee, dass eine Sammlung null sein könnte, anstatt einfach leer zu sein. Funktionssprachen mit nativer Tupel- / Sammlungsunterstützung funktionieren auf diese Weise. Selbst in OOP hasse ich es, zurückzukehren, nullwenn eine Sammlung der Rückgabetyp ist. / schimpfen über
Berin Loritsch
Dies kann jedoch eine statische Methode wie in einer Utility-Klasse in OOP-Sprachen wie Java oder C # sein. Dieser Code ist teilweise kürzer, weil Sie darum bitten, die Liste zu übergeben und sie nicht selbst zu halten. Der ursprüngliche Code enthält auch eine Datenstruktur, und durch einfaches Verschieben wird der ursprüngliche Code ohne Änderung der Konzepte kürzer.
Markieren Sie
@ Mark: sicher, und ich denke, das OP weiß das schon. Ich verstehe die Frage als "wie man dies auf funktionale Weise ausdrückt", nicht obligatorisch in einer Nicht-OOP-Sprache.
Doc Brown
@ Mark FP und OO schließen sich nicht aus.
Pieter B
17

Ich frage speziell, wie dieses Beispiel in einer FP-Sprache modelliert / programmiert werden soll (tatsächlicher Code, nicht theoretisch).

In "einer" FP-Sprache? Wenn etwas ausreicht, dann wähle ich Emacs Lisp. Es hat das Konzept von Typen (Art, Art von), aber nur eingebaute. Ihr Beispiel reduziert sich also auf "Wie multiplizieren Sie jedes Element in einer Liste mit etwas und geben eine neue Liste zurück".

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

Los geht's. Andere Sprachen sind ähnlich, mit dem Unterschied, dass Sie den Vorteil expliziter Typen mit der üblichen funktionalen "Matching" -Semantik erhalten. Schauen Sie sich Haskell an:

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

(Oder so ähnlich, es ist Ewigkeiten her ...)

Ich möchte offen sein für die Argumente des Autors,

Warum? Ich habe versucht, den Artikel zu lesen. Ich musste nach einer Seite aufgeben und scannte nur schnell den Rest.

Das Problem des Artikels ist nicht, dass es gegen OOP ist. Ich bin auch nicht blind "pro OOP". Ich habe mit logischen, funktionalen und OOP-Paradigmen programmiert, wenn möglich ziemlich oft in derselben Sprache und häufig ohne eines der drei, rein imperativ oder sogar auf Assembler-Ebene. Ich würde niemals sagen, dass eines dieser Paradigmen dem anderen in jeder Hinsicht weit überlegen ist. Würde ich argumentieren, dass ich Sprache X besser mag als Y? Natürlich würde ich! Aber darum geht es in diesem Artikel nicht.

Das Problem des Artikels ist, dass er vom ersten bis zum letzten Satz eine Fülle von rhetorischen Werkzeugen (Irrtümern) verwendet. Es ist völlig sinnlos, überhaupt damit zu beginnen, alle darin enthaltenen Fehler zu beschreiben. Der Autor macht deutlich, dass er kein Interesse an Diskussionen hat, er befindet sich auf einem Kreuzzug. Wieso sich die Mühe machen?

Letztendlich sind all diese Dinge nur Werkzeuge, um einen Job zu erledigen. Es kann Jobs geben, bei denen OOP besser ist, und es kann andere Jobs geben, bei denen FP besser ist oder bei denen beide übertrieben sind. Das Wichtigste ist, das richtige Werkzeug für den Job auszuwählen und zu erledigen.

AnoE
quelle
4
"Überaus klar, dass er kein Interesse an Diskussionen hat, er ist auf einem Kreuzzug." Haben Sie eine positive Stimme für dieses Juwel.
Euphorisch
Benötigen Sie keine Num-Einschränkung für Ihren Haskell-Code? wie kannst du sonst (*) anrufen?
jk.
@jk., es ist Ewigkeiten her, dass ich Haskell gemacht habe, nur um die Einschränkung des OP für die Antwort zu erfüllen, nach der er sucht. ;) Wenn jemand meinen Code reparieren möchte, zögern Sie nicht. Aber sicher, ich werde es auf Num umstellen.
AnoE
7

Der Autor machte einen sehr guten Punkt und wählte dann ein glanzloses Beispiel, um zu versuchen, es zu sichern. Die Beschwerde betrifft nicht die Implementierung der Klasse, sondern die Idee, dass die Datenhierarchie untrennbar mit der Funktionshierarchie verbunden ist.

Daraus folgt, dass es zum Verständnis des Standpunkts des Autors nicht hilfreich wäre, nur zu sehen, wie er diese einzelne Klasse in einem funktionalen Stil implementieren würde. Sie müssten sehen, wie er den gesamten Kontext von Daten und Funktionen rund um diese Klasse in einem funktionalen Stil gestaltet.

Denken Sie über die möglichen Datentypen nach, die mit Produkten und Preisen verbunden sind. Um ein paar Ideen zu sammeln: Name, Upc-Code, Kategorie, Versandgewicht, Preis, Währung, Rabattcode, Rabattregel.

Dies ist der einfache Teil des objektorientierten Designs. Wir machen nur eine Klasse für alle oben genannten "Objekte" und wir sind gut, oder? Eine ProductKlasse bilden, um einige davon miteinander zu kombinieren?

Aber warten Sie, Sie können Sammlungen und Aggregate einiger dieser Typen haben: Setzen Sie [Kategorie], (Rabattcode -> Preis), (Menge -> Rabattbetrag) und so weiter. Wo passen die hin? Erstellen wir eine separate CategoryManagerKategorie, um alle Arten von Kategorien im Auge zu behalten, oder gehört diese Verantwortung zu der CategoryKlasse, die wir bereits erstellt haben?

Was ist nun mit Funktionen, die Ihnen einen Preisnachlass gewähren, wenn Sie eine bestimmte Menge von Artikeln aus zwei verschiedenen Kategorien haben? Geht das in die ProductKlasse, die CategoryKlasse, die DiscountRuleKlasse, die CategoryManagerKlasse, oder brauchen wir etwas Neues? So kommen wir zu Dingen wie DiscountRuleProductCategoryFactoryBuilder.

Im Funktionscode ist Ihre Datenhierarchie vollständig orthogonal zu Ihren Funktionen. Sie können Ihre Funktionen so sortieren, wie es semantisch sinnvoll ist. Sie können beispielsweise alle Funktionen, die die Preise von Produkten ändern, zusammenfassen. In diesem Fall ist es sinnvoll, allgemeine Funktionen wie mapPricesim folgenden Scala-Beispiel herauszufiltern :

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)

Ich könnte wahrscheinlich andere preisbezogenen Funktionen hinzufügen hier wie decreasePrice, applyBulkDiscountusw.

Da wir auch eine Sammlung von verwenden Products, muss die OOP-Version Methoden zum Verwalten dieser Sammlung enthalten. Sie wollten jedoch nicht, dass es in diesem Modul um die Produktauswahl geht, sondern um Preise. Die Funktionsdatenkopplung hat Sie gezwungen, auch das Sammelverwaltungs-Boilerplate hineinzuwerfen.

Sie können versuchen, dies zu lösen, indem Sie das productsMitglied in eine separate Klasse einordnen. Am Ende erhalten Sie jedoch sehr eng gekoppelte Klassen. OO-Programmierer halten die Kopplung von Funktionsdaten für sehr natürlich und sogar vorteilhaft, aber mit dem Verlust der Flexibilität sind hohe Kosten verbunden. Jedes Mal, wenn Sie eine Funktion erstellen, müssen Sie sie nur einer Klasse zuweisen. Wenn Sie eine Funktion verwenden möchten, müssen Sie einen Weg finden, um die gekoppelten Daten an den Verwendungsort zu bringen. Diese Einschränkungen sind enorm.

Karl Bielefeldt
quelle
2

Das einfache Trennen der Daten und der Funktion, auf die der Autor anspielte, könnte in F # ("eine FP-Sprache") so aussehen.

module Product =

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

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

        { product with Price = newPrice }

Auf diese Weise können Sie eine Preiserhöhung für eine Produktliste durchführen.

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

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

Hinweis: Wenn Sie mit FP nicht vertraut sind, gibt jede Funktion einen Wert zurück. Aus einer C-ähnlichen Sprache kommend, können Sie die letzte Anweisung in einer Funktion so behandeln, als hätte sie eine returnvor sich.

Ich habe einige Typanmerkungen eingefügt, die jedoch nicht erforderlich sein sollten. Getter / Setter sind hier nicht erforderlich, da das Modul die Daten nicht besitzt. Es besitzt die Struktur der Daten und die verfügbaren Operationen. Dies ist auch bei zu sehen List, wodurch mapeine Funktion für jedes Element in der Liste ausgeführt wird und das Ergebnis in einer neuen Liste zurückgegeben wird.

Beachten Sie, dass das Produktmodul nichts über Schleifen wissen muss, da diese Verantwortung beim Listenmodul verbleibt (das die Notwendigkeit für Schleifen geschaffen hat).

Kasey Speakman
quelle
1

Lassen Sie mich dies mit der Tatsache vorwegnehmen, dass ich kein Experte für funktionale Programmierung bin. Ich bin eher eine OOP-Person. Obwohl ich mir ziemlich sicher bin, wie Sie mit FP die gleiche Funktionalität erreichen würden, könnte ich mich irren.

Dies ist In Typescript (daher alle Typanmerkungen). Typoskript (wie Javascript) ist eine Multi-Domain-Sprache.

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 );

Im Detail (und auch hier kein FP-Experte) ist zu verstehen, dass es nicht viele vordefinierte Verhaltensweisen gibt. Es gibt keine "Preiserhöhungsmethode", die eine Preiserhöhung auf die gesamte Liste anwendet, da dies natürlich keine OOP ist: Es gibt keine Klasse, in der ein solches Verhalten definiert werden kann. Anstatt ein Objekt zu erstellen, in dem eine Liste von Produkten gespeichert ist, erstellen Sie einfach ein Array von Produkten. Sie können dann Standard-FP-Prozeduren verwenden, um dieses Array nach Ihren Wünschen zu bearbeiten: Filtern, um bestimmte Elemente auszuwählen, Zuordnen, um Interna anzupassen usw. Sie haben am Ende eine detailliertere Kontrolle über Ihre Produktliste, ohne sich durch die einschränken zu müssen API, die Ihnen der SimpleProductManager bietet. Dies kann von einigen als Vorteil angesehen werden. Es ist auch wahr, dass Sie nicht ' Sie müssen sich keine Gedanken über Gepäck machen, das mit der ProductManager-Klasse verbunden ist. Schließlich müssen Sie sich keine Sorgen um "SetProducts" oder "GetProducts" machen, da es kein Objekt gibt, das Ihre Produkte verbirgt. Stattdessen haben Sie nur die Liste der Produkte, mit denen Sie arbeiten. Dies kann wiederum ein Vorteil oder ein Nachteil sein, abhängig von den Umständen / der Person, mit der Sie sprechen. Außerdem gibt es offensichtlich keine Klassenhierarchie (worüber er sich beschwert hat), da es überhaupt keine Klassen gibt. Dies kann je nach den Umständen / der Person, mit der Sie sprechen, ein Vorteil oder ein Nachteil sein. Außerdem gibt es offensichtlich keine Klassenhierarchie (worüber er sich beschwert hat), da es überhaupt keine Klassen gibt. Dies kann je nach den Umständen / der Person, mit der Sie sprechen, ein Vorteil oder ein Nachteil sein. Außerdem gibt es offensichtlich keine Klassenhierarchie (worüber er sich beschwert hat), da es überhaupt keine Klassen gibt.

Ich habe mir nicht die Zeit genommen, sein gesamtes Geschwätz zu lesen. Ich benutze FP-Praktiken, wenn es bequem ist, aber ich bin definitiv eher ein OOP-Typ. Da ich Ihre Frage beantwortet habe, dachte ich mir, ich würde auch einige kurze Kommentare zu seinen Meinungen abgeben. Ich denke, dies ist ein sehr ausgeklügeltes Beispiel, das die "Nachteile" von OOP hervorhebt. In diesem speziellen Fall ist OOP für die gezeigte Funktionalität wahrscheinlich übertrieben, und FP wäre wahrscheinlich besser geeignet. Andererseits, wenn dies für so etwas wie einen Einkaufswagen wäre, ist der Schutz Ihrer Produktliste und die Einschränkung des Zugriffs darauf (glaube ich) ein sehr wichtiges Ziel des Programms, und FP hat keine Möglichkeit, solche Dinge durchzusetzen. Auch hier kann es sein, dass ich kein FP-Experte bin, aber nachdem ich Einkaufswagen für E-Commerce-Systeme implementiert habe, würde ich lieber OOP als FP verwenden.

Persönlich fällt es mir schwer, jemanden ernst zu nehmen, der ein so starkes Argument für "X ist einfach schrecklich. Verwenden Sie immer Y" macht. Die Programmierung verfügt über eine Vielzahl von Werkzeugen und Paradigmen, da eine Vielzahl von Problemen zu lösen ist. FP hat seinen Platz, OOP hat seinen Platz, und niemand wird ein großartiger Programmierer sein, wenn er die Nachteile und Vorteile all unserer Tools nicht versteht und wann er sie verwenden soll.

** Hinweis: In meinem Beispiel gibt es natürlich eine Klasse: die Produktklasse. In diesem Fall ist es jedoch einfach ein dummer Datencontainer: Ich glaube nicht, dass meine Verwendung gegen die Prinzipien von FP verstößt. Es ist eher ein Helfer für die Typprüfung.

** Hinweis: Ich erinnere mich nicht auf Anhieb und habe nicht überprüft, ob die Art und Weise, wie ich die Kartenfunktion verwendet habe, die Produkte an Ort und Stelle modifizieren würde, dh ich habe versehentlich den Preis der Produkte in den Originalprodukten verdoppelt Array. Das ist offensichtlich die Art von Nebeneffekt, die FP zu vermeiden versucht, und mit etwas mehr Code könnte ich sichergehen, dass dies nicht geschieht.

Conor Mancone
quelle
2
Dies ist im klassischen Sinne kein OOP-Beispiel. In echtem OOP würden Daten mit Verhalten kombiniert; Hier haben Sie die beiden getrennt. Es ist nicht unbedingt eine schlechte Sache (ich finde es tatsächlich sauberer), aber es ist nicht das, was ich als klassisches OOP bezeichnen würde.
Robert Harvey
0

Es scheint mir nicht, dass der SimpleProductManager ein Kind von etwas ist (erweitert oder erbt).

Es handelt sich lediglich um die Implementierung der ProductManager-Schnittstelle, bei der es sich im Grunde um einen Vertrag handelt, der definiert, welche Aktionen (Verhaltensweisen) das Objekt ausführen muss.

Wenn es ein Kind wäre (oder besser gesagt, eine geerbte Klasse oder eine Klasse, die eine andere Klassenfunktionalität erweitert), würde es wie folgt geschrieben:

class SimpleProductManager extends ProductManager {
    ...
}

Im Grunde sagt der Autor:

Wir haben ein Objekt, dessen Verhalten ist: setProducts, erhöhenPreis, getProducts. Und es ist uns egal, ob das Objekt auch ein anderes Verhalten hat oder wie das Verhalten implementiert wird.

Die SimpleProductManager-Klasse implementiert es. Grundsätzlich werden Aktionen ausgeführt.

Es kann auch als PercentagePriceIncreaser bezeichnet werden, da das Hauptverhalten darin besteht, den Preis um einen bestimmten Prozentwert zu erhöhen.

Wir können aber auch eine andere Klasse implementieren: ValuePriceIncreaser.

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

Aus externer Sicht hat sich nichts geändert, die Schnittstelle ist dieselbe, es gibt immer noch dieselben drei Methoden, aber das Verhalten ist unterschiedlich.

Da es in FP keine Schnittstellen gibt, ist die Implementierung schwierig. In C können wir beispielsweise Zeiger auf Funktionen halten und entsprechend unseren Anforderungen einen entsprechenden aufrufen. Am Ende funktioniert es in OOP sehr, sehr ähnlich, aber es wird vom Compiler "automatisiert".

Fis
quelle