MSDN Magazin > Home > Ausgaben > 2008 > Juni >  Muster in der Praxis: Das Offen-Geschlossen-Pri...
Muster in der Praxis
Das Offen-Geschlossen-Prinzip
Jeremy Miller

Dies ist die erste Ausgabe einer neuen MSDN® Magazin-Rubrik zu den Grundlagen des Softwareentwurfs. Mein Ziel ist es, Entwurfsmuster und -prinzipien so zu diskutieren, dass sie nicht an ein spezielles Tool oder eine bestimmte Lebenszyklusmethodik gebunden sind. Anders gesagt, besteht mein Plan darin, über das Grundlagenwissen zu sprechen, dass Sie bei jeder Technologie und jedem Projekt zu besseren Entwürfen führen kann.
Ich möchte mit einer Besprechung des Offen-Geschlossen-Prinzips (Open Closed Principle) und weiterer damit zusammenhängender Ideen beginnen, die von Robert C. Martin in seinem Buch „Agile Software Development, Principles, Patterns, and Practices“ vorgestellt werden. Lassen Sie sich nicht von dem Wort „Agile“ im Titel abschrecken. Es geht einfach um das Streben nach guten Softwareentwürfen.
Fragen Sie sich Folgendes: Wie oft beginnen Sie, eine Anwendung von Grund auf neu zu schreiben, und wie oft beginnen Sie dagegen damit, einer vorhandenen Codebasis neue Funktionalität hinzuzufügen? Die Chancen stehen gut, dass Sie weit mehr Zeit damit verbringen, einer vorhandenen Codebasis neue Features hinzuzufügen.
Stellen Sie sich eine weitere Frage: Ist es einfacher, ganz neuen Code zu schreiben oder Änderungen an vorhandenem Code vorzunehmen? Es ist meist weit einfacher für mich, ganz neue Methoden und Klassen zu schreiben, als mich in alten Code zu vertiefen und die zu ändernden Abschnitte zu suchen. Das Ändern alten Codes birgt die Gefahr, vorhandene Funktionalität zu beschädigen. Bei neuem Code müssen Sie meist nur die neue Funktionalität testen. Wenn Sie alten Code ändern, müssen Sie beides tun: Ihre Änderungen testen und dann eine Reihe von Regressionstests durchführen, um sicherzustellen, dass kein vorhandener Code beschädigt wurde.
Sie arbeiten also meist mit einer vorhandenen Codebasis, und doch ist es einfacher, ganz neuen Code zu schreiben als alten Code zu ändern. Würden Sie sich nicht wünschen, dass das Erweitern einer vorhandenen Codebasis ebenso produktiv und unproblematisch wie das Schreiben ganz neuen Codes ist? Dies ist, wo das Offen-Geschlossen-Prinzip ins Spiel kommt. Das Offen-Geschlossen-Prinzip besagt in etwa Folgendes: Softwareentitäten sollten offen für Erweiterungen, aber geschlossen für Änderungen sein.
Es klingt wie ein begrifflicher Widerspruch, aber es ist keiner. Es bedeutet einfach, dass Sie eine Anwendung so strukturieren sollten, dass Sie neue Funktionalität mit minimalen Änderungen an vorhandenem Code hinzufügen können. Ich dachte früher, dass das Offen-Geschlossen-Prinzip einfach das Verwenden von Plug-Ins meint, aber das ist zu kurz gegriffen.
Was Sie vermeiden möchten, ist, dass sich eine einfache Änderung auf all die verschiedenen Klassen Ihrer Anwendung auswirkt. Das würde das System anfällig für Regressionsprobleme und die Erweiterung teuer machen. Um die Änderungen zu isolieren, möchten Sie Klassen und Methoden so schreiben, dass sie, nachdem sie einmal geschrieben wurden, nie wieder geändert werden müssen.
Wie würden Sie Code strukturieren, um Änderungen zu isolieren? Ich würde sagen, dass der erste Schritt darin besteht, dem Prinzip der einzigen Verantwortung (Single Responsibility Principle) zu folgen.

Das Prinzip der einzigen Verantwortung
Durch Befolgen des Offen-Geschlossen-Prinzips möchte ich in die Lage versetzt werden, Klassen oder Methoden zu schreiben und mich dann nie wieder um sie kümmern zu müssen in der Gewissheit, dass sie ihren Zweck erfüllen und nicht später geändert werden müssen. Sie werden nie das vollkommene Offen-Geschlossen-Nirwana erreichen, aber Sie können ihm näher kommen, indem Sie streng das verwandte Prinzip der einzigen Verantwortung befolgen: Es sollte einen und nur einen Grund geben, eine Klasse zu ändern.
Eine der einfachsten Möglichkeiten, Klassen zu schreiben, die nie geändert werden müssen, besteht darin, Klassen zu schreiben, die nur einem Zweck dienen. Dann muss eine Klasse nur dann geändert werden, wenn sich genau das, was die Klasse tut, ändert. Abbildung 1 zeigt ein Beispiel, das nicht dem Prinzip der einzigen Verantwortung folgt. Ich bezweifle ernsthaft, dass Sie ein System so entwerfen würden, aber es ist gut, sich zu erinnern, warum wir Code nicht so strukturieren.
public class OrderProcessingModule {
  public void Process(OrderStatusMessage orderStatusMessage) {
    // Get the connection string from configuration
    string connectionString = 
      ConfigurationManager.ConnectionStrings["Main"]
      .ConnectionString;

    Order order = null;

    using (SqlConnection connection = 
      new SqlConnection(connectionString)) {
      // go get some data from the database
      order = fetchData(orderStatusMessage, connection);
    }

    // Apply the changes to the Order from the OrderStatusMessage
    updateTheOrder(order);

    // International orders have a unique set of business rules
    if (order.IsInternational) {
      processInternationalOrder(order);
    }

    // We need to treat larger orders in a special manner
    else if (order.LineItems.Count > 10) {
      processLargeDomesticOrder(order);
    }

    // Smaller domestic orders
    else {
      processRegularDomesticOrder(order);
    }

    // Ship the order if it's ready
    if (order.IsReadyToShip()) {
      ShippingGateway gateway = new ShippingGateway();

      // Transform the Order object into a Shipment 
      ShipmentMessage message = 
        createShipmentMessageForOrder(order);
      gateway.SendShipment(message);
  } 
}
OrderProcessingModule ist ziemlich beschäftigt. Es betreibt Datenzugriff, sammelt Konfigurationsdateiinformationen, folgt Geschäftsregeln zur Auftragsverarbeitung (was wahrscheinlich allein schon sehr kompliziert ist) und wandelt abgeschlossene Aufträge in Lieferungen um. Wenn Sie OrderProcessingModule so erstellen würden, müssten Sie wahrscheinlich ständig Änderungen an diesem Code vornehmen. Viele Änderungen der Systemanforderungen würden viel zu viele Änderungen am OrderProcessingModule-Code zur Folge haben, was das System unzuverlässig und Änderungen teuer macht.
Statt ein einziges großes Codeobjekt zu erstellen, sollten Sie dem Prinzip der einzigen Verantwortung folgen und die gesamte OrderProcessingModule-Klasse in ein Subsystem verwandter Klassen aufteilen, bei dem jede Klasse ihre eigene spezielle Verantwortung trägt. Zum Beispiel könnten Sie alle Datenzugriffsfunktionen in eine neue Klasse namens „OrderDataService“ und die Auftragsgeschäftslogik in eine weitere Klasse auslagern. (Ich werde darauf im nächsten Abschnitt ausführlicher eingehen.)
Was das Offen-Geschlossen-Prinzip betrifft, so sollte das Aufteilen von Geschäftslogik- und Datenzugriffsfunktionen in separate Klassen Ihnen ermöglichen, eine der beiden Funktionen zu ändern, ohne die andere zu beeinträchtigen. Eine Änderung der physischen Datenbankbereitstellung könnte Sie veranlassen, den Datenzugriff durch etwas ganz Anderes zu ersetzen (offen für Erweiterung), während die Auftragslogikklassen gänzlich unberührt bleiben (für Änderungen geschlossen).
Das Prinzip der einzigen Verantwortung besteht nicht nur darin, kleinere Klassen und Methoden zu schreiben. Es geht darum, dass jede Klasse einen zusammenhängenden Satz verwandter Funktionen implementieren sollte. Eine einfache Möglichkeit, dem Prinzip der einzigen Verantwortung zu folgen, besteht darin, sich immer wieder zu fragen, ob jede Methode und jeder Vorgang einer Klasse in direktem Zusammenhang mit dem Namen dieser Klasse steht. Wenn Sie einige Methoden finden, die nicht zum Namen der Klasse passen, sollten Sie erwägen, diese Methoden in andere Klassen zu verschieben.

Das Muster der Verantwortungskette
Geschäftsregeln unterliegen im Lebenszyklus einer Codebasis wahrscheinlich mehr Änderungen als jeder andere Teil des Systems. In der OrderProcessingModule-Klasse gab es ziemlich viel Verzweigungslogik für die Auftragsverarbeitung basierend auf der Art des eingegangenen Auftrags:
if (order.IsInternational) {
  processInternationalOrder(order);
}

else if (order.LineItems.Count > 10) {
  processLargeDomesticOrder(order);
}

else {
  processRegularDomesticOrder(order);
}
Es ist sehr wahrscheinlich, dass, während das Geschäft wächst, ein reales Auftragsverarbeitungssystem ebenfalls wachsen würde, um mehr Auftragstypen zu umfassen, und dass es viele spezielle Ausnahmefälle gibt, wie z. B. Aufträge für die Regierung und bevorzugte Kunden sowie wöchentliche Sonderangebote. Es wäre sehr vorteilhaft, wenn Sie neue Auftragsbehandlungslogik schreiben und testen könnten, ohne dass die Gefahr besteht, eine der vorhandenen Geschäftsregeln zu beschädigen.
Zu diesem Zweck können Sie beim Auftragsverarbeitungsbeispiel das Offen-Geschlossen-Prinzip stärker berücksichtigen, indem Sie eine Art des Verantwortungskettenmusters verwenden (siehe Abbildung 2). Das Erste, was ich getan habe, war, jede bedingte Verzweigung in der ursprünglichen OrderProcessingModule-Klasse in eine separate Klasse auszulagern, die die IOrderHandler-Schnittstelle implementiert:
public interface IOrderHandler {
  void ProcessOrder(Order order);
  bool CanProcess(Order order);
}
public class OrderProcessingModule {
  private IOrderHandler[] _handlers;

  public OrderProcessingModule() {
    _handlers = new IOrderHandler[] {
                new InternationalOrderHandler(),
                new SmallDomesticOrderHandler(),
                new LargeDomesticOrderHandler(),
    };
  }

  public void Process (OrderStatusMessage orderStatusMessage, 
    Order order) {
    // Apply the changes to the Order from the OrderStatusMessage
    updateTheOrder(order);

    // Find the first IOrderHandler that "knows" how
    // to process this Order
    IOrderHandler handler = 
      Array.Find(_handlers, h => h.CanProcess(order));

    handler.ProcessOrder(order);
  }

  private void updateTheOrder(Order order) {
  }
}   
Ich würde dann eine separate Implementierung von IOrderHandler für jeden Auftragstyp schreiben, einschließlich der Logik, die im Wesentlichen sagt: „Ich weiß, was ich mit dieser Art von Auftrag zu tun habe. Lass mich ihn verarbeiten.“
Nun, da die Geschäftslogik für jede Art von Auftrag in einer separaten Handlerklasse isoliert ist, können Sie die Geschäftsregeln für eine Art von Auftrag ändern, ohne befürchten zu müssen, dass Sie die Regeln für eine andere Art von Auftrag beschädigen. Sie können sogar mit minimalen Änderungen an vorhandenem Code vollkommen neue Arten der Auftragsverarbeitung hinzufügen.
Angenommen, ich muss z. B. zu einem späteren Zeitpunkt Unterstützung für Regierungsaufträge im System hinzufügen. Mit dem Muster der Verantwortungskette kann ich eine vollkommen neue Klasse namens „GovernmentOrderHandler“ schreiben, die die IOrderHandler-Schnittstelle implementiert. Nachdem ich geprüft habe, ob GovernmentOrderHandler wie vorgesehen funktioniert, kann ich die neuen Regeln zur Verarbeitung von Regierungsaufträgen hinzufügen, indem ich eine einzige Zeile der Konstruktorfunktion von OrderProcessingModule ändere:
public OrderProcessingModule() {
  _handlers = new IOrderHandler[] {
              new InternationalOrderHandler(),
              new SmallDomesticOrderHandler(),
              new LargeDomesticOrderHandler(),
              new GovernmentOrderHandler(),
  };
}
Durch Befolgen des Offen-Geschlossen-Prinzips bei den Auftragsverarbeitungsregeln habe ich das Hinzufügen neuer Arten der Auftragsbehandlungslogik stark vereinfacht. Ich konnte die Regierungsauftragsregeln so hinzufügen, dass die Gefahr, andere Arten von Aufträgen zu destabilisieren, viel geringer war, als wenn ich eine einzige Klasse alle verschiedenen Arten der Auftragsverarbeitung hätte implementieren lassen.

Double Dispatch
Was ist, wenn die Schritte in Zukunft komplizierter werden? Was ist, wenn reine Polymorphie nicht ganz ausreicht, um alle möglichen zukünftigen Variationen zuzulassen? Wir können ein Muster namens „Double Dispatch“ verwenden, um die Variation so in die Unterklassen auszulagern, dass wir keine vorhandenen Schnittstellenverträge beschädigen.
Angenommen, ich erstelle eine zusammengesetzte Desktopanwendung, die immer einen Bildschirm auf einmal in einer Art von Hauptbereich anzeigt. Jedes Mal, wenn ich einen neuen Bildschirm in der Anwendung öffne, muss ich einiges tun. Ich muss vielleicht die verfügbaren Menüs ändern, den Status der bereits geöffneten Bildschirme prüfen, eine Reihe von Schritten ausführen, um die Anzeige des gesamten Bildschirms anzupassen, und natürlich irgendwie den neuen Bildschirm anzeigen.
Ich verwende für meine Desktopclientarchitekturen in der Regel einige der vielfältigen Optionen des MVP-Musters (Model View Presenter), und ich verwende gewöhnlich das Application Controller-Muster, um die verschiedenen MVP-Triaden in der Anwendung zu koordinieren. Bei der Verwendung von MVP mit einem Application Controller (weitere Informationen zu MVP finden Sie in der MSDN Magazin-Rubrik „Entwurfsmuster“ von Jean Paul Boodhoo unter msdn2.microsoft.com/magazine/cc188690) könnte die Bildschirmaktivierung folgende drei Grundbestandteile beinhalten:
  1. Einen Presenter für einen einzelnen Bildschirm. Jeder Presenter weiß alles, was es über einen bestimmten Bildschirm zu wissen gibt.
  2. Eine ApplicationShell für das Hauptformular der Anwendung. Die ApplicationShell ist verantwortlich für das Anzeigen einzelner Ansichten innerhalb einer Art von Panel oder TabControl. Die ApplicationShell enthält auch sämtliche Menüs.
  3. Einen ApplicationController, der als „Datenverkehrspolizist“ der Anwendung fungiert. Der ApplicationController weiß Bescheid über die ApplicationShell und jeden Presenter, der die Anwendung durchläuft. Der ApplicationController steuert die Bildschirmaktivierung und die Deaktivierungslebenszyklen.
Wenn ich bei der Aktivierung einfach nur View in der ApplicationShell zeigen muss, könnte der Code wie in Abbildung 3 dargestellt aussehen. Dies funktioniert tadellos für eine einfache Anwendung, aber was ist, wenn die Anwendung komplizierter wird? Was ist, wenn bei der zweiten Version die Notwendigkeit besteht, der Hauptshell Menüelemente hinzuzufügen, wenn einige Bildschirme aktiv sind? Was ist, wenn ich für einige Ansichten (aber nicht für alle) zusätzliche Steuerelemente in einem neuen Fensterbereich entlang dem linken Rand des Hauptbildschirms zeigen möchte?
public interface IApplicationShell {
  void DisplayMainView(object view);
}

public interface IPresenter {
  // Just exposes a getter for the inner WinForms UserControl or Form
  object View { get; }
}

public class ApplicationController {
  private IApplicationShell _shell;

  public ApplicationController(IApplicationShell shell) {
    _shell = shell;
  }

  public void ActivateScreen(IPresenter presenter) {
    teardownCurrentScreen();

    // Setup the new screen
    _shell.DisplayMainView(presenter.View);
  }

  private void teardownCurrentScreen() {
    // teardown the existing screen
  }
}
Ich will immer noch, dass die Architektur erweiterbar ist, damit ich der Anwendung neue Bildschirme hinzufügen kann, indem ich einfach neue Presenter hinzufüge. Deshalb sollten die Informationen über diese Konstrukte für neue Menüs und den linken Fensterbereich in die vorhandene Presenter-Abstraktion eingehen. Ich müsste dann die ApplicationShell oder den ApplicationController so ändern, dass sie auf die neuen Menüelemente und die zusätzlichen Steuerelemente im linken Fensterbereich reagieren.
Abbildung 4 zeigt eine mögliche Lösung. Ich habe der IPresenter-Schnittstelle neue Eigenschaften hinzugefügt, um die neuen Menüelemente sowie zusätzliche Steuerelemente, die dem neuen linken Fensterbereich hinzugefügt werden könnten, zu modellieren. Ich habe für diese neuen Konzepte auch einige neue Member zu IApplicationShell hinzugefügt. Dann habe ich der ApplicationController.ActivateScreen(IPresenter)-Methode neuen Code hinzugefügt.
public class MenuCommand
{
	// ...
}
public interface IApplicationShell
{
	void DisplayMainView(object view);
	
	// New behavior
	void AddMenuCommands(MenuCommand[] commands);
	void DisplayInExplorerPane(object paneView);
}
public interface IPresenter
{
	object View { get; }

	// New properties
	MenuCommand[] Commands{ get; }
	object[] ExplorerViews { get; }
}
public class ApplicationController
	{
	private IApplicationShell _shell;
		
	public ApplicationController(IApplicationShell shell)
		{	
		_shell = shell;
		}

	public void ActivateScreen(IPresenter presenter)
	{
		teardownCurrentScreen();
		
		// Setup the new screen
		_shell.DisplayMainView(presenter.View);

		// New code
		_shell.AddMenuCommands(presenter.Commands);
		foreach (var explorerView in presenter.ExplorerViews)
		{
		_shell.DisplayInExplorerPane(explorerView);
		}
	}

	private void teardownCurrentScreen()
	{
		// teardown the existing screen
	}
}
Entspricht diese Lösung dem Offen-Geschlossen-Prinzip? Nicht im Geringsten. Zuerst musste ich die IPresenter-Schnittstelle ändern. Da es eine Schnittstelle ist, hätte ich jeden Implementierer der IPresenter-Schnittstelle in meiner Codebasis finden und leere Implementierungen dieser neuen Methoden hinzufügen müssen, damit mein Code wieder kompilierbar wäre. Das ist oft eine unmögliche Änderung, v. a. wenn sich irgendeiner dieser IPresenter-Implementierer außerhalb Ihrer unmittelbaren Kontrolle befindet. Mehr dazu später.
Ich müsste auch die ApplicationController-Klasse ändern, damit sie über alle neuen Arten von Anpassungen informiert ist, die jeder Bildschirm in der Haupt-ApplicationShell benötigen könnte. Abschließend müsste ich ApplicationShell ändern, um die neuen Shellanpassungen zu unterstützen. Die Änderungen wären gering, aber andererseits ist es nicht unwahrscheinlich, dass ich später noch weitere Bildschirmanpassungen hinzufügen möchte.
In einer realen Anwendung kann die ApplicationController-Klasse kompliziert genug sein, selbst wenn keine zusätzliche Verantwortung für das Konfigurieren von ApplicationShell zu tragen wäre. Es wäre gut, wenn wir diese Verantwortung bei jedem Presenter belassen könnten.
Die Änderungen an jeder IPresenter-Implementierung könnten verringert werden, indem eine abstrakte Klasse namens „Presenter“ statt einer Schnittstelle verwendet wird. Ich könnte der abstrakten Klasse einfach Standardimplementierungen hinzufügen (siehe Abbildung 5). Ich müsste keine der vorhandenen Presenter-Implementierungen ändern, um das neue Verhalten hinzuzufügen.
public abstract class BasePresenter
{
    public abstract object View { get;}

    // Default implementation of Commands
    public virtual MenuCommand[] Commands
    {
         get
         {
             return new MenuCommand[0];
         }
    }

    // Default ExplorerViews
    public virtual object[] ExplorerViews
    {
         get
         {
             return new object[0];
         }
    }
}
Es gibt eine andere, letztlich sauberere Möglichkeit, dem Ideal des Offen-Geschlossen-Prinzips näher zu kommen. Statt in der IPresenter- oder BasePresenter-Abstraktion Getter zu platzieren, könnte ich das Double Dispatch-Muster verwenden.
Ich wurde vor kurzem im wirklichen Leben mit einer unerwarteten Vorführung des Double Dispatch-Musters konfrontiert. Mein Team war gerade in ein neues Büro umgezogen, und wir hatten mit Netzwerkproblemen zu kämpfen. Unser Netzwerkguru rief mich letzte Woche an und begann mir zu erklären, was mein Mitarbeiter tun sollte, um eine Verbindung zum VPN herzustellen. Er ratterte ein Netzwerkkauderwelsch herunter, das ich nicht verstand, sodass ich schließlich den Telefonhörer einfach meinem Mitarbeiter reichte, um die beiden direkt miteinander reden zu lassen.
Lassen Sie uns das Gleiche jetzt für den ApplicationController tun. Statt dass der ApplicationController jeden Presenter fragt, was in der ApplicationShell angezeigt werden muss, überspringt der Presenter einfach den Vermittler und teilt der ApplicationShell mit, was sie für jeden Bildschirm tun muss (siehe Abbildung 6).
public interface IPresenter {
  void SetupView(IApplicationShell shell);
}

public class ApplicationController {
  private IApplicationShell _shell;

  public ApplicationController(IApplicationShell shell) {
    _shell = shell;
  }

  public void ActivateScreen(IPresenter presenter) {
    teardownCurrentScreen();

    // Set up the new screen using Double Dispatch
    presenter.SetupView(_shell);
  }

  private void teardownCurrentScreen() {
    // tear down the existing screen
  }
} 
Ich müsste ApplicationShell für das neue Menü und die Steuerelemente im linken Fensterbereich ändern, unabhängig davon, was ich zuerst getan habe. Hätte ich aber mit der Double Dispatch-Strategie begonnen, hätte ich die neuen Änderungen mit weniger Modifikationen am ApplicationController und jedem der Presenter vornehmen können. Ich muss nicht mehr den ApplicationController oder die Presenter-Klassen ändern, um zusätzliche Bildschirmkonzepte zu erstellen. Die Architektur ist offen für die Erweiterung durch neue Shellkonzepte, aber der ApplicationController und die einzelnen Presenter-Klassen sind für Modifikationen geschlossen.

Das Liskovsche Substitutionsprinzip
Wie ich bereits gesagt habe, besteht die häufigste Anwendung des Offen-Geschlossen-Prinzips in der Verwendung einer Polymorphie zur Ersetzung eines vorhandenen Teils der Anwendung durch eine vollkommen neue Klasse. Angenommen, Sie haben am Anfang eine Klasse namens „BusinessProcess“, deren Aufgabe darin besteht, einen Geschäftsprozess auszuführen. Dabei muss sie Daten aus einer Datenquelle abrufen:
public class BusinessProcess {
  private IDataSource _source;

  public BusinessProcess(IDataSource source) {
    _source = source;
  }
}
public interface IDataSource {
  Entity FindEntity(long key);
}
Der Entwurf folgt dem Offen-Geschlossen-Prinzip, wenn Sie das System erweitern können, indem Sie Implementierungen von IDataSource austauschen, ohne Änderungen an der BusinessProcess-Klasse vorzunehmen. Sie könnten mit einer einfachen, auf XML-Dateien basierenden Methode beginnen, dann zur Speicherung mittels einer Datenbank übergehen und schließlich eine Art des Zwischenspeicherns verwenden, aber Sie möchten immer noch nicht die BusinessProcess-Klasse ändern. All das ist möglich, aber nur, wenn Sie ein verwandtes Prinzip befolgen: das Liskovsche Substitutionsprinzip.
Grob gesagt, folgen Sie dem Liskovschen Substitutionsprinzip, wenn Sie jede Implementierung einer Abstraktion an jeder Stelle verwenden können, wo diese Abstraktion akzeptiert wird. BusinessProcess sollte jede Implementierung von IDataSource ohne Änderung verwenden können. BusinessProcess sollte nichts über die internen Details von IDataSource wissen, bis auf das, was über die öffentliche Schnittstelle mitgeteilt wird.
Um es deutlicher zu machen, zeigt Abbildung 7 ein Beispiel, das gegen das Liskovsche Substitutionsprinzip verstößt. Diese Version der BusinessProcess-Klasse verfügt über spezielle Logik, um eine FileSource zu starten, und muss auch mit spezieller Fehlerbehandlungslogik für die DatabaseSource-Klasse vertraut sein. Die Implementierer von IDataSource werden so erstellt, dass sie alle ihre spezifischen Infrastrukturanforderungen behandeln können. Dadurch wird das Schreiben der BusinessProcess-Klasse ermöglicht (Abbildung 8).
public class BusinessProcess {
  private IDataSource _source;

  public BusinessProcess(IDataSource source) {
    _source = source; 
  }

  public void Process() {
    long theKey = 112;

    // Special code if we're using a FileSource
    if (_source is FileSource)  {
      ((FileSource)_source).LoadFile();
    }

    try {
      Entity entity = _source.FindEntity(theKey);
    }
    catch (System.Data.DataException) {
      // Special exception handling for the DatabaseSource,
      // This is an example of "Downcasting"
      ((DatabaseSource)_source).CleanUpTheConnection(); 
    }
  }
}
public class BusinessProcess {
  private readonly IDataSource _source;

  public BusinessProcess(IDataSource source) {
    _source = source;
  }

  public void Process(Message message) {
    // the first part of the Process() method

    // There is NO code specific to any implementation of     // IDataSource here
    Entity entity = _source.FindEntity(message.Key);

    // the last part of the Process() method
  }
}

Zusammenfassung
Erinnern Sie sich einfach daran, dass das Offen-Geschlossen-Prinzip nur dann durch Polymorphie realisiert wird, wenn eine Klasse nur vom öffentlichen Vertrag der anderen Klassen abhängt, mit denen sie interagiert. Wenn eine Klasse, die eine Abstraktion verwendet, in einem Abschnitt eine Umwandlung in eine spezifische Unterklasse durchführen muss, wird gegen das Offen-Geschlossen-Prinzip verstoßen.
Wenn eine Klasse, die eine andere Klasse verwendet, Informationen über die interne Funktionsweise ihrer Abhängigkeit einbettet (wie die Annahme, dass die Ergebnisse einer Methode immer absteigend der Größe nach sortiert sind), können Sie diese Abhängigkeit nicht wirklich durch eine andere Implementierung ersetzen. Die Arten impliziter Kopplung mit einer spezifischen Implementierung sind besonders schädlich, weil sie für einen Leser Ihres Codes nicht offensichtlich sind. Der Nutzer einer Abstraktion darf von nichts als dem öffentlichen Vertrag dieser Abstraktion abhängen.
Ich würde empfehlen, dass Sie das Offen-Geschlossen-Prinzip als eine Entwurfsmethode statt als Ziel betrachten. Wenn Sie versuchen, alles, was sich Ihrer Meinung nach vielleicht ändern könnte, vollständig erweiterbar zu gestalten, werden Sie wahrscheinlich ein zu kompliziertes System entwerfen, mit dem sich schlecht arbeiten lässt. Es ist Ihnen vielleicht nicht immer möglich, Code zu schreiben, der das Offen-Geschlossen-Prinzip in jeder Hinsicht befolgt, aber schon allein, sich in die Richtung zu bewegen, kann von Nutzen sein.

Senden Sie Fragen und Kommentare (in englischer Sprache) an mmpatt@microsoft.com.

Jeremy Miller ist ein Microsoft-MVP für C# und ist der Entwickler des Open-Source-Tools „StructureMap“ (structuremap.sourceforge.net) für Abhängigkeitsinjektion mit .NET und des bevorstehenden Tools „StoryTeller“ (storyteller.tigris.org) für FitNesse-Tests in .NET. Besuchen Sie seinen Blog „The Shade Tree Developer“ unter codebetter.com/blogs/jeremy.miller, der Teil der CodeBetter-Website ist.

Communityinhalt   Was ist Community Content?
Neuen Inhalt hinzufügen RSS  Anmerkungen
Processing
Page view tracker