2009-03-18 4 views
19

Das Open-Closed-Prinzip besagt, dass "Software-Entitäten (Klassen, Module, Funktionen usw.) zur Erweiterung geöffnet, aber zur Änderung geschlossen" sein sollten.Offen-geschlossen-Prinzip und Java-Modifikator "final"

Allerdings gibt Joshua Bloch in seinem berühmten Buch "Effective Java" den folgenden Rat: "Design und Dokument für die Vererbung, oder verbieten es" und ermutigt Programmierer den "finalen" Modifikator zu verwenden, um Unterklassen zu verbieten.

Ich denke, diese beiden Prinzipien widersprechen sich eindeutig (liege ich falsch?). Welchen Grundsatz befolgen Sie beim Schreiben Ihres Codes und warum? Lassen Sie Ihre Klassen offen, verbieten Sie Vererbung bei einigen von ihnen (welche?), Oder verwenden Sie den letzten Modifikator wann immer möglich?

Antwort

22

Offen gesagt denke ich, dass das offene/geschlossene Prinzip eher ein Anachronismus ist als nicht. Es stammt aus den 80er und 90er Jahren, als OO-Frameworks auf dem Prinzip aufgebaut wurden, dass alles von etwas anderem erben muss und dass alles unterklassierbar sein sollte.

Dies wurde am häufigsten in UI-Frameworks der Ära wie MFC und Java Swing typisiert. In Swing haben Sie eine lächerliche Vererbung, bei der (iirc) die Schaltfläche checkbox (oder andersherum) erweitert wird und eines davon nicht verwendet wird (ich denke, es ist das setDisabled() -Aufruf-Kontrollkästchen). Warum teilen sie eine Abstammung? Kein Grund außer, nun, sie hatten einige Methoden gemeinsam.

Heutzutage ist die Zusammensetzung der Vererbung vorzuziehen. Während Java die Vererbung standardmäßig zuließ, nahm .Net den (moderneren) Ansatz, es standardmäßig zu verbieten, was ich für korrekter halte (und konsequenter mit den Prinzipien von Josh Bloch).

DI/IoC haben auch für die Zusammensetzung weiter den Fall gemacht.

Josh Bloch weist auch darauf hin, dass Vererbung Einkapselung bricht und gibt einige gute Beispiele dafür. Es wurde auch gezeigt, dass das Ändern des Verhaltens von Java-Sammlungen konsistenter ist, wenn es durch Delegieren erfolgt, anstatt die Klassen zu erweitern.

Persönlich betrachte ich Vererbung in diesen Tagen als wenig mehr als ein Implementierungsdetail.

+0

Kannst du erklären, was du meinst, indem du standardmäßig "Vererbung" annimmst? –

+2

C# -Methoden sind standardmäßig versiegelt (nicht virtuell). Sie müssen sie explizit als virtuell kennzeichnen. Schade, das gleiche gilt nicht für Klassen. –

+0

http://en.wikipedia.org/wiki/Comparison_of_C_Sharp_and_Java Methoden in C# sind standardmäßig nicht virtuell. In Java sind sie standardmäßig virtuell. – cletus

6

Ich glaube nicht, dass die beiden Aussagen sich widersprechen. Ein Typ kann zur Erweiterung geöffnet sein und dennoch zur Vererbung geschlossen sein.

Eine Möglichkeit, dies zu tun, ist die Abhängigkeitsinjektion. Anstatt Instanzen eigener Hilfstypen zu erstellen, kann ein Typ diese bei der Erstellung angeben. Dies ermöglicht es Ihnen, die Teile des Typs zu ändern (d. H. Offen für die Erweiterung), ohne den Typ selbst zu ändern (d. H. Für die Modifikation zu schließen).

+1

Ich stimme zu. Ich glaube, dass hier jemand versucht, mit seinem linken Gehirn zu rechnen. Linkes Gehirn versteht Computer nicht - obwohl es beim Debuggen nützlich sein kann. – paulmurray

+0

Wenn Sie abstimmen, hinterlassen Sie bitte einen Kommentar. Vielen Dank. –

+0

Diese Antwort bezieht sich auf eine andere Art von "Erweiterung" als die, über die OCP spricht. Im letzteren Fall handelt es sich um eine Erweiterung durch Unterklassen * ausschließlich *. Also kann eine Klasse natürlich nicht offen für Erweiterungen sein (im Sinne von OCP) und gleichzeitig zur Vererbung geschlossen werden. Wenn Sie jemals die Möglichkeit haben, schauen Sie sich an, wie Meyer (OCPs Originalautor) im Buch darüber spricht ("Object-oriented software construction"): Es geht um Vererbung. –

7

In (offen für Erweiterung, geschlossen für Änderung) können Sie noch den letzten Modifikator verwenden. Hier ein Beispiel:

public final class ClosedClass { 

    private IMyExtension myExtension; 

    public ClosedClass(IMyExtension myExtension) 
    { 
     this.myExtension = myExtension; 
    } 

    // methods that use the IMyExtension object 

} 

public interface IMyExtension { 
    public void doStuff(); 
} 

Die ClosedClass wird für die Modifikation innerhalb der Klasse geschlossen, sondern offen für die Verlängerung durch einen anderen. In diesem Fall kann es alles sein, was die IMyExtension Schnittstelle implementiert. Dieser Trick ist eine Variation der Abhängigkeitsinjektion, da wir die geschlossene Klasse durch einen anderen, in diesem Fall den Konstruktor, speisen. Da die Erweiterung ein interface ist, kann es nicht final sein, aber seine implementierende Klasse kann sein.

Die Verwendung von final für Klassen, um sie in Java zu schließen, ähnelt der Verwendung von sealed in C#. Es gibt similar discussions darüber auf der .NET-Seite.

+0

guter Punkt und Beispiel –

+0

Dieses Beispiel sieht eher wie DIP als OCP aus. Das Problem hier ist, dass OCP, nach seiner ursprünglichen Definition, scheint keine anderen Formen der Erweiterung zuzulassen; Es erfordert die Verwendung der Implementierungsvererbung. Ich weiß nicht, vielleicht könnten wir einen OCP 2.0 8 haben. 8 ^} –

3

Ich respektiere Joshua Bloch sehr viel, und ich halte Effective Java ziemlich das Java Bibel sein. Aber ich denke, dass der automatische Zugriff auf private oft ein Fehler ist. Ich neige dazu, die Dinge protected standardmäßig so zu machen, dass sie mindestens durch Erweiterung der Klasse zugegriffen werden können. Dies ergab sich meist aus der Notwendigkeit, Testkomponenten zu vereinigen, aber ich finde es auch praktisch, das Standardverhalten von Klassen zu überschreiben. Ich finde es sehr ärgerlich, wenn ich in der Codebasis meiner eigenen Firma arbeite und am Ende & die Quelle ändern muss, weil der Autor alles "versteckt" hat. Wenn es überhaupt in meiner Macht ist, lobby ich, um den Zugang zu protected geändert zu haben, um die Verdoppelung zu vermeiden, die IMHO viel schlechter ist.

Denken Sie auch daran, dass Blochs Hintergrund sehrÖffentlichkeit ist Fundament API-Bibliotheken in der Gestaltung; die Leiste, um solchen Code "richtig" zu bekommen, muss sehr hoch gesetzt werden, also ist es wahrscheinlich nicht die gleiche Situation wie der meiste Code, den Sie schreiben werden. Wichtige Bibliotheken wie die JRE selbst neigen dazu, restriktiver zu sein, um sicherzustellen, dass die Sprache nicht missbraucht wird. Sehen Sie alle veralteten APIs in der JRE? Es ist fast unmöglich, sie zu ändern oder zu entfernen. Ihre Codebasis ist wahrscheinlich nicht in Stein gemeißelt, so dass Sie tun die Möglichkeit haben, Dinge zu beheben, wenn sich herausstellt, dass Sie zunächst einen Fehler gemacht haben.

+0

Eigentlich, wenn Sie privaten Zugriff auf Attribute haben, sollten sie in der Regel durch Methoden oder zugänglich durch Getter oder Setter in der Superklasse veränderbar sein. Sie sollten den tatsächlichen Zustand einer Klasse nicht wirklich testen, stattdessen ist es sinnvoller, das Verhalten der Klasse zu testen. – Spoike

+0

-1 tschüss einkapselung – eljenso

+1

@ spoike- was macht dich denken, dass ich über Felder sprach?Ich spreche (meistens) davon, geschützten Zugriff für Methoden zu verwenden und Klassen nicht "final" zu deklarieren. –

4

Heutzutage verwende ich den letzten Modifikator standardmäßig fast reflexiv als Teil des Boilerplate. Es macht die Dinge leichter zu verstehen, wenn Sie wissen, dass eine bestimmte Methode immer so funktioniert, wie Sie sie gerade im Code sehen.

Natürlich gibt es manchmal Situationen, in denen eine Klassenhierarchie genau das ist, was Sie wollen, und es wäre albern, sie nicht zu verwenden. Aber fürchten Sie sich vor Hierarchien von mehr als zwei Ebenen oder solchen, bei denen nicht abstrakte Klassen weiter unterklassifiziert werden. Eine Klasse sollte entweder abstrakt oder endgültig sein.

Die meiste Zeit, mit der Zusammensetzung ist der Weg zu gehen. Setze alle üblichen Maschinen in eine Klasse, lege die verschiedenen Fälle in verschiedene Klassen, dann setze Instanzen zusammen, damit sie ganz funktionieren.

Sie können diese "Abhängigkeitsinjektion" oder "Strategie-Muster" oder "Besucher-Muster" oder was auch immer nennen, aber was es kocht, ist die Verwendung von Komposition statt Vererbung, um Wiederholungen zu vermeiden.

3

Die beiden Aussagen

Software Entitäten (Klassen, Module, Funktionen, usw.) sollten für Erweiterung offen sein, aber für die Änderung geschlossen.

und

Entwurf und Dokument für die Vererbung oder aber es verbieten.

sind nicht in direktem Widerspruch zueinander. Sie können dem Prinzip "offen-geschlossen" folgen, solange Sie es entwerfen und dokumentieren (wie von Bloch empfohlen).

Ich glaube nicht, dass Bloch besagt, dass Sie sollten bevorzugen Erbe zu verbieten, indem die endgültige Modifier, nur, dass Sie explizit in jedem Klassenvererbung zu erlauben oder zu verbieten wählen sollten Sie erstellen.Sein Rat ist, dass Sie darüber nachdenken und für sich selbst entscheiden sollten, anstatt nur das Standardverhalten des Compilers zu akzeptieren.

+1

Wahr. Leider machen fast alle Java-Entwickler standardmäßig alle Klassen, die sie nicht-final erstellen; und sie werden diese Klassen sicher nicht für die Vererbung entwerfen oder dokumentieren ... Also, mein Rat ist: Wenn Sie Zweifel haben, machen Sie es endgültig. –

1

Ich glaube nicht, dass das Open/Closed-Prinzip, wie es ursprünglich vorgestellt wurde, die Interpretation ermöglicht, dass finale Klassen durch Injektion von Abhängigkeiten erweitert werden können.

In meinem Verständnis geht es beim Prinzip darum, keine direkten Änderungen an Code zuzulassen, der in Produktion gebracht wurde, und die Möglichkeit, Implementierungsvererbung zu verwenden, während man noch Funktionsmodifikationen zulässt.

Wie in der ersten Antwort darauf hingewiesen, hat dies historische Wurzeln. Vor Jahrzehnten gab es eine Vererbung, Entwickler-Tests gab es nicht, und die Neukompilierung der Codebasis dauerte oft zu lange. Beachten Sie auch, dass in C++ die Implementierungsdetails einer Klasse (insbesondere private Felder) häufig in der Header-Datei ".h" offen gelegt wurden. Wenn ein Programmierer dies ändern müsste, müssten alle Clients neu kompiliert werden. Beachten Sie, dass dies bei modernen Sprachen wie Java oder C# nicht der Fall ist. Außerdem glaube ich nicht, dass Entwickler damals auf ausgefeilte IDEs zurückgreifen konnten, die in der Lage waren, eine Abhängigkeitsanalyse direkt durchzuführen, wodurch die Notwendigkeit häufiger vollständiger Neuerstellungen vermieden wurde.

In meiner eigenen Erfahrung, bevorzuge ich das genaue Gegenteil: "Klassen sollten für Erweiterung (final) standardmäßig geschlossen werden, aber offen für die Änderung". Denken Sie darüber nach: Heute bevorzugen wir Praktiken wie Versionskontrolle (macht es einfach, frühere Versionen einer Klasse wiederherzustellen/zu vergleichen), refactoring (was uns ermutigt, Code zu ändern, um Design zu verbessern, oder als Auftakt zur Einführung neuer Funktionen) und Entwicklertest, der ein Sicherheitsnetz bietet, wenn der vorhandene Code geändert wird.