Wie man sich in der Praxis an das Open-Closed-Prinzip hält

14

Ich verstehe die Absicht des Open-Closed-Prinzips. Es soll das Risiko verringern, dass etwas beschädigt wird, das bereits funktioniert, während es geändert wird, indem Sie versuchen, es ohne Änderung zu erweitern.

Ich hatte jedoch einige Probleme zu verstehen, wie dieses Prinzip in der Praxis angewendet wird. Meines Erachtens gibt es zwei Möglichkeiten, dies anzuwenden. Vor und nach einer möglichen Änderung:

  1. Vorher: Programmiere auf Abstraktionen und sage die Zukunft voraus, so oft du kannst. Beispielsweise muss eine Methode drive(Car car)geändert werden, wenn Motorcycledem System in Zukunft s hinzugefügt werden, sodass dies wahrscheinlich gegen OCP verstößt. Es drive(MotorVehicle vehicle)ist jedoch weniger wahrscheinlich, dass sich die Methode in Zukunft ändern muss, sodass sie an OCP festhält.

    Es ist jedoch ziemlich schwierig, die Zukunft vorherzusagen und im Voraus zu wissen, welche Änderungen am System vorgenommen werden.

  2. After: Wenn eine Änderung erforderlich ist, erweitern Sie eine Klasse, anstatt den aktuellen Code zu ändern.

Übung 1 ist nicht schwer zu verstehen. In der zweiten Übung habe ich jedoch Probleme, die Bewerbung zu verstehen.

Zum Beispiel (ich nahm es von einem Video auf YouTube): Lassen Sie uns sagen , dass wir eine Methode in einer Klasse, die akzeptiert CreditCardObjekte: makePayment(CraditCard card). Ein Tag Voucherwird dem System hinzugefügt. Diese Methode unterstützt sie nicht und muss daher geändert werden.

Bei der ersten Implementierung der Methode ist es uns nicht gelungen, die Zukunft vorherzusagen und abstrakter zu programmieren (z. B. makePayment(Payment pay)müssen wir jetzt den vorhandenen Code ändern.

In Übung 2 heißt es, wir sollten die Funktionalität erweitern, anstatt sie zu ändern. Was bedeutet das? Soll ich die vorhandene Klasse in Unterklassen unterteilen, anstatt nur den vorhandenen Code zu ändern? Sollte ich eine Art Wrapper erstellen, um zu vermeiden, dass Code neu geschrieben wird?

Oder bezieht sich das Prinzip nicht einmal auf das richtige Ändern / Hinzufügen von Funktionen, sondern vielmehr auf das Vermeiden von Änderungen an erster Stelle (dh Programmieren auf Abstraktionen)?

Aviv Cohn
quelle
1
Das Open / Closed-Prinzip bestimmt nicht den von Ihnen verwendeten Mechanismus. Vererbung ist normalerweise die falsche Wahl. Es ist auch unmöglich, sich vor allen zukünftigen Änderungen zu schützen. Es ist am besten, nicht zu versuchen, die Zukunft vorherzusagen, aber wenn eine Änderung erforderlich ist, ändern Sie das Design, damit zukünftige Änderungen dieser Art berücksichtigt werden können.
Doval

Antworten:

14

Gestaltungsprinzipien müssen immer gegeneinander abgewogen werden. Sie können die Zukunft nicht vorhersagen, und die meisten Programmierer tun es schrecklich, wenn sie es versuchen. Aus diesem Grund haben wir die Dreierregel , bei der es in erster Linie um die Vervielfältigung geht, die jedoch auch für die Umgestaltung anderer Designprinzipien gilt.

Wenn Sie nur eine Implementierung einer Schnittstelle haben, müssen Sie sich nicht viel um OCP kümmern, es sei denn, es ist glasklar, wo Erweiterungen stattfinden würden. Tatsächlich verlieren Sie oft an Klarheit, wenn Sie versuchen, in dieser Situation zu viel zu entwerfen. Wenn Sie es einmal erweitern, überarbeiten Sie es, um es OCP-freundlich zu machen, wenn dies der einfachste und klarste Weg ist, dies zu tun. Wenn Sie es auf eine dritte Implementierung erweitern, stellen Sie sicher, dass Sie es unter Berücksichtigung von OCP umgestalten, auch wenn es etwas mehr Aufwand erfordert.

In der Praxis ist das Refactoring beim Hinzufügen einer dritten Implementierung in der Regel nicht allzu schwierig, wenn nur zwei Implementierungen vorhanden sind. Wenn Sie es über diesen Punkt hinaus wachsen lassen, wird es schwierig, es aufrechtzuerhalten.

Karl Bielefeldt
quelle
1
Danke für die Antwort. Lassen Sie mich sehen, ob ich verstehe, was Sie sagen: Was Sie sagen, ist, dass ich mich um OCP kümmern sollte, hauptsächlich nachdem ich gezwungen war, Änderungen an einer Klasse vorzunehmen. Das heißt: Wenn ich zum ersten Mal eine Klasse implementiere, sollte ich mir keine Gedanken über OCP machen, da es ohnehin schwierig ist, die Zukunft vorherzusagen. Wenn ich es zum ersten Mal erweitern / modifizieren muss, ist es vielleicht eine gute Idee, es ein wenig umzugestalten, um in Zukunft flexibler zu sein (mehr OCP). Und in dem dritten Mal, in dem ich die Klasse erweitern / modifizieren muss, ist es Zeit, einige Umgestaltungen vorzunehmen, um die Einhaltung von OCP zu verbessern. Ist es das was du meinst?
Aviv Cohn
1
Das ist die Idee.
Karl Bielefeldt
2

Ich denke, Sie schauen zu weit in die Zukunft. Lösen Sie das aktuelle Problem auf flexible Weise, die offen / geschlossen hält.

Angenommen, Sie müssen eine drive(Car car)Methode implementieren . Abhängig von Ihrer Sprache haben Sie mehrere Möglichkeiten.

  • Für Sprachen, die das Überladen unterstützen (C ++), verwenden Sie einfach drive(const Car& car)

    Irgendwann später brauchen Sie vielleicht drive(const Motorcycle& motorcycle), aber es wird nicht stören drive(const Car& car). Kein Problem!

  • Fügen Sie für Sprachen, die keine Überladung unterstützen (Ziel C), den Typnamen in die Methode ein -driveCar:(Car *)car.

    Irgendwann später müssen Sie möglicherweise -driveMotorcycle:(Motorcycle *)motorcycle, aber auch hier wird es nicht stören.

Dies kann drive(Car car)nicht geändert werden, kann jedoch auf andere Fahrzeugtypen ausgeweitet werden. Diese minimalistische Zukunftsplanung ermöglicht es Ihnen, Ihre Arbeit heute zu erledigen, verhindert jedoch, dass Sie sich in Zukunft blockieren.

Der Versuch, sich die grundlegendsten Typen vorzustellen, die Sie benötigen, kann zu einer unendlichen Regression führen. Was passiert, wenn Sie einen Segue, ein Fahrrad oder einen Jumbo-Jet fahren möchten? Wie konstruieren Sie einen einzelnen generischen abstrakten Typ, der alle Geräte berücksichtigt, die Menschen für die Mobilität einsetzen und verwenden können?

Jeffery Thomas
quelle
Das Ändern einer Klasse zum Hinzufügen Ihrer neuen Methode verstößt gegen das Open-Closed-Prinzip. Ihr Vorschlag beseitigt auch die Möglichkeit, das Liskov-Substitutionsprinzip auf alle Fahrzeuge anzuwenden, die fahren können, wodurch einer der stärksten Teile von OO im Wesentlichen beseitigt wird.
Eintauchen
@Dunk Ich habe meine Antwort auf das polymorphe Open / Closed-Prinzip gestützt, nicht auf das strikte Meyer-Open / Closed-Prinzip. Es ist zulässig, Klassen zu aktualisieren, um neue Schnittstellen zu unterstützen. In diesem Beispiel wird die Fahrzeugschnittstelle von der Motorradschnittstelle getrennt gehalten. Diese könnten als separate abstrakte Fahrklassen für Auto und Motorrad formalisiert werden, die von der ausführenden Klasse unterstützt werden könnten.
Jeffery Thomas
@Dunk Das Liskov-Substitutionsprinzip ist nützlich, wird aber nicht kostenlos angeboten. Wenn die ursprüngliche Spezifikation nur ein Auto erfordert, ist die Schaffung eines allgemeineren Fahrzeugs die zusätzlichen Kosten für Geld, Zeit und Komplexität möglicherweise nicht wert. Darüber hinaus ist es unwahrscheinlich, dass ein generischeres Fahrzeug perfekt für ungeplante Unterklassen geeignet ist. Entweder muss die Schnittstelle für ein Motorrad in die Fahrzeugschnittstelle (die nur für den Umgang mit Autos ausgelegt ist) eingeschnitten werden, oder Sie müssen das Fahrzeug für den Umgang mit Motorrädern modifizieren (eine echte Verletzung von offen / geschlossen).
Jeffery Thomas
Das Liskov-Substitutionsprinzip ist nicht kostenlos, aber auch nicht teuer. Und normalerweise zahlt es sich um ein Vielfaches aus, selbst wenn in der Hauptanwendung nie eine andere Unterklasse davon geerbt wird. Die Anwendung von LSP erleichtert das automatisierte Testen erheblich, was bereits ein Gewinn ist. Auch wenn Sie auf keinen Fall verrückt werden sollten und davon ausgehen, dass alles LSP benötigt, können Sie dies nicht tun, wenn Sie eine Anwendung erstellen und nicht wissen, was sie in einer zukünftigen Rev. voraussichtlich benötigen wird wissen genug über Ihre Anwendung oder ihre Domäne.
Eintauchen
1
In Bezug auf die Definition von OCP. Möglicherweise sind in den Branchen, in denen ich gearbeitet habe, häufig höhere Überprüfungen erforderlich als bei einem normalen Handelsunternehmen. Wenn sich jedoch eine Datei / Klasse ändert, müssen Sie nicht nur die Datei / Klasse erneut testen, sondern auch alles andere verwendet diese Datei / Klasse für Ihre Regressionstests. Es spielt also keine Rolle, ob jemand sagt, dass polymorphes Öffnen / Schließen in Ordnung ist. Das Ändern der Benutzeroberfläche hat weitreichende Konsequenzen, sodass es nicht in Ordnung ist.
Eintauchen
2

Ich verstehe die Absicht des Open-Closed-Prinzips. Es soll das Risiko verringern, dass etwas beschädigt wird, das bereits funktioniert, während es geändert wird, indem Sie versuchen, es ohne Änderung zu erweitern.

Es geht auch darum, nicht alle Objekte zu brechen, die auf dieser Methode basieren, indem das Verhalten bereits vorhandener Objekte nicht geändert wird. Sobald ein Objekt eine Verhaltensänderung angekündigt hat, ist dies riskant, da Sie das bekannte und erwartete Verhalten des Objekts ändern, ohne genau zu wissen, welche anderen Objekte dieses Verhalten erwarten.

Was bedeutet das? Soll ich die vorhandene Klasse in Unterklassen unterteilen, anstatt nur den vorhandenen Code zu ändern?

Jep.

"Akzeptiert nur Kreditkarten" wird als Teil des Verhaltens dieser Klasse über ihre öffentliche Schnittstelle definiert. Der Programmierer hat der Welt erklärt, dass die Methode dieses Objekts nur Kreditkarten akzeptiert. Sie hat es mit einem nicht besonders eindeutigen Methodennamen gemacht, aber es ist geschafft. Der Rest des Systems verlässt sich darauf.

Das mag damals Sinn gemacht haben, aber wenn es sich jetzt ändern muss, sollten Sie eine neue Klasse erstellen, die andere Dinge als Kreditkarten akzeptiert.

Neues Verhalten = neue Klasse

Nebenbei - Eine gute Möglichkeit, die Zukunft vorherzusagen, besteht darin, über den Namen nachzudenken, den Sie einer Methode gegeben haben. Haben Sie einer Methode einen wirklich allgemein klingenden Methodennamen wie makePayment mit bestimmten Regeln in der Methode gegeben, um genau zu bestimmen, welche Zahlung sie leisten kann? Das ist ein Codegeruch. Wenn Sie spezifische Regeln haben, sollten diese anhand des Methodennamens verdeutlicht werden - makePayment sollte makeCreditCardPayment sein. Tun Sie dies, wenn Sie das Objekt zum ersten Mal schreiben, und andere Programmierer werden es Ihnen danken.

Cormac Mulhall
quelle