Warum sollte der Unterricht nicht so gestaltet werden, dass er „offen“ ist?

44

Beim Lesen verschiedener Stapelüberlauffragen und des Codes anderer wird der allgemeine Konsens zum Entwerfen von Klassen geschlossen. Dies bedeutet, dass in Java und C # standardmäßig alles privat ist, Felder final sind, einige Methoden final sind und manchmal Klassen sogar final sind .

Die Idee dahinter ist, Implementierungsdetails zu verbergen, was ein sehr guter Grund ist. Bei den protectedmeisten OOP-Sprachen und Polymorphismen funktioniert dies jedoch nicht.

Jedes Mal, wenn ich einer Klasse Funktionen hinzufügen oder ändern möchte, werde ich normalerweise durch private und endgültige Platzierungen behindert. Hier sind Implementierungsdetails wichtig: Sie übernehmen die Implementierung und erweitern sie, wobei Sie die Konsequenzen genau kennen. Da ich jedoch keinen Zugriff auf private und endgültige Felder und Methoden bekomme, habe ich drei Möglichkeiten:

  • Erweitern Sie die Klasse nicht, sondern umgehen Sie das Problem, das zu komplexerem Code führt
  • Kopieren Sie die gesamte Klasse und fügen Sie sie ein, um die Wiederverwendbarkeit des Codes zu beenden
  • Fork das Projekt

Das sind keine guten Optionen. Warum wird es nicht protectedin Projekten verwendet, die in Sprachen geschrieben sind, die es unterstützen? Warum verbieten einige Projekte explizit das Erben von Klassen?

TheLQ
quelle
1
Ich stimme zu, ich hatte dieses Problem mit Java Swing-Komponenten, es ist wirklich schlimm.
Jonas
20
Obligatorisch: steve-yegge.blogspot.com/2010/07/…
Shog9
3
Wenn Sie dieses Problem haben, ist die Wahrscheinlichkeit groß, dass die Klassen von Anfang an schlecht entworfen wurden - oder Sie versuchen, sie falsch zu verwenden. Sie bitten eine Klasse nicht um Informationen, Sie bitten sie, etwas für Sie zu tun - daher sollten Sie ihre Daten im Allgemeinen nicht benötigen. Dies ist zwar nicht immer der Fall, aber wenn Sie feststellen, dass Sie auf die Daten einer Klasse zugreifen müssen, ist mit großer Wahrscheinlichkeit ein Fehler aufgetreten.
Bill K
2
"Die meisten OOP-Sprachen?" Ich weiß viel mehr, wo Klassen nicht geschlossen werden können. Sie haben die vierte Option weggelassen: Sprachen ändern.
Kevin Cline
@Jonas Eines der Hauptprobleme von Swing ist, dass es viel zu viel Implementierung aufdeckt. Das tötet wirklich die Entwicklung.
Tom Hawtin - Tackline

Antworten:

55

Das Entwerfen von Klassen, die im erweiterten Zustand ordnungsgemäß funktionieren, insbesondere wenn der Programmierer, der das Erweitern ausführt, nicht vollständig versteht, wie die Klasse funktionieren soll, ist mit erheblichem zusätzlichen Aufwand verbunden. Sie können nicht einfach alles, was privat ist, öffentlich (oder geschützt) machen und das als "offen" bezeichnen. Wenn Sie zulassen, dass eine andere Person den Wert einer Variablen ändert, müssen Sie berücksichtigen, wie sich alle möglichen Werte auf die Klasse auswirken. (Was ist, wenn sie die Variable auf null setzen? Ein leeres Array? Eine negative Zahl?) Dasselbe gilt, wenn andere Personen eine Methode aufrufen dürfen. Es bedarf sorgfältiger Überlegungen.

Es ist also nicht so sehr wichtig, dass Klassen nicht geöffnet sind, aber manchmal ist es die Mühe nicht wert, sie zu öffnen.

Natürlich ist es auch möglich, dass die Bibliotheksautoren nur faul waren. Kommt darauf an, von welcher Bibliothek du sprichst. :-)

Aaron
quelle
13
Ja, aus diesem Grund sind Klassen standardmäßig versiegelt. Da Sie nicht vorhersagen können, auf welche Weise Ihre Clients die Klasse erweitern können, ist es sicherer, sie zu versiegeln, als sich dazu zu verpflichten, die möglicherweise unbegrenzte Anzahl von Erweiterungsmöglichkeiten für die Klasse zu unterstützen.
Robert Harvey
18
Sie müssen nicht vorhersagen, wie Ihre Klasse überschrieben wird. Sie müssen lediglich davon ausgehen, dass die Mitarbeiter, die Ihre Klasse erweitern, wissen, was sie tun. Wenn sie die Klasse erweitern und etwas auf null setzen, wenn es nicht null sein sollte, dann ist es ihre Schuld, nicht deine. Der einzige Ort, an dem dieses Argument Sinn macht, sind superkritische Anwendungen, bei denen etwas schrecklich schief geht, wenn ein Feld null ist.
TheLQ
5
@TheLQ: Das ist eine ziemlich große Annahme.
Robert Harvey
9
@TheLQ Wessen Fehler es ist, ist eine sehr subjektive Frage. Wenn sie eine Variable auf einen scheinbar vernünftigen Wert setzen und Sie nicht dokumentieren, dass dies nicht zulässig ist, und Ihr Code keine ArgumentException (oder eine gleichwertige Ausnahme) auslöst, wird Ihr Server jedoch offline geschaltet oder führt zu einem Sicherheits-Exploit, dann würde ich sagen, es ist genauso deine Schuld wie ihre. Und wenn Sie die gültigen Werte dokumentiert und eine Argumentationsprüfung durchgeführt haben, dann haben Sie genau die Art von Anstrengung angegeben, von der ich spreche, und Sie müssen sich fragen, ob diese Anstrengung Ihre Zeit wirklich gut genutzt hat.
Aaron
24

Standardmäßig alles privat zu machen, klingt hart, aber sehen Sie es sich von der anderen Seite an: Wenn alles standardmäßig privat ist, sollte es eine bewusste Entscheidung sein, etwas öffentlich zu machen (oder zu schützen, was fast dasselbe ist); Es ist der Vertrag des Klassenautors mit Ihnen, dem Verbraucher, über die Verwendung der Klasse. Dies ist für Sie beide ein Vorteil: Der Autor kann das Innenleben der Klasse ändern, solange die Benutzeroberfläche unverändert bleibt. und Sie wissen genau, auf welche Teile der Klasse Sie sich verlassen können und welche Änderungen unterliegen.

Die zugrunde liegende Idee ist "lose Kopplung" (auch als "schmale Schnittstellen" bezeichnet); Sein Wert liegt darin, die Komplexität gering zu halten. Durch Verringern der Anzahl der Arten, in denen Komponenten interagieren können, wird auch das Ausmaß der gegenseitigen Abhängigkeit zwischen ihnen verringert. und gegenseitige Abhängigkeit ist eine der schlimmsten Arten von Komplexität, wenn es um Wartung und Änderungsmanagement geht.

In gut gestalteten Bibliotheken haben Klassen, die es wert sind, durch Vererbung erweitert zu werden, geschützte und öffentliche Mitglieder an genau den richtigen Stellen und verbergen alles andere.

tdammers
quelle
Das ergibt einen Sinn. Es gibt jedoch immer noch das Problem, wenn Sie etwas sehr Ähnliches benötigen, aber 1 oder 2 Dinge in der Implementierung geändert wurden (innere Funktionsweise der Klasse). Ich bemühe mich auch zu verstehen, dass Sie, wenn Sie die Klasse überschreiben, nicht bereits stark von ihrer Implementierung abhängen?
TheLQ
5
+1 für die Vorteile der losen Kupplung. @TheLQ, es ist die Schnittstelle, von der Sie stark abhängig sein sollten, nicht die Implementierung.
Karl Bielefeldt
19

Alles, was nicht privat ist, soll in jeder zukünftigen Version der Klasse mehr oder weniger mit unverändertem Verhalten existieren . Tatsächlich kann es als Teil der API betrachtet werden, dokumentiert oder nicht. Aus diesem Grund kann es später zu Kompatibilitätsproblemen kommen, wenn zu viele Details angezeigt werden.

In Bezug auf "final" resp. "versiegelte" Klassen, ein typischer Fall sind unveränderliche Klassen. Viele Teile des Frameworks hängen davon ab, dass Zeichenfolgen unveränderlich sind. Wenn die String-Klasse nicht final wäre, wäre es einfach (sogar verlockend), eine veränderbare String-Unterklasse zu erstellen, die in vielen anderen Klassen alle Arten von Fehlern verursacht, wenn sie anstelle der unveränderlichen String-Klasse verwendet wird.

user281377
quelle
4
Das ist die wahre Antwort. Andere Antworten berühren wichtige Dinge, aber das wirklich Relevante ist: Alles, was nicht finalund / oder privateverriegelt ist und niemals geändert werden kann .
Konrad Rudolph
1
@Konrad: Bis die Entwickler beschließen, alles neu zu gestalten und nicht abwärtskompatibel zu sein.
JAB
Das ist wahr, aber es lohnt sich anzumerken, dass dies eine weitaus schwächere Anforderung ist als für publicMitglieder; protectedMitglieder schließen einen Vertrag nur zwischen der Klasse, die sie ausstellt, und ihren unmittelbar abgeleiteten Klassen. im gegensatz dazu publicmitglieder für verträge mit allen verbrauchern im namen aller möglichen gegenwärtigen und zukünftigen vererbten klassen.
Supercat
14

In OO gibt es zwei Möglichkeiten, dem vorhandenen Code Funktionalität hinzuzufügen.

Der erste ist durch Vererbung: Sie nehmen eine Klasse und leiten daraus ab. Vererbung sollte jedoch mit Vorsicht verwendet werden. Sie sollten die öffentliche Vererbung hauptsächlich verwenden, wenn Sie eine isA- Beziehung zwischen der Basis und der abgeleiteten Klasse haben (z. B. ist ein Rechteck eine Form). Vermeiden Sie stattdessen die öffentliche Vererbung, um eine vorhandene Implementierung wiederzuverwenden. Grundsätzlich wird die öffentliche Vererbung verwendet, um sicherzustellen, dass die abgeleitete Klasse dieselbe Schnittstelle wie die Basisklasse (oder eine größere) hat, sodass Sie das Substitutionsprinzip von Liskov anwenden können .

Die andere Methode zum Hinzufügen von Funktionen ist die Verwendung der Delegierung oder Komposition. Sie schreiben Ihre eigene Klasse, die die vorhandene Klasse als internes Objekt verwendet, und delegieren einen Teil der Implementierungsarbeit an sie.

Ich bin mir nicht sicher, welche Idee hinter all den Finalen in der Bibliothek steckt, die Sie verwenden möchten. Wahrscheinlich wollten die Programmierer sicherstellen, dass Sie keine neue Klasse von ihren erben, da dies nicht der beste Weg ist, ihren Code zu erweitern.

knüpfen
quelle
5
Toller Punkt bei Komposition vs. Vererbung.
Zsolt Török
+1, aber mir hat das Beispiel "Ist eine Form" für die Vererbung nie gefallen. Ein Rechteck und ein Kreis können zwei sehr unterschiedliche Dinge sein. Ich benutze gerne mehr, ein Quadrat ist ein Rechteck, das beschränkt ist. Rechtecke können wie Formen verwendet werden.
tylermac
@ TylerMac: in der Tat. Das Quadrat / Rechteck-Gehäuse ist tatsächlich eines der Gegenbeispiele des LSP. Wahrscheinlich sollte ich über ein wirkungsvolleres Beispiel nachdenken.
Knulp
3
Quadrat / Rechteck ist nur dann ein Gegenbeispiel, wenn Sie Änderungen zulassen.
Starblue
11

Die meisten Antworten haben Recht: Klassen sollten versiegelt werden (final, NotOverridable usw.), wenn das Objekt nicht entworfen oder erweitert werden soll.

Ich würde jedoch nicht in Betracht ziehen, einfach alles richtige Code-Design zu schließen. Das "O" in SOLID steht für "Open-Closed-Prinzip" und besagt, dass eine Klasse für Änderungen "geschlossen", für Erweiterungen jedoch "offen" sein soll. Die Idee ist, dass Sie Code in einem Objekt haben. Es funktioniert gut. Das Hinzufügen von Funktionen sollte nicht das Öffnen dieses Codes und das Vornehmen von chirurgischen Änderungen erfordern, die das bisherige Arbeitsverhalten beeinträchtigen könnten. Stattdessen sollte man Vererbung oder Abhängigkeitsinjektion verwenden können, um die Klasse zu "erweitern", um zusätzliche Dinge zu tun.

Beispielsweise kann eine Klasse "ConsoleWriter" Text abrufen und an die Konsole ausgeben. Das macht es gut. In bestimmten Fällen muss jedoch AUCH die gleiche Ausgabe in eine Datei geschrieben werden. Das Öffnen des ConsoleWriter-Codes, das Ändern der externen Schnittstelle, um den Hauptfunktionen den Parameter "WriteToFile" hinzuzufügen, und das Platzieren des zusätzlichen Datei-Schreibcodes neben dem Konsole-Schreibcode wird im Allgemeinen als eine schlechte Sache angesehen.

Stattdessen können Sie eine der beiden folgenden Aktionen ausführen: Sie können von ConsoleWriter das Format ConsoleAndFileWriter ableiten und die Write () - Methode erweitern, um zuerst die Basisimplementierung aufzurufen und dann auch in eine Datei zu schreiben. Sie können auch ein Interface IWriter aus ConsoleWriter extrahieren und dieses Interface erneut implementieren, um zwei neue Klassen zu erstellen. einen FileWriter und einen "MultiWriter", der anderen IWriter zugewiesen werden kann und jeden Aufruf seiner Methoden an alle angegebenen Writer "sendet". Welches Sie wählen, hängt davon ab, was Sie brauchen werden. Einfache Ableitung ist zwar einfacher, aber wenn Sie nicht genau wissen, ob Sie die Nachricht irgendwann an einen Netzwerkclient senden müssen, müssen Sie drei Dateien, zwei Named Pipes und die Konsole extrahieren und die Schnittstelle erstellen "Y-Adapter"; es'

Wenn die Write () - Funktion noch nie als virtuell deklariert wurde (oder versiegelt wurde), haben Sie jetzt Probleme, wenn Sie den Quellcode nicht kontrollieren. Manchmal sogar, wenn du es tust. Dies ist im Allgemeinen eine schlechte Position, und es frustriert Benutzer von Closed-Source- oder Limited-Source-APIs, wenn dies geschieht. Es gibt jedoch berechtigte Gründe, warum Write () oder die gesamte Klasse ConsoleWriter versiegelt sind. In einem geschützten Feld können sich vertrauliche Informationen befinden (die wiederum von einer Basisklasse überschrieben werden, um diese Informationen bereitzustellen). Möglicherweise ist die Validierung unzureichend. Wenn Sie eine API schreiben, müssen Sie davon ausgehen, dass die Programmierer, die Ihren Code verwenden, nicht schlauer oder gütiger sind als der durchschnittliche "Endbenutzer" des endgültigen Systems. so wirst du nie enttäuscht.

KeithS
quelle
Guter Punkt. Vererbung kann gut oder schlecht sein, daher ist es falsch zu sagen, dass jede Klasse besiegelt werden sollte. Es kommt wirklich auf das jeweilige Problem an.
Knulp
2

Ich denke, es ist wahrscheinlich von allen Leuten, die eine OO-Sprache für C-Programmierung verwenden. Ich sehe dich an, Java. Wenn Ihr Hammer wie ein Objekt geformt ist, Sie jedoch ein prozedurales System möchten und / oder Klassen nicht wirklich verstehen, muss Ihr Projekt gesperrt werden.

Grundsätzlich muss Ihr Projekt, wenn Sie in einer oo-Sprache schreiben möchten, dem oo-Paradigma folgen, das die Erweiterung umfasst. Natürlich, wenn jemand eine Bibliothek in einem anderen Paradigma geschrieben hat, sollte man sie vielleicht sowieso nicht erweitern.

Spencer Rathbun
quelle
7
BTW, OO und Procedural schließen sich nachdrücklich NICHT aus. Sie können keine OO ohne Prozeduren haben. Komposition ist ebenso ein OO-Konzept wie Erweiterung.
Michael K
@Michael natürlich nicht. Ich habe angegeben, dass Sie ein prozedurales System wünschen , das heißt eines ohne OO-Konzepte. Ich habe gesehen, dass Leute Java verwenden, um reinen prozeduralen Code zu schreiben, und es funktioniert nicht, wenn Sie versuchen, auf OO-Weise damit zu interagieren.
Spencer Rathbun
1
Das könnte gelöst werden, wenn Java erstklassige Funktionen hätte. Meh.
Michael K
Es ist schwierig, neuen Schülern Java- oder DOTNet-Sprachen beizubringen. Normalerweise unterrichte ich Procedural mit Procedural Pascal oder "Plain C" und wechsle später mit Object Pascal oder "C ++" zu OO. Und lassen Sie Java für später. Das Unterrichten der Programmierung mit einem Procedura-Programm sieht aus, als würde ein einzelnes (Singleton-) Objekt gut funktionieren.
Umlcat
@Michael K: In OOP ist eine erstklassige Funktion nur ein Objekt mit genau einer Methode.
Giorgio
2

Weil sie es nicht besser wissen.

Die ursprünglichen Autoren klammern sich wahrscheinlich an ein Missverständnis der SOLID-Prinzipien, das aus einer verwirrten und komplizierten C ++ - Welt stammt.

Ich hoffe, Sie werden feststellen, dass die Rubin-, Python- und Perlwelten nicht die Probleme haben, die die Antworten hier für den Grund der Versiegelung halten. Beachten Sie, dass es orthogonal zur dynamischen Typisierung ist. Zugriffsmodifikatoren sind in den meisten (allen?) Sprachen einfach zu bedienen. C ++ - Felder können durch Umwandeln in einen anderen Typ verworfen werden (C ++ ist schwächer). Java und C # können Reflektion verwenden. Zugriffsmodifikatoren machen die Dinge gerade so schwierig, dass Sie es nicht tun können, es sei denn, Sie möchten WIRKLICH.

Das Versiegeln von Klassen und das Markieren von Mitgliedern als privat verstößt ausdrücklich gegen das Prinzip, dass einfache Dinge einfach sein sollten und schwierige Dinge möglich sein sollten. Plötzlich sollten Dinge einfach sein, nicht wahr?

Ich möchte Sie ermutigen, den Standpunkt der ursprünglichen Autoren zu verstehen. Ein Großteil davon stammt von einer akademischen Idee der Verkapselung, die in der realen Welt niemals absoluten Erfolg gezeigt hat. Ich habe noch nie ein Framework oder eine Bibliothek gesehen, in der sich ein Entwickler irgendwo nicht gewünscht hätte, dass es ein wenig anders funktioniert, und keinen guten Grund hatte, es zu ändern. Es gibt zwei Möglichkeiten, die die ursprünglichen Softwareentwickler geplagt haben, die Mitglieder besiegelt und privat gemacht haben.

  1. Arroganz - sie glaubten wirklich, sie seien offen für Erweiterungen und geschlossen für Modifikationen
  2. Selbstzufriedenheit - sie wussten, dass es andere Anwendungsfälle geben könnte, entschieden sich jedoch, nicht für diese Anwendungsfälle zu schreiben

Ich denke, im Unternehmensrahmen wird # 2 wahrscheinlich der Fall sein. Diese C ++ -, Java- und .NET-Frameworks müssen "fertig" sein und bestimmten Richtlinien folgen. Diese Richtlinien beziehen sich normalerweise auf versiegelte Typen, es sei denn, der Typ wurde explizit als Teil einer Typhierarchie und privater Member für viele Dinge entworfen, die für andere nützlich sein könnten. Da sie sich jedoch nicht direkt auf diese Klasse beziehen, werden sie nicht verfügbar gemacht . Das Extrahieren eines neuen Typs wäre zu teuer, um ihn zu unterstützen, zu dokumentieren usw.

Die gesamte Idee hinter Zugriffsmodifikatoren ist, dass Programmierer vor sich selbst geschützt werden sollten. "C-Programmierung ist schlecht, weil Sie sich damit in den Fuß schießen können." Es ist keine Philosophie, der ich als Programmierer zustimme.

Ich bevorzuge Pythons Name Mangling-Ansatz. Sie können ganz einfach (viel einfacher als Nachdenken) Privates ersetzen, wenn Sie dies benötigen. Eine großartige Beschreibung dazu finden Sie hier: http://bytebaker.com/2009/03/31/python-properties-vs-java-access-modifiers/

Rubys privater Modifizierer ist eigentlich eher wie in C # geschützt und hat keinen privaten als C # Modifizierer. Geschützt ist ein bisschen anders. Hier gibt es eine gute Erklärung: http://www.ruby-lang.org/en/documentation/ruby-from-other-languages/

Denken Sie daran, dass Ihre statische Sprache nicht den veralteten Stilen des in dieser Sprache verfassten Codes entsprechen muss.

jrwren
quelle
5
"Einkapselung hat nie absoluten Erfolg gezeigt". Ich hätte gedacht, dass jeder zustimmen würde, dass ein Mangel an Kapselung eine Menge Ärger verursacht hat.
Joh
5
-1. Dummköpfe eilen herein, wo Engel Angst haben, aufzutreten. Das Feiern der Fähigkeit, private Variablen zu ändern, zeigt einen gravierenden Fehler in Ihrem Codierungsstil.
Riwalk
2
Abgesehen davon, dass dies umstritten und wahrscheinlich falsch ist, ist dieses Posting auch ziemlich arrogant und beleidigend. Schön gemacht.
Konrad Rudolph
1
Es gibt zwei Denkschulen, deren Befürworter sich nicht gut zu verstehen scheinen. Die Leute, die statisch tippen, möchten, dass es einfach ist, über die Software nachzudenken, die Leute, die dynamisch tippen, möchten, dass es einfach ist, Funktionen hinzuzufügen. Welches besser ist, hängt grundsätzlich von der erwarteten Laufzeit des Projekts ab.
Joh
1

EDIT: Ich glaube, Klassen sollten so gestaltet sein, dass sie offen sind. Mit einigen Einschränkungen, sollte aber nicht für die Vererbung geschlossen werden.

Warum: "Final Classes" oder "Sealed Classes" erscheinen mir komisch. Ich muss nie eine meiner eigenen Klassen als "final" ("versiegelt") markieren, weil ich diese Klassen möglicherweise später vererben muss.

Ich habe Bibliotheken von Drittanbietern mit Klassen gekauft / heruntergeladen (die meisten visuellen Steuerelemente), und wirklich dankbar, dass keine dieser Bibliotheken "final" verwendet hat, auch wenn dies von der Programmiersprache unterstützt wird, in der sie codiert wurden, da ich diese Klassen nicht mehr erweitert habe. auch wenn ich bibliotheken ohne den quellcode kompiliert habe.

Manchmal muss ich mich mit einigen gelegentlichen "versiegelten" Klassen auseinandersetzen und habe damit aufgehört, einen neuen "Wrapper" für die Klasse zu erstellen, der ähnliche Mitglieder hat.

class MyBaseWrapper {
  protected FinalClass _FinalObject;

  public virtual DoSomething()
  {
    // these method cannot be overriden,
    // its from a "final" class
    // the wrapper method can  be open and virtual:
    FinalObject.DoSomething();
  }
} // class MyBaseWrapper


class MyBaseWrapper: MyBaseWrapper {

  public override DoSomething()
  {
    // the wrapper method allows "overriding",
    // a method from a "final" class:
    DoSomethingNewBefore();
    FinalObject.DoSomething();
    DoSomethingNewAfter();
  }
} // class MyBaseWrapper

Scope-Clasifier ermöglichen das "Versiegeln" einiger Features, ohne die Vererbung einzuschränken.

Als ich von Structured Progr. Bei OOP habe ich angefangen, private Klassenmitglieder zu verwenden, aber nach vielen Projekten habe ich die Verwendung von protected beendet und diese Eigenschaften oder Methoden gegebenenfalls öffentlich gemacht .

PS Ich mag "final" nicht als Schlüsselwort in C #, sondern verwende "sealed" wie Java oder "sterile" nach der Vererbungsmetapher. Außerdem ist "final" ein mehrdeutiges Wort, das in verschiedenen Zusammenhängen verwendet wird.

umlcat
quelle
-1: Sie gaben an, dass Sie offene Designs mögen, dies beantwortet jedoch nicht die Frage, weshalb Klassen nicht so gestaltet werden sollten, dass sie offen sind.
Joh
@Joh Sorry, ich habe mich nicht gut erklärt, ich habe versucht die gegenteilige Meinung auszudrücken, da sollte nicht gesiegelt werden. Vielen Dank für die Beschreibung, warum Sie nicht einverstanden sind.
Umlcat