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.
// 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).
quelle
Antworten:
Im FP-Stil
Product
wäre dies eine unveränderliche Klasse,product.setPrice
würde keinProduct
Objekt mutieren, sondern stattdessen ein neues Objekt zurückgeben, und dieincreasePrice
Funktion 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: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.
quelle
null
wenn eine Sammlung der Rückgabetyp ist. / schimpfen überIn "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".
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:
(Oder so ähnlich, es ist Ewigkeiten her ...)
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.
quelle
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
Product
Klasse 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
CategoryManager
Kategorie, um alle Arten von Kategorien im Auge zu behalten, oder gehört diese Verantwortung zu derCategory
Klasse, 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
Product
Klasse, dieCategory
Klasse, dieDiscountRule
Klasse, dieCategoryManager
Klasse, oder brauchen wir etwas Neues? So kommen wir zu Dingen wieDiscountRuleProductCategoryFactoryBuilder
.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
mapPrices
im folgenden Scala-Beispiel herauszufiltern :Ich könnte wahrscheinlich andere preisbezogenen Funktionen hinzufügen hier wie
decreasePrice
,applyBulkDiscount
usw.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
products
Mitglied 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.quelle
Das einfache Trennen der Daten und der Funktion, auf die der Autor anspielte, könnte in F # ("eine FP-Sprache") so aussehen.
Auf diese Weise können Sie eine Preiserhöhung für eine Produktliste durchführen.
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
return
vor 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
, wodurchmap
eine 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).
quelle
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.
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.
quelle
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:
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.
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".
quelle