Offen Geschlossenes Prinzip in Entwurfsmustern

8

Ich bin etwas verwirrt darüber, wie das Open-Closed-Prinzip im wirklichen Leben angewendet werden kann. Die Anforderungen an geschäftliche Änderungen ändern sich im Laufe der Zeit. Nach dem Open-Closed-Prinzip sollten Sie die Klasse erweitern, anstatt die vorhandene Klasse zu ändern. Jedes Mal, wenn ich eine Klasse erweitere, scheint es mir nicht praktisch zu sein, die Anforderung zu erfüllen. Lassen Sie mich ein Beispiel mit dem Zugbuchungssystem geben.

Im Zugbuchungssystem gibt es ein Ticketobjekt. Es kann verschiedene Arten von Tickets geben. Regular Ticket, Concession Ticket usw. Ticket ist eine abstrakte Klasse und RegularTicket und ConcessionTickets sind konkrete Klassen. Da alle Tickets über die übliche PrintTicket-Methode verfügen, wird sie in Ticket geschrieben, einer abstrakten Basisklasse. Nehmen wir an, das hat ein paar Monate lang gut funktioniert. Jetzt kommt eine neue Anforderung hinzu, die besagt, dass das Format des Tickets geändert werden muss. Möglicherweise werden dem gedruckten Ticket nur noch wenige Felder hinzugefügt oder das Format wird geändert. Um diese Anforderung zu erfüllen, habe ich folgende Möglichkeiten

  1. Ändern Sie die PrintTicket () -Methode in der abstrakten Ticketklasse. Dies verstößt jedoch gegen das Open-Closed-Prinzip.
  2. Überschreiben Sie die PrintTicket () -Methode in untergeordneten Klassen. Dadurch wird jedoch die Drucklogik dupliziert, die gegen das DRY-Prinzip (Wiederholen Sie sich nicht) verstößt.

Fragen sind also

  1. Wie kann ich die oben genannten Geschäftsanforderungen erfüllen, ohne das Open / Closed-Prinzip zu verletzen?
  2. Wann soll die Klasse wegen Änderung geschlossen werden? Nach welchen Kriterien ist die Klasse wegen Änderung geschlossen? Ist es nach der ersten Implementierung der Klasse oder kann nach der ersten Bereitstellung in der Produktion sein oder kann etwas anderes sein.
parag
quelle
1
Ich denke, das Problem hier ist, dass Ihre Ticketklasse nicht wissen sollte, wie sie sich selbst drucken soll. Eine TicketPrinter-Klasse sollte ein Ticket erhalten und wissen, wie man es druckt. In diesem Fall können Sie das Dekotatormuster verwenden, um Ihre Druckerklasse zu erhöhen oder sie zu überschreiben, um sie vollständig zu ändern.
1
Decorator wird auch mit dem gleichen Open / Closed-Problem konfrontiert sein. Die Anforderungen können sich in Zukunft um ein Vielfaches ändern. In diesem Fall wechseln Sie den Dekorator oder erweitern den Dekorator. Es scheint nicht praktisch zu sein, den Dekorateur jedes Mal zu erweitern, und das Ändern des Dekorators verstößt gegen das Open / Closed-Prinzip.
Parag
1
Es ist sehr schwierig, mit OCP für willkürliche Änderungen im Design zu entwerfen. Es ist ein Glücksspiel, die Dimension auszuwählen, von der Sie erwarten, dass sie in Bezug auf den stabilen Teil variiert.
Fuhrmanator

Antworten:

5

Ich habe folgende Möglichkeiten

  • Ändern Sie die PrintTicket()Methode in der abstrakten Ticketklasse. Dies verstößt jedoch gegen das Open-Closed-Prinzip.
  • Überschreiben Sie die PrintTicket()Methode in untergeordneten Klassen, aber dies dupliziert die Drucklogik, die gegen das DRY-Prinzip (Wiederholen Sie sich nicht) verstößt.

Dies hängt von der Art und Weise ab, wie das PrintTicketimplementiert wird. Wenn die Methode Informationen aus Unterklassen berücksichtigen muss, muss sie eine Möglichkeit bieten, zusätzliche Informationen bereitzustellen.

Darüber hinaus können Sie überschreiben, ohne Ihren Code zu wiederholen. Wenn Sie beispielsweise Ihre Basisklassenmethode aus der Implementierung aufrufen, vermeiden Sie Wiederholungen:

class ConcessionTicket : Ticket {
    public override string PrintTicket() {
        return $"{base.PrintTicket()} (concession)"
    }
}

Wie kann ich die oben genannten Geschäftsanforderungen erfüllen, ohne das Open / Closed-Prinzip zu verletzen?

Das Muster der Vorlagenmethode bietet eine dritte Option: Implementieren Sie esPrintTicketin der Basisklasse und verlassen Sie sich auf abgeleitete Klassen, um bei Bedarf zusätzliche Details bereitzustellen.

Hier ist ein Beispiel mit Ihrer Klassenhierarchie:

abstract class Ticket {
    public string Name {get;}
    public string Train {get;}
    protected virtual string AdditionalDetails() {
        return "";
    }
    public string PrintTicket() {
        return $"{Name} : {Train}{AdditionalDetails()}";
    }
}

class RegularTicket : Ticket {
    ... // Uses the default implementation of AdditionalDetails()
}


class ConcessionTicket : Ticket {
    protected override string AdditionalDetails() {
        return " (concession)";
    }
}

Nach welchen Kriterien wird berücksichtigt, dass die Klasse wegen Änderung geschlossen ist?

Es ist nicht die Klasse, die für Änderungen geschlossen werden sollte, sondern die Schnittstelle dieser Klasse (ich meine "Schnittstelle" im weiteren Sinne, dh eine Sammlung von Methoden und Eigenschaften und deren Verhalten, nicht das Sprachkonstrukt). Die Klasse verbirgt ihre Implementierung, sodass die Eigentümer der Klasse sie jederzeit ändern können, solange ihr extern sichtbares Verhalten unverändert bleibt.

Ist es nach der ersten Implementierung der Klasse oder nach der ersten Bereitstellung in der Produktion oder etwas anderes?

Die Schnittstelle der Klasse muss nach dem ersten Veröffentlichen für die externe Verwendung geschlossen bleiben. Interne Verwendungsklassen bleiben für immer für Refactoring offen, da Sie alle Verwendungen finden und korrigieren können.

Abgesehen von den einfachsten Fällen ist es nicht praktikabel zu erwarten, dass Ihre Klassenhierarchie nach einer bestimmten Anzahl von Refactoring-Iterationen alle möglichen Verwendungsszenarien abdeckt. Zusätzliche Anforderungen, die völlig neue Methoden in der Basisklasse erfordern, werden regelmäßig gestellt, sodass Ihre Klasse für immer für Änderungen durch Sie offen bleibt.

dasblinkenlight
quelle
Ja, in diesem speziellen Fall funktioniert das Templating. Dies ist jedoch nur ein Beispiel für das Problem. Es kann viele Szenarien / Geschäftsprobleme geben, in denen die Vorlagen- / Plugin-Methode möglicherweise nicht anwendbar ist.
Parag
1
Ihr Punkt auf der Klassenschnittstelle ist meiner Meinung nach die beste Lösung für dieses spezielle Szenario. Um es auf die spezifische Frage zu konzentrieren: Man könnte eine PrintTicket-Methode mit einer bestimmten Ausgabe haben, die in der Schnittstelle für die Ticketklasse definiert ist (zusammen mit definierten Eingaben). Solange die Ticketausgabe immer dieselbe ist, spielt es keine Rolle, wie die zugrunde liegende Klassen geändert. - Auch wenn sich die Eigenschaften eines Tickets immer ändern sollen, können Sie Ihre Klasse als bekannte Geschäftsanforderung gestalten. Ticket sollte eine Sammlung eines ticketProperty-Objekts irgendeiner Art enthalten
user3908435
Könnte auch eine TicketPrinterKlasse erstellen, die an den Konstruktor übergeben wird.
Zymus
2

Beginnen wir mit einer einfachen Linie des Open-Closed-Prinzips "Open for extension Closed for modification". Betrachten Sie Ihr Problem. Ich würde die Klassen so gestalten, dass das Basisticket für das Drucken gemeinsamer Ticketinformationen verantwortlich ist und andere Ticketarten für das Drucken ihres eigenen Formats über dem Basisdruck verantwortlich sind.

abstract class Ticket
{
    public string Print()
    {
        // Print base properties here. and return 
        return PrintBasicInfo() + AddionalPrint();
    }

    protected abstract string AddionalPrint();

    private string PrintBasicInfo()
    {
        // print base ticket info
    }
}

class ConcessionTicket : Ticket
{
    protected override string AddionalPrint()
    {
        // Specific to Conecession ticket printing
    }
}

class RegularTicket : Ticket
{
    protected override string AddionalPrint()
    {
        // Specific to Regular ticket printing
    }
}

Auf diese Weise erzwingen Sie jede neue Art von Ticket, die in das System eingeführt wird, und implementieren zusätzlich zu den grundlegenden Informationen zum Drucken von Tickets eine eigene Druckfunktion. Das Basisticket ist jetzt zur Änderung geschlossen, kann jedoch erweitert werden, indem zusätzliche Methoden für die abgeleiteten Typen bereitgestellt werden.

vendettamit
quelle
2

Das Problem ist nicht, dass Sie gegen das Open / Closed-Prinzip verstoßen, sondern dass Sie gegen das Prinzip der Einzelverantwortung verstoßen.

Dies ist buchstäblich ein Schulbuchbeispiel für ein SRP-Problem oder wie in Wikipedia angegeben :

Stellen Sie sich als Beispiel ein Modul vor, das einen Bericht kompiliert und druckt. Stellen Sie sich vor, ein solches Modul kann aus zwei Gründen geändert werden. Erstens könnte sich der Inhalt des Berichts ändern. Zweitens könnte sich das Format des Berichts ändern. Diese beiden Dinge ändern sich aus sehr unterschiedlichen Gründen; eine inhaltliche und eine kosmetische. Das Prinzip der Einzelverantwortung besagt, dass diese beiden Aspekte des Problems tatsächlich zwei getrennte Verantwortlichkeiten sind und daher in getrennten Klassen oder Modulen liegen sollten. Es wäre ein schlechtes Design, zwei Dinge zu koppeln, die sich aus unterschiedlichen Gründen zu unterschiedlichen Zeiten ändern.

Die verschiedenen Ticketklassen können sich ändern, da Sie ihnen neue Informationen hinzufügen. Das Drucken des Tickets kann sich ändern, da Sie ein neues Layout benötigen. Daher sollten Sie über eine TicketPrinter- Klasse verfügen , die Tickets als Eingabe akzeptiert.

Ihre Ticketklassen können dann verschiedene Schnittstellen implementieren, um verschiedene Datentypen oder eine Art Datenvorlage bereitzustellen.

Björn
quelle
1

Ändern Sie die PrintTicket () -Methode in der abstrakten Ticketklasse. Dies verstößt jedoch gegen das Open-Closed-Prinzip.

Dies verstößt nicht gegen das Open-Closed-Prinzip. Der Code ändert sich ständig. Deshalb ist SOLID wichtig, damit der Code wartbar, flexibel und änderbar bleibt. Es ist nichts Falsches daran, Code zu ändern.


Bei OCP geht es mehr darum, dass externe Klassen die beabsichtigte Funktionalität einer Klasse nicht manipulieren können.

Zum Beispiel habe ich eine Klasse A, die einen String im Konstruktor akzeptiert und überprüft, ob dieser String ein bestimmtes Format hat, indem eine Methode zum Überprüfen des Strings aufgerufen wird. Diese Überprüfungsmethode muss auch von Clients verwendet werden können A, damit sie in der naiven Implementierung ausgeführt wird public.

Jetzt kann ich diese Klasse "brechen", indem ich von ihr erbe und die Überprüfungsmethode überschreibe, um einfach immer zurückzukehren true. Aufgrund des Polymorphismus kann ich meine Unterklasse immer noch überall dort verwenden A, wo ich sie verwenden kann , wodurch möglicherweise unerwünschtes Verhalten in meinem Programm entsteht.

Dies scheint eine böswillige Sache zu sein, aber in einer komplexeren Codebasis könnte dies als "ehrlicher Fehler" geschehen. Um der OCP zu entsprechen, Asollte die Klasse so gestaltet sein, dass dieser Fehler nicht möglich ist.

Ich könnte dies tun, indem ich zum Beispiel die Überprüfungsmethode mache final.

Jorn Vernee
quelle
Ich habe noch nie eine Beschreibung von OCP gesehen, die besagt, dass dies eine Verletzung ist. LSP definitiv, aber nicht OCP.
Jules
@ Jules Ich fand später heraus, dass die Wikipedia-Definition in der Tat sehr unterschiedlich ist. Dies war eines der Beispiele, die verwendet wurden, um mir OCP beizubringen (obwohl es etwas anders gewesen sein könnte). Der Punkt ist, es ging nicht darum, sich zu verbieten, Ihren Code zu ändern. Das macht für mich auch keinen Sinn. Wenn Sie Code schreiben und sich die Umgebung so ändert, dass Ihr Code nicht mehr passt, ändern Sie ihn oder werfen Sie ihn besser weg und beginnen Sie erneut. Warum etwas behalten, das kaputt ist ...
Jorn Vernee
0

Vererbung ist nicht der einzige Mechanismus, der verwendet werden kann, um die Anforderungen von OCP zu erfüllen, und in den meisten Fällen würde ich argumentieren, dass es selten der beste ist - es gibt normalerweise bessere Möglichkeiten, solange Sie ein wenig im Voraus planen, dies zu berücksichtigen Art von Änderungen, die Sie wahrscheinlich benötigen.

In diesem Fall würde ich argumentieren, dass die Verwendung eines Template-Systems sehr sinnvoll wäre, wenn Sie häufige Formatänderungen an Ihrem Ticketdrucksystem erwarten (und das scheint mir eine ziemlich sichere Sache zu sein). Jetzt müssen wir den Code überhaupt nicht mehr berühren: Alles, was wir tun müssen, um diese Anforderung zu erfüllen, ist eine Vorlage zu ändern (solange Ihr Vorlagensystem sowieso auf alle erforderlichen Daten zugreifen kann).

In der Realität kann ein System niemals vollständig gegen Änderungen geschlossen werden, und selbst das Schließen würde eine enorme Verschwendung erfordern. Sie müssen also beurteilen, welche Art von Änderungen wahrscheinlich sind, und Ihren Code entsprechend strukturieren.

Jules
quelle