Open-Closed-Prinzip

Hallo Habr! Hier ist eine Übersetzung eines Artikels von Robert Martin über das Open-Closed-Prinzip , den er im Januar 1996 veröffentlichte. Der Artikel ist, gelinde gesagt, nicht der neueste. Aber in RuNet werden Onkel Bobs Artikel über SOLID nur in abgeschnittener Form nacherzählt, sodass ich dachte, dass eine vollständige Übersetzung nicht überflüssig wäre.



Ich habe mich entschlossen, mit dem Buchstaben O zu beginnen, da das Prinzip der Offenheitsschließung tatsächlich von zentraler Bedeutung ist. Unter anderem gibt es viele wichtige Feinheiten, die es wert sind, beachtet zu werden:


  • Kein Programm kann zu 100% "geschlossen" werden.
  • Objektorientierte Programmierung (OOP) arbeitet nicht mit physischen Objekten der realen Welt, sondern mit Konzepten - zum Beispiel dem Konzept der "Ordnung".

Dies ist der erste Artikel in meiner Spalte " Engineer Notes" für den C ++ - Bericht . Die in dieser Spalte veröffentlichten Artikel konzentrieren sich auf die Verwendung von C ++ und OOP und gehen auf die Schwierigkeiten bei der Softwareentwicklung ein. Ich werde versuchen, die Materialien für praktizierende Ingenieure pragmatisch und nützlich zu machen. Zur Dokumentation des objektorientierten Designs in diesen Artikeln werde ich die Buch-Notation verwenden.


Mit der objektorientierten Programmierung sind viele Heuristiken verbunden. Beispiel: "Alle Mitgliedsvariablen müssen privat sein" oder "Globale Variablen sollten vermieden werden" oder "Typbestimmung zur Laufzeit ist gefährlich". Was ist der Grund für solche Heuristiken? Warum sind sie wahr? Sind sie immer wahr? In dieser Spalte wird das diesen Heuristiken zugrunde liegende Entwurfsprinzip untersucht - das Prinzip des Offenheitsverschlusses.
Ivar Jacobson sagte: „Alle Systeme ändern sich während des Lebenszyklus. Dies muss beim Entwurf eines Systems berücksichtigt werden, für das mehr als eine Version erwartet wird. “ Wie können wir ein System so gestalten, dass es angesichts von Veränderungen stabil ist und mehr als eine Version erwartet wird? Bertrand Meyer erzählte uns davon bereits 1988, als das mittlerweile berühmte Prinzip der Offenheit und Nähe formuliert wurde:


Programmentitäten (Klassen, Module, Funktionen usw.) müssen zur Erweiterung geöffnet und für Änderungen geschlossen sein.


Wenn eine Änderung im Programm eine Kaskade von Änderungen in den abhängigen Modulen zur Folge hat, zeigt das Programm unerwünschte Anzeichen eines „schlechten“ Designs an.


Das Programm wird zerbrechlich, unflexibel, unvorhersehbar und unbenutzt. Das Prinzip der Offenheit und Nähe löst diese Probleme auf sehr einfache Weise. Er sagt, dass es notwendig ist, Module zu entwerfen, die sich nie ändern . Wenn sich die Anforderungen ändern, müssen Sie das Verhalten solcher Module erweitern, indem Sie neuen Code hinzufügen, anstatt den alten, bereits funktionierenden Code zu ändern.


Beschreibung


Module, die das Prinzip der Offenheit und Nähe erfüllen, weisen zwei Hauptmerkmale auf:


  1. Zur Erweiterung öffnen. Dies bedeutet, dass das Verhalten des Moduls erweitert werden kann. Das heißt, wir können dem Modul ein neues Verhalten hinzufügen, das den sich ändernden Anforderungen für die Anwendung entspricht oder den Anforderungen neuer Anwendungen entspricht.
  2. Wegen Änderung geschlossen. Der Quellcode eines solchen Moduls ist unantastbar. Niemand hat das Recht, Änderungen daran vorzunehmen.

Es scheint, dass diese beiden Zeichen nicht zusammenpassen. Die Standardmethode zum Erweitern des Verhaltens eines Moduls besteht darin, Änderungen daran vorzunehmen. Ein Modul, das nicht geändert werden kann, wird normalerweise als Modul mit festem Verhalten betrachtet. Wie können diese beiden entgegengesetzten Bedingungen erfüllt werden?


Der Schlüssel zur Lösung ist die Abstraktion.


In C ++ ist es unter Verwendung der Prinzipien des objektorientierten Entwurfs möglich, feste Abstraktionen zu erstellen, die eine unbegrenzte Anzahl möglicher Verhaltensweisen darstellen können.


Abstraktionen sind abstrakte Basisklassen, und eine unbegrenzte Anzahl möglicher Verhaltensweisen wird durch alle möglichen Nachfolgeklassen dargestellt. Ein Modul kann die Abstraktion manipulieren. Ein solches Modul ist wegen Änderungen geschlossen, da es von einer festen Abstraktion abhängt. Das Verhalten des Moduls kann auch erweitert werden, indem neue Nachkommen der Abstraktion erstellt werden.


Das folgende Diagramm zeigt eine einfache Entwurfsoption, die dem Prinzip der Offenheit und Nähe nicht entspricht. Beide Klassen, Client und Server , sind nicht abstrakt. Es gibt keine Garantie dafür, dass Funktionen, die Mitglieder der Server sind, virtuell sind. Die Client Klasse verwendet die Server Klasse. Wenn das Client Klassenobjekt ein anderes Serverobjekt verwenden soll, müssen wir die Client Klasse so ändern, dass sie auf die neue Serverklasse verweist.


Bild
Geschlossener Client


Das folgende Diagramm zeigt die entsprechende Gestaltungsmöglichkeit, die dem Prinzip der Offenheit-Nähe entspricht. In diesem Fall ist die AbstractServer Klasse eine abstrakte Klasse, deren Elementfunktionen virtuell sind. Die Client Klasse verwendet die Abstraktion. Objekte der Client Klasse verwenden jedoch Objekte der Server Nachfolgerklasse. Wenn Objekte der Client Klasse eine andere Serverklasse verwenden sollen, führen wir einen neuen Nachkommen der AbstractServer Klasse ein. Die Client Klasse bleibt unverändert.


Bild
Client öffnen


Shape abstrakt


Stellen Sie sich eine Anwendung vor, die Kreise und Quadrate in einer Standard-GUI zeichnen soll. Kreise und Quadrate müssen in einer bestimmten Reihenfolge gezeichnet werden. In der entsprechenden Reihenfolge wird eine Liste von Kreisen und Quadraten erstellt. Das Programm sollte diese Liste in der Reihenfolge durchgehen und jeden Kreis oder jedes Quadrat zeichnen.


In C könnten wir mit prozeduralen Programmiertechniken, die nicht dem Open-Close-Prinzip entsprechen, dieses Problem lösen, wie in Listing 1 gezeigt. Hier sehen wir viele Datenstrukturen mit demselben ersten Element. Dieses Element ist ein Typcode, der die Datenstruktur als Kreis oder Quadrat identifiziert. Die DrawAllShapes Funktion durchläuft ein Array von Zeigern auf diese Datenstrukturen, erkennt den Typcode und ruft dann die entsprechende Funktion ( DrawCircle oder DrawSquare ) auf.


 // 1 //  /    enum ShapeType {circle, square} struct Shape { ShapeType itsType; }; struct Circle { ShapeType itsType; double itsRadius; Point itsCenter; }; struct Square { ShapeType itsType; double itsSide; Point itsTopLeft; }; // //     // void DrawSquare(struct Square*) void DrawCircle(struct Circle*); typedef struct Shape *ShapePointer; void DrawAllShapes(ShapePointer list[], int n) { int i; for (i=0; i<n; i++) { struct Shape* s = list[i]; switch (s->itsType) { case square: DrawSquare((struct Square*)s); break; case circle: DrawCircle((struct Circle*)s); break; } } } 

Die DrawAllShapes Funktion DrawAllShapes nicht das Prinzip des Offenheitsschlusses, da sie nicht aus neuen Formen geschlossen werden kann. Wenn ich diese Funktion um die Möglichkeit erweitern möchte, Formen aus einer Liste mit Dreiecken zu zeichnen, muss ich die Funktion ändern. Tatsächlich muss ich die Funktion für jeden neuen Formtyp ändern, den ich zeichnen muss.


Natürlich ist dieses Programm nur ein Beispiel. Im wirklichen Leben würde der switch aus der DrawAllShapes Funktion in verschiedenen Funktionen in der gesamten Anwendung immer wieder wiederholt, und jeder würde etwas anderes tun. Das Hinzufügen neuer Formen zu einer solchen Anwendung bedeutet, alle Stellen zu finden, an denen solche switch (oder if/else Ketten) verwendet werden, und jeder von ihnen eine neue Form hinzuzufügen. Darüber hinaus ist es sehr unwahrscheinlich, dass alle switch und if/else Ketten so gut strukturiert sind wie in DrawAllShapes . Es ist viel wahrscheinlicher, dass Prädikate angeben, if mit logischen Operatoren kombiniert werden oder if case von switch so kombiniert werden, dass eine bestimmte Stelle im Code „vereinfacht“ wird. Daher kann das Problem, alle Stellen zu finden und zu verstehen, an denen Sie eine neue Figur hinzufügen müssen, nicht trivial sein.


In Listing 2 werde ich Code zeigen, der eine Quadrat- / Kreislösung demonstriert, die dem Prinzip des Offenheitsschlusses entspricht. Eine abstrakte Shape wird eingeführt. Diese abstrakte Klasse enthält eine reine virtuelle Draw . Die Klassen Circle und Square sind Nachkommen der Shape Klasse.


 // 2 //  /  - class Shape { public: virtual void Draw() const = 0; }; class Square : public Shape { public: virtual void Draw() const; }; class Circle : public Shape { public: virtual void Draw() const; }; void DrawAllShapes(Set<Shape*>& list) { for (Iterator<Shape*>i(list); i; i++) (*i)->Draw(); } 

Beachten Sie, dass wir nur einen neuen Nachkommen der Shape Klasse hinzufügen müssen, wenn wir das Verhalten der DrawAllShapes Funktion in Listing 2 erweitern möchten, um eine neue Art von Form zu zeichnen. Die DrawAllShapes Funktion muss nicht DrawAllShapes werden. Daher erfüllt DrawAllShapes das Prinzip der Offenheit und Nähe. Sein Verhalten kann erweitert werden, ohne die Funktion selbst zu ändern.


In der realen Welt würde die Shape Klasse viele andere Methoden enthalten. Das Hinzufügen einer neuen Form zur Anwendung ist jedoch immer noch sehr einfach, da Sie lediglich einen neuen Erben eingeben und diese Funktionen implementieren müssen. Sie müssen nicht die gesamte Anwendung nach Orten durchsuchen, an denen Änderungen erforderlich sind.


Daher werden Programme, die dem Prinzip der Offenheit und Nähe entsprechen, durch Hinzufügen neuen Codes und nicht durch Ändern des vorhandenen Codes geändert. Sie kaskadieren keine Änderungen, die für Programme charakteristisch sind, die diesem Prinzip nicht entsprechen.


Closed-Entry-Strategie


Offensichtlich kann kein Programm zu 100% geschlossen werden. Was passiert beispielsweise mit der Funktion DrawAllShapes in Listing 2, wenn wir entscheiden, dass zuerst Kreise und dann Quadrate gezeichnet werden sollen? Die DrawAllShapes Funktion DrawAllShapes bei dieser Art von Änderung nicht geschlossen. Im Allgemeinen spielt es keine Rolle, wie "geschlossen" das Modul ist, es gibt immer eine Art von Änderung, von der es nicht geschlossen wird.


Da die Schließung nicht vollständig sein kann, muss sie strategisch eingeführt werden. Das heißt, der Designer muss die Arten von Änderungen auswählen, aus denen das Programm geschlossen wird. Dies erfordert einige Erfahrung. Ein erfahrener Entwickler kennt Benutzer und Branche gut genug, um die Wahrscheinlichkeit verschiedener Änderungen zu berechnen. Er stellt dann sicher, dass das Prinzip der Offenheit und Nähe für die wahrscheinlichsten Änderungen eingehalten wird.


Verwendung der Abstraktion, um zusätzliche Nähe zu erreichen


Wie können wir die DrawAllShapes Funktion aufgrund von Änderungen in der Zeichnungsreihenfolge schließen? Denken Sie daran, dass der Abschluss auf Abstraktion basiert. Um DrawAllShapes von der Bestellung DrawAllShapes , benötigen wir daher eine Art „Ordnungsabstraktion“. Ein oben dargestellter Sonderfall der Bestellung ist das Zeichnen von Figuren eines Typs vor Figuren eines anderen Typs.


Die Bestellrichtlinie impliziert, dass Sie mit zwei Objekten festlegen können, welches zuerst gezeichnet werden soll. Daher können wir eine Methode für die Shape Klasse mit dem Namen Precedes , die ein anderes Shape Objekt als Argument verwendet und den Booleschen Wert true zurückgibt true wenn das Shape Objekt, das diese Nachricht empfangen hat, vor dem Shape Objekt sortiert werden muss als Argument übergeben.


In C ++ kann diese Funktion als Überladung des Operators "<" dargestellt werden. Listing 3 zeigt die Shape Klasse mit Sortiermethoden.


Nachdem wir nun die Möglichkeit haben, die Reihenfolge der Objekte der Shape Klasse zu bestimmen, können wir sie sortieren und dann zeichnen. Listing 4 zeigt den entsprechenden C ++ - Code. Es verwendet die Klassen Set , OrderedSet und Iterator aus der Kategorie Components die in meinem Buch entwickelt wurden (Entwerfen objektorientierter C ++ - Anwendungen mit der Booch-Methode, Robert C. Martin, Prentice Hall, 1995).


Daher haben wir die Reihenfolge der Objekte der Shape Klasse implementiert und sie in der entsprechenden Reihenfolge gezeichnet. Aber wir haben immer noch keine Implementierung der Abstraktion der Ordnung. Offensichtlich muss jedes Shape Objekt die Precedes Methode überschreiben, um die Reihenfolge zu bestimmen. Wie kann das funktionieren? Welcher Code muss in Circle::Precedes werden, damit Kreise zu Quadraten gezeichnet werden? Achten Sie auf Listing 5.


 // 3 //  Shape    . class Shape { public: virtual void Draw() const = 0; virtual bool Precedes(const Shape&) const = 0; bool operator<(const Shape& s) {return Precedes(s);} }; 

 // 4 // DrawAllShapes   void DrawAllShapes(Set<Shape*>& list) { //    OrderedSet  . OrderedSet<Shape*> orderedList = list; orderedList.Sort(); for (Iterator<Shape*> i(orderedList); i; i++) (*i)->Draw(); } 

 // 5 //    bool Circle::Precedes(const Shape& s) const { if (dynamic_cast<Square*>(s)) return true; else return false; } 

Es ist klar, dass diese Funktion nicht dem Prinzip der Offenheit-Nähe entspricht. Es gibt keine Möglichkeit, es vor den neuen Nachkommen der Shape Klasse zu schließen. Jedes Mal, wenn ein neuer Nachkomme der Shape Klasse angezeigt wird, muss diese Funktion geändert werden.


Verwenden eines datengesteuerten Ansatzes zum Schließen


Die Nähe der Erben der Shape Klasse kann mithilfe eines tabellarischen Ansatzes erreicht werden, der nicht zu Änderungen in jeder geerbten Klasse führt. Ein Beispiel für diesen Ansatz ist in Listing 6 dargestellt.


Mit diesem Ansatz haben wir die DrawAllShapes Funktion erfolgreich DrawAllShapes von Änderungen in Bezug auf die Reihenfolge und jedem Nachkommen der Shape Klasse geschlossen - aufgrund der Einführung eines neuen Nachkommen oder aufgrund einer Änderung der Ordnungsrichtlinie für Objekte der Shape Klasse in Abhängigkeit von ihrem Typ (z. B. Objekte der Squares Klasse) zuerst gezeichnet werden).


 // 6 //     #include <typeinfo.h> #include <string.h> enum {false, true}; typedef int bool; class Shape { public: virtual void Draw() const = 0; virtual bool Precedes(const Shape&) const; bool operator<(const Shape& s) const {return Precedes(s);} private: static char* typeOrderTable[]; }; char* Shape::typeOrderTable[] = { "Circle", "Square", 0 }; //      . //   ,    //  . ,    , //      bool Shape::Precedes(const Shape& s) const { const char* thisType = typeid(*this).name(); const char* argType = typeid(s).name(); bool done = false; int thisOrd = -1; int argOrd = -1; for (int i=0; !done; i++) { const char* tableEntry = typeOrderTable[i]; if (tableEntry != 0) { if (strcmp(tableEntry, thisType) == 0) thisOrd = i; if (strcmp(tableEntry, argType) == 0) argOrd = i; if ((argOrd > 0) && (thisOrd > 0)) done = true; } else // table entry == 0 done = true; } return thisOrd < argOrd; } 

Das einzige Element, das nicht daran gehindert wird, die Reihenfolge der Zeichnungsformen zu ändern, ist eine Tabelle. Die Tabelle kann in einem separaten Modul platziert werden, das von allen anderen Modulen getrennt ist. Daher wirken sich die Änderungen nicht auf andere Module aus.


Weitere Schließung


Dies ist nicht das Ende der Geschichte. Wir haben die Hierarchie der Shape Klasse und der DrawAllShapes Funktion geschlossen, um die Ordnungsrichtlinie basierend auf dem DrawAllShapes ändern. Die Nachkommen der Shape Klasse werden jedoch nicht von Ordnungsrichtlinien ausgeschlossen, die keinen Formtypen zugeordnet sind. Es scheint, dass wir das Zeichnen von Formen nach einer übergeordneten Struktur anordnen müssen. Eine vollständige Untersuchung solcher Probleme würde den Rahmen dieses Artikels sprengen. Ein interessierter Leser könnte jedoch darüber nachdenken, wie dieses Problem mithilfe der abstrakten OrderedObject Klasse OrderedShape , die in der OrderedShape Klasse enthalten ist, die von den OrderedObject und OrderedObject erbt.


Heuristiken und Konventionen


Wie bereits am Anfang des Artikels erwähnt, ist das Prinzip der Offenheit und Nähe die Hauptmotivation für viele Heuristiken und Konventionen, die sich im Laufe der vielen Jahre der Entwicklung des OOP-Paradigmas herausgebildet haben. Das Folgende sind die wichtigsten.


Machen Sie alle Mitgliedsvariablen privat


Dies ist eine der dauerhaftesten Konventionen der PLO. Mitgliedsvariablen sollten nur den Methoden der Klasse bekannt sein, in der sie definiert sind. Variable Mitglieder sollten anderen Klassen, einschließlich abgeleiteten Klassen, nicht bekannt sein. Daher müssen sie mit einem private Zugriffsmodifikator deklariert werden, nicht public oder protected .
In Anbetracht des Prinzips der Offenheit und Nähe ist der Grund für eine solche Konvention verständlich. Wenn sich Klassenmitgliedsvariablen ändern, muss sich jede von ihnen abhängige Funktion ändern. Das heißt, die Funktion wird nicht aufgrund von Änderungen an diesen Variablen geschlossen.


In OOP erwarten wir, dass die Methoden einer Klasse nicht für Änderungen in Variablen geschlossen sind, die Mitglieder dieser Klasse sind. Wir erwarten jedoch, dass jede andere Klasse, einschließlich Unterklassen, wegen Änderungen an diesen Variablen geschlossen wird. Dies wird als Kapselung bezeichnet.


Aber was ist, wenn Sie eine Variable haben, von der Sie sicher sind, dass sie sich niemals ändern wird? Ist es sinnvoll, es private zu machen? In Listing 7 wird beispielsweise die Device angezeigt, die den bool status des variablen Mitglieds enthält. Es speichert den Status der letzten Operation. Wenn der Vorgang erfolgreich war, ist der Wert der Statusvariablen true , andernfalls false .


 // 7 //   class Device { public: bool status; }; 

Wir wissen, dass sich der Typ oder die Bedeutung dieser Variablen niemals ändern wird. Warum also nicht public und dem Kunden direkten Zugriff darauf gewähren? Wenn sich die Variable wirklich nie ändert, wenn alle Clients die Regeln befolgen und nur aus dieser Variablen lesen, ist nichts falsch daran, dass die Variable öffentlich ist. Überlegen Sie jedoch, was passieren wird, wenn einer der Clients die Gelegenheit nutzt, in diese Variable zu schreiben und ihren Wert zu ändern.


Plötzlich kann dieser Client den Betrieb eines anderen Clients der Device beeinträchtigen. Dies bedeutet, dass es unmöglich ist, Clients der Device vor Änderungen an diesem falschen Modul zu schließen. Das ist zu viel Risiko.


Nehmen wir andererseits an, wir haben die in Listing 8 gezeigte Zeitklasse. Welche Gefahr besteht für die Veröffentlichung der Variablen, die Mitglieder dieser Klasse sind? Es ist sehr unwahrscheinlich, dass sie sich ändern werden. Darüber hinaus spielt es keine Rolle, ob die Client-Module die Werte dieser Variablen ändern oder nicht, da eine Änderung dieser Variablen angenommen wird. Es ist auch sehr unwahrscheinlich, dass geerbte Klassen vom Wert einer bestimmten Mitgliedsvariablen abhängen können. Gibt es also ein Problem?


 // 8 class Time { public: int hours, minutes, seconds; Time& operator-=(int seconds); Time& operator+=(int seconds); bool operator< (const Time&); bool operator> (const Time&); bool operator==(const Time&); bool operator!=(const Time&); }; 

Die einzige Beschwerde, die ich gegen den Code in Listing 8 machen könnte, ist, dass die Zeitänderung nicht atomar ist. Das heißt, der Client kann den Wert der Minutenvariablen ändern, ohne den Wert der hours zu ändern. Dies kann dazu führen, dass ein Objekt der Zeitklasse inkonsistente Daten enthält. Ich würde es vorziehen, eine einzige Funktion zum Einstellen der Zeit einzuführen, die drei Argumente benötigt, wodurch das Einstellen der Zeit zu einer atomaren Operation wird. Dies ist jedoch ein schwaches Argument.


Es ist leicht, andere Bedingungen zu finden, unter denen die Veröffentlichung dieser Variablen zu Problemen führen kann. Letztendlich gibt es jedoch keinen überzeugenden Grund, sie private zu machen. Ich denke immer noch, dass es ein schlechter Stil ist, solche Variablen öffentlich zu machen, aber vielleicht ist es kein schlechtes Design. Ich glaube, dass dies ein schlechter Stil ist, da es fast nichts kostet, die entsprechenden Funktionen für den Zugriff auf diese Mitglieder einzugeben, und es sich definitiv lohnt, sich vor dem geringen Risiko zu schützen, das mit dem möglichen Auftreten von Problemen beim Schließen verbunden ist.


In solchen seltenen Fällen, in denen das Prinzip der Offenheit nicht verletzt wird, hängt das Verbot public und protected Variablen daher mehr vom Stil und nicht vom Inhalt ab.


Keine globalen Variablen ... überhaupt nicht!


Das Argument gegen globale Variablen ist dasselbe wie das Argument gegen öffentliche Mitgliedsvariablen. Kein Modul, das von einer globalen Variablen abhängt, kann von einem Modul geschlossen werden, das darauf schreiben kann. Jedes Modul, das diese Variable auf eine Weise verwendet, die nicht von anderen Modulen beabsichtigt ist, bricht diese Module. Es ist zu riskant, viele Module zu haben, abhängig von den Unwägbarkeiten eines einzelnen bösartigen Moduls.
Andererseits schaden globale Variablen in Fällen, in denen eine geringe Anzahl von Modulen von ihnen abhängt oder nicht falsch verwendet werden kann, nicht. Der Designer muss bewerten, wie viel Datenschutz geopfert wird, und feststellen, ob sich die Bequemlichkeit der globalen Variablen lohnt.


Auch hier kommen Stilprobleme ins Spiel. Alternativen zur Verwendung globaler Variablen sind normalerweise kostengünstig. In solchen Fällen ist die Verwendung einer Technik, die zwar ein geringes, aber ein Risiko für den Verschluss einführt, anstelle einer Technik, die ein solches Risiko vollständig ausschließt, ein Zeichen für einen schlechten Stil. Manchmal ist es jedoch sehr praktisch, globale Variablen zu verwenden. Ein typisches Beispiel sind die globalen Variablen cout und cin. In solchen Fällen können Sie den Stil der Einfachheit halber opfern, wenn das Prinzip der Offenheit und Nähe nicht verletzt wird.


RTTI ist gefährlich


Ein weiteres häufiges Verbot ist die Verwendung von dynamic_cast . Sehr oft wird dynamic_cast oder eine andere Form der Laufzeittypbestimmung (RTTI) als äußerst gefährliche Technik beschuldigt und sollte daher vermieden werden. Gleichzeitig geben sie häufig ein Beispiel aus Listing 9 an, das offensichtlich gegen das Prinzip der Offenheit und Nähe verstößt. Listing 10 zeigt jedoch ein Beispiel für ein ähnliches Programm, das dynamic_cast ohne das Open-Close-Prinzip zu verletzen.


Der Unterschied zwischen ihnen besteht darin, dass im ersten Fall, der in Listing 9 gezeigt wird, der Code jedes Mal geändert werden muss, wenn ein neuer Nachkomme der Shape Klasse erscheint (ganz zu schweigen davon, dass dies eine absolut lächerliche Lösung ist). In Listing 10 sind in diesem Fall jedoch keine Änderungen erforderlich. Daher verstößt der Code in Listing 10 nicht gegen das Open-Close-Prinzip.
In diesem Fall gilt als Faustregel, dass RTTI verwendet werden kann, wenn das Prinzip der Offenheitsschließung nicht verletzt wird.


 // 9 //RTTI,   -. class Shape {}; class Square : public Shape { private: Point itsTopLeft; double itsSide; friend DrawSquare(Square*); }; class Circle : public Shape { private: Point itsCenter; double itsRadius; friend DrawCircle(Circle*); }; void DrawAllShapes(Set<Shape*>& ss) { for (Iterator<Shape*>i(ss); i; i++) { Circle* c = dynamic_cast<Circle*>(*i); Square* s = dynamic_cast<Square*>(*i); if (c) DrawCircle(c); else if (s) DrawSquare(s); } } 

 // 10 //RTTI,    -. class Shape { public: virtual void Draw() cont = 0; }; class Square : public Shape { // . }; void DrawSquaresOnly(Set<Shape*>& ss) { for (Iterator<Shape*>i(ss); i; i++) { Square* s = dynamic_cast<Square*>(*i); if (s) s->Draw(); } } 

Fazit


Ich konnte lange über das Prinzip der Offenheit und Nähe sprechen. In vielerlei Hinsicht ist dieses Prinzip für die objektorientierte Programmierung am wichtigsten. Die Einhaltung dieses speziellen Prinzips bietet die Hauptvorteile der objektorientierten Technologie, nämlich Wiederverwendung und Support.


, - -. , , , , , .

Source: https://habr.com/ru/post/de472186/


All Articles