Nutzen Sie die Vorteile des Open-Closed-Prinzips?

11

Das Open-Closed-Prinzip (OCP) besagt, dass ein Objekt zur Erweiterung geöffnet, zur Änderung jedoch geschlossen sein sollte. Ich glaube, ich verstehe es und verwende es in Verbindung mit SRP, um Klassen zu erstellen, die nur eines tun. Und ich versuche, viele kleine Methoden zu erstellen, die es ermöglichen, alle Verhaltenssteuerelemente in Methoden zu extrahieren, die in einigen Unterklassen erweitert oder überschrieben werden können. So erhalte ich Klassen mit vielen Erweiterungspunkten, sei es durch: Abhängigkeitsinjektion und -zusammensetzung, Ereignisse, Delegierung usw.

Betrachten Sie Folgendes als einfache, erweiterbare Klasse:

class PaycheckCalculator {
    // ...
    protected decimal GetOvertimeFactor() { return 2.0M; }
}

Sagen wir nun zum Beispiel, dass sich die OvertimeFactorÄnderungen auf 1.5 ändern. Da die obige Klasse erweitert werden sollte, kann ich leicht eine andere Unterklasse erstellen und zurückgeben OvertimeFactor.

Aber ... obwohl die Klasse für die Erweiterung und Einhaltung von OCP ausgelegt ist, werde ich die betreffende einzelne Methode ändern, anstatt die betreffende Methode zu unterklassifizieren und zu überschreiben und dann meine Objekte in meinem IoC-Container neu zu verkabeln.

Infolgedessen habe ich einen Teil dessen verletzt, was OCP zu erreichen versucht. Es fühlt sich an, als wäre ich nur faul, weil das oben genannte etwas einfacher ist. Verstehe ich OCP falsch? Sollte ich wirklich etwas anderes machen? Nutzen Sie die Vorteile von OCP anders?

Update : Basierend auf den Antworten sieht es so aus, als ob dieses erfundene Beispiel aus verschiedenen Gründen schlecht ist. Die Hauptabsicht des Beispiels bestand darin, zu demonstrieren, dass die Klasse so erweitert werden sollte, dass Methoden bereitgestellt werden, die beim Überschreiben das Verhalten öffentlicher Methoden ändern , ohne dass interner oder privater Code geändert werden muss. Trotzdem habe ich OCP definitiv falsch verstanden.

Kaleb Pederson
quelle

Antworten:

9

Wenn Sie die Basisklasse ändern, ist sie nicht wirklich geschlossen, oder?

Denken Sie an die Situation, in der Sie die Bibliothek für die Welt freigegeben haben. Wenn Sie das Verhalten Ihrer Basisklasse ändern, indem Sie den Überstundenfaktor auf 1,5 ändern, haben Sie alle Personen verletzt, die Ihren Code verwenden, vorausgesetzt, die Klasse wurde geschlossen.

Um die Klasse wirklich geschlossen, aber offen zu machen, sollten Sie den Überstundenfaktor aus einer alternativen Quelle (Konfigurationsdatei vielleicht) abrufen oder eine virtuelle Methode beweisen, die überschrieben werden kann?

Wenn die Klasse wirklich geschlossen wäre, würden nach Ihrer Änderung keine Testfälle fehlschlagen (vorausgesetzt, Sie haben eine 100% ige Abdeckung mit all Ihren Testfällen), und ich würde annehmen, dass es einen Testfall gibt, der dies überprüft GetOvertimeFactor() == 2.0M.

Nicht über Ingenieur

Aber nehmen Sie dieses Open-Close-Prinzip nicht zum logischen Schluss und lassen Sie alles von Anfang an konfigurieren (das ist über das Engineering hinaus). Definieren Sie nur die Bits, die Sie aktuell benötigen.

Das geschlossene Prinzip hindert Sie nicht daran, das Objekt neu zu konstruieren. Es hindert Sie lediglich daran, die aktuell definierte öffentliche Schnittstelle in Ihr Objekt zu ändern ( geschützte Mitglieder sind Teil der öffentlichen Schnittstelle). Sie können noch weitere Funktionen hinzufügen, solange die alte Funktionalität nicht beschädigt ist.

Martin York
quelle
"Das geschlossene Prinzip hindert Sie nicht daran, das Objekt neu zu konstruieren." Eigentlich ist es tut . Wenn Sie das Buch lesen, in dem das Open-Closed-Prinzip zum ersten Mal vorgeschlagen wurde, oder den Artikel, in dem das Akronym "OCP" eingeführt wurde, heißt es: "Niemand darf Änderungen am Quellcode vornehmen" (außer Fehler) Korrekturen).
Rogério
@ Rogério: Das mag wahr sein (1988). Bei der aktuellen Definition (die 1990 populär wurde, als OO populär wurde) geht es jedoch darum, eine konsistente öffentliche Schnittstelle aufrechtzuerhalten. During the 1990s, the open/closed principle became popularly redefined to refer to the use of abstracted interfaces, where the implementations can be changed and multiple implementations could be created and polymorphically substituted for each other. en.wikipedia.org/wiki/Open/closed_principle
Martin York
Vielen Dank für die Wikipedia-Referenz. Ich bin mir jedoch nicht sicher, ob die "aktuelle" Definition wirklich anders ist, da sie immer noch auf der Vererbung von Typen (Klassen oder Schnittstellen) beruht. Und das von mir erwähnte Zitat "Keine Änderungen des Quellcodes" stammt aus Robert Martins OCP 1996-Artikel, der (angeblich) der "aktuellen Definition" entspricht. Persönlich denke ich, dass das Open-Closed-Prinzip inzwischen vergessen wäre, wenn Martin ihm kein Akronym gegeben hätte, das anscheinend viel Marketingwert hat. Das Prinzip selbst ist veraltet und schädlich, IMO.
Rogério
3

Das Open Closed-Prinzip ist also ein Problem ... besonders wenn Sie versuchen, es gleichzeitig mit YAGNI anzuwenden . Wie halte ich mich gleichzeitig an beide? Wenden Sie die Dreierregel an . Wenn Sie zum ersten Mal eine Änderung vornehmen, nehmen Sie diese direkt vor. Und das zweite Mal auch. Das dritte Mal ist es Zeit, diese Veränderung zu abstrahieren.

Ein anderer Ansatz ist "täusche mich einmal ...". Wenn Sie eine Änderung vornehmen müssen, wenden Sie OCP an, um sich in Zukunft vor dieser Änderung zu schützen . Ich würde fast so weit gehen, vorzuschlagen, dass die Änderung der Überstundenquote eine neue Geschichte ist. "Als Lohnbuchhalter möchte ich die Überstundenquote ändern, damit ich die geltenden Arbeitsgesetze einhalten kann." Jetzt haben Sie eine neue Benutzeroberfläche, mit der Sie die Überstundenrate ändern und speichern können. GetOvertimeFactor () fragt nur das Repository nach der Überstundenrate.

Michael Brown
quelle
2

In dem von Ihnen veröffentlichten Beispiel sollte der Überstundenfaktor eine Variable oder eine Konstante sein. * (Java-Beispiel)

class PaycheckCalculator {
   float overtimeFactor;

   protected float setOvertimeFactor(float overtimeFactor) {
      this.overtimeFactor = overtimeFactor;
   }

   protected float getOvertimeFactor() {
      return overtimeFactor;
   }
}

ODER

class PaycheckCalculator {
   public static final float OVERTIME_FACTOR = 1.5f;
}

Wenn Sie dann die Klasse erweitern, setzen oder überschreiben Sie den Faktor. "Magische Zahlen" sollten nur einmal erscheinen. Dies ist viel mehr im Stil von OCP und DRY (Don't Repeat Yourself), da es nicht erforderlich ist, eine ganz neue Klasse für einen anderen Faktor zu erstellen, wenn die erste Methode verwendet wird und nur die Konstante in einer Redewendung geändert werden muss Platz in der zweiten.

Ich würde den ersten in Fällen verwenden, in denen es mehrere Arten von Taschenrechnern gibt, die jeweils unterschiedliche konstante Werte benötigen. Ein Beispiel wäre das Chain of Responsibility-Muster, das normalerweise mit geerbten Typen implementiert wird. Ein Objekt, das nur die Schnittstelle sehen kann (dh getOvertimeFactor()), verwendet sie, um alle benötigten Informationen abzurufen, während sich die Untertypen um die tatsächlich bereitgestellten Informationen kümmern.

Die zweite ist nützlich in Fällen, in denen die Konstante wahrscheinlich nicht geändert wird, aber an mehreren Stellen verwendet wird. Es ist viel einfacher, eine Konstante zu ändern (in dem unwahrscheinlichen Fall, dass dies der Fall ist), als sie überall festzulegen oder aus einer Eigenschaftendatei abzurufen.

Das Open-Closed-Prinzip ist weniger ein Aufruf, ein vorhandenes Objekt nicht zu ändern, als eine Warnung, die Schnittstelle für sie unverändert zu lassen. Wenn Sie ein etwas anderes Verhalten als eine Klasse oder zusätzliche Funktionen für einen bestimmten Fall benötigen, erweitern und überschreiben Sie diese. Wenn sich jedoch die Anforderungen für die Klasse selbst ändern (z. B. das Ändern des Faktors), müssen Sie die Klasse ändern. Es macht keinen Sinn in einer riesigen Klassenhierarchie, von der die meisten nie verwendet werden.

Michael K.
quelle
Dies ist eine Datenänderung, keine Codeänderung. Die Überstundenrate sollte nicht fest codiert sein.
Jim C
Sie scheinen Ihr Get und Ihr Set rückwärts zu haben.
Mason Wheeler
Hoppla! hätte testen sollen ...
Michael K
2

Ich sehe Ihr Beispiel nicht wirklich als eine großartige Darstellung von OCP. Ich denke, was die Regel wirklich bedeutet, ist Folgendes:

Wenn Sie eine Funktion hinzufügen möchten, müssen Sie nur eine Klasse hinzufügen und keine andere Klasse (möglicherweise jedoch eine Konfigurationsdatei) ändern.

Eine schlechte Implementierung unten. Jedes Mal, wenn Sie ein Spiel hinzufügen, müssen Sie die GamePlayer-Klasse ändern.

class GamePlayer
{
   public void PlayGame(string game)
   {
      switch(game)
      {
          case "Poker":
              PlayPoker();
              break;

          case "Gin": 
              PlayGin();
              break;

          ...
      }
   }

   ...
}

Die GamePlayer-Klasse sollte niemals geändert werden müssen

class GamePlayer
{
    ...

    public void PlayGame(string game)
    {
        Game g = GameFactory.GetByName(game); 
        g.Play();   
    }

    ...
}

Angenommen, meine GameFactory hält sich auch an OCP, wenn ich ein weiteres Spiel hinzufügen möchte, müsste ich nur eine neue Klasse erstellen, die von der GameKlasse erbt, und alles sollte einfach funktionieren.

Allzu oft werden Klassen wie die erste nach Jahren der "Erweiterungen" aufgebaut und von der Originalversion nie richtig überarbeitet (oder schlimmer noch, was mehrere Klassen sein sollten, bleibt eine große Klasse).

Das von Ihnen angegebene Beispiel ist OCP-ish. Meiner Meinung nach wäre der richtige Weg, um Änderungen der Überstundenrate zu verarbeiten, eine Datenbank mit historischen Raten, damit die Daten erneut verarbeitet werden können. Der Code sollte weiterhin zur Änderung geschlossen werden, da immer der entsprechende Wert aus der Suche geladen wird.

Als Beispiel aus der Praxis habe ich eine Variante meines Beispiels verwendet, und das Open-Closed-Prinzip strahlt wirklich. Funktionalität ist sehr einfach hinzuzufügen, da ich nur von einer abstrakten Basisklasse ableiten muss und meine "Fabrik" sie automatisch aufnimmt und es dem "Spieler" egal ist, welche konkrete Implementierung die Fabrik zurückgibt.

Austin Salonen
quelle
1

In diesem Beispiel haben Sie einen sogenannten "magischen Wert". Im Wesentlichen ein fest codierter Wert, der sich im Laufe der Zeit ändern kann oder nicht. Ich werde versuchen, das Rätsel zu lösen, das Sie generisch ausdrücken, aber dies ist ein Beispiel für die Art von Dingen, bei denen das Erstellen einer Unterklasse mehr Arbeit bedeutet als das Ändern eines Werts in einer Klasse.

Höchstwahrscheinlich haben Sie das Verhalten in Ihrer Klassenhierarchie zu früh angegeben.

Nehmen wir an, wir haben die PaycheckCalculator. Das OvertimeFactorwürde höchstwahrscheinlich von Informationen über den Mitarbeiter abgeschlüsselt werden. Ein stündlicher Mitarbeiter kann einen Überstundenbonus erhalten, während ein Angestellter nichts bezahlt bekommt. Trotzdem werden einige Angestellte aufgrund des Vertrags, an dem sie gearbeitet haben, gerade Zeit bekommen. Sie können entscheiden, dass es bestimmte bekannte Klassen von Vergütungsszenarien gibt, und so würden Sie Ihre Logik aufbauen.

In der Basisklasse PaycheckCalculatormachen Sie es abstrakt und geben die erwarteten Methoden an. Die Kernberechnungen sind die gleichen, nur dass bestimmte Faktoren unterschiedlich berechnet werden. Sie HourlyPaycheckCalculatorwürden dann die getOvertimeFactorMethode implementieren und je nach Fall 1,5 oder 2,0 zurückgeben. Sie StraightTimePaycheckCalculatorwürden das implementieren getOvertimeFactor, um 1.0 zurückzugeben. Schließlich wäre eine dritte Implementierung eine NoOvertimePaycheckCalculator, die die getOvertimeFactorRückgabe von 0 implementiert .

Der Schlüssel besteht darin, nur das Verhalten in der Basisklasse zu beschreiben, das erweitert werden soll. Die Details von Teilen des Gesamtalgorithmus oder bestimmten Werten würden durch Unterklassen ausgefüllt. Die Tatsache, dass Sie einen Standardwert für die getOvertimeFactorZeilen angegeben haben, führt zu einer schnellen und einfachen "Korrektur" der einen Zeile, anstatt die Klasse wie beabsichtigt zu erweitern. Es wird auch die Tatsache hervorgehoben, dass mit der Erweiterung des Unterrichts Anstrengungen verbunden sind. Es sind auch Anstrengungen erforderlich, um die Hierarchie der Klassen in Ihrer Anwendung zu verstehen. Sie möchten Ihre Klassen so gestalten, dass die Notwendigkeit, Unterklassen zu erstellen, minimiert wird und gleichzeitig die erforderliche Flexibilität geboten wird.

Denkanstöße: Wenn unsere Klassen bestimmte Datenfaktoren wie OvertimeFactorin Ihrem Beispiel enthalten, benötigen Sie möglicherweise eine Möglichkeit, diese Informationen aus einer anderen Quelle abzurufen. Beispielsweise würde eine Eigenschaftendatei (da dies wie Java aussieht) oder eine Datenbank den Wert enthalten, und Sie PaycheckCalculatorwürden ein Datenzugriffsobjekt verwenden, um Ihre Werte abzurufen. Auf diese Weise können die richtigen Personen das Verhalten des Systems ändern, ohne dass ein Code neu geschrieben werden muss.

Berin Loritsch
quelle