Drucken

[CCD] Open-Closed Principle

Wie bereits angekündigt, werde ich hier in meinem Blog in lockerer Folge Prinzipien und Praktiken aus dem Wertesystem der Clean Code Developer (CCD) vorstellen. Heute behandele ich ein Prinzip des Grünen Grads der Clean Code Developer, das Open-Closed Principle (OCP, dt.: Offen-Geschlossen Prinzip).

CCD-LogoFakt ist: Softwaresysteme ändern sich regelmäßig! Ständig werden neue Anforderungen an die Software gestellt, bereits bestehende Anforderungen werden geändert. Dieses kann durch den Kunden bzw. Auftraggeber getrieben sein, aber auch technologischer Fortschritt oder andere sich ändernde Randbedingungen können derartige Veränderungen erforderlich machen.

„All systems change during their life cycles. This must be borne in mind when developing systems expected to last longer than the first version.“
(Ivar Jacobson, 1992)

Das Open-Closed Principle wurde erstmals von dem französischen Informatiker Betrand Meyer, Entwickler der objektorientierten Programmiersprache Eiffel und heute Professor für Software Engineering an der ETH in Zürich, in seinem Buch Object Oriented Software Construction (Prentice Hall, 1988) vorgestellt. Es lautet:

Software-Einheiten (Module, Klassen, Funktionen, etc.) sollten offen für Erweiterungen, aber geschlossen für Modifikationen sein.

Vielleicht kennt der eine oder andere Leser folgendes Phänomen: eine Änderung oder Erweiterung einer Software führt zu einer wahren Kaskade an weiteren Änderungen, oder es treten unerwünschte Seiteneffekte auf. Das Open-Closed-Principle soll dafür sorgen, das bereits bestehendes und korrektes Verhalten von Software zwar erweitert werden kann („…offen für Erweiterungen…“), diese Erweiterungen aber nicht zu unerwünschten Seiteneffekten führen bzw. bereits bestehendes Verhalten nicht verändert wird („…geschlossen für Modifikationen…“). Das Open-Closed Principle besagt also, das ein (ideales!) Softwaresystem dadurch gut erweiterbar und veränderbar wird, indem man das System aus Modulen aufbaut, welche sich niemals verändern dürfen. Wenn sich die Anforderungen an das System ändern dann werden diese erfüllt, indem man neuen Code hinzufügt, und nicht dadurch, das man bestehenden, funktionierenden Code ändert. In der objekt-orientierten Software-Entwicklung können wir daher das Open-Closed Principle auch wie folgt ausdrücken:

Software-Einheiten (Module, Klassen, Funktionen, etc.) sollten erweitert werden, indem man ihnen durch Nutzung von objekt-orientierten Mechanismen wie Vererbung und Polymorphie neues Verhalten hinzufügt, anstatt diese Module intern zu verändern.

Zuerst möchte ich ein C++ Code-Beispiel zeigen, welches eine Verletzung des Open-Closed Principles darstellt. Nehmen wir mal an wir haben ein Softwaresystem, welches verschiedene Geräte für die Weitverkehrskommunikation nutzt:

#include <string>

struct WideAreaNetworkDevice
{
 enum Type
 {
  isdnModem,
  wirelessLan,
  dslModem
 } _type;
};

struct Data { };

struct IsdnModem
{
 WideAreaNetworkDevice::Type type;
 int bandwidth;
 double latency;
 // ...more stuff related to a ISDN modem here...
};

struct WirelessLan
{
 WideAreaNetworkDevice::Type type;
 // ...more stuff related to a WLAN device here...
};

struct DslModem
{
 WideAreaNetworkDevice::Type type;
 // ...more stuff related to a DSL modem here...
};

// A function to establish a connection using a particular WAN device...
void connect(WideAreaNetworkDevice& device,
 std::string& user,
 std::string& pw)
{
 switch (device._type)
 {
 case WideAreaNetworkDevice::isdnModem:
  connectUsingIsdnModem((IsdnModem&)device, user, pw);
  break;

 case WideAreaNetworkDevice::wirelessLan:
  connectUsingWlanDevice((WirelessLan&)device, user, pw);
  break;

 case WideAreaNetworkDevice::dslModem:
  connectUsingDslModem((DslModem&)device, user, pw);
  break;

 default:
  // ...the default case...
 } 
}

// A function to send data using a particular WAN device...
void sendData(WideAreaNetworkDevice& device,
 Data& data)
{
 switch (device._type)
 {
 case WideAreaNetworkDevice::isdnModem:
  sendDataUsingIsdnModem((IsdnModem&)device, data);
  break;

 case WideAreaNetworkDevice::wirelessLan:
  sendDataUsingWlanDevice((WirelessLan&)device, data);
  break;

 case WideAreaNetworkDevice::dslModem:
  sendDataUsingDslModem((DslModem&)device, data);
  break;

 default:
  // ...the default case...
 } 
}

// A function to receive data using a particular WAN device...
// ...yada-yada-yada...
// switch-case-statements as far as the eye can reach
// Q: did you get an idea about the problems of this design?

Natürlich ist dieses Programmdesign nicht grundsätzlich falsch; es dürfte das tun, was der Entwickler beabsichtigt hat. Allerdings tendieren derartige Entwürfe dazu, mit switch-case- oder if-else-statements sprichwörtlich durchsetzt zu sein, also: zahlreiche Fallentscheidungen, die den Programmverlauf in Abhängigkeit vom - in diesem Fall - verwendeten Gerät zur Weitverkehrskommunikation steuern.

Das Problem ist: jedes Mal, wenn entweder ein neues Gerät hinzugefügt werden muss (z.B. ein SatComNetworkDevice), oder wenn an den bestehenden Geräten etwas modifiziert werden soll, muss der gesamte Code nach jeglichem Vorkommen von diesen Fallentscheidungen durchforstet werden, was ein erhebliches Fehlerpotenzial in sich birgt. Somit ist dieses Modul nicht geschlossen für Veränderungen.

Hier nun das gleiche Programm nach einem Redesign unter Beachtung des OCP:

#include <string>

struct Data { };

// The abstract base class of all WAN devices
class WideAreaNetworkDevice
{
public:
 virtual void connect(std::string& user, std::string& pw) = 0;
 virtual void sendData(Data& data) = 0;
 // ...more pure virtual function here...
};

// A ISDN modem
class IsdnModem : public WideAreaNetworkDevice
{
public:
 virtual void connect(std::string& user, std::string& pw)
 {
  // ...
 };

 virtual void sendData(Data& data)
 {
  // ...
 };

private:
 int bandwidth;
 double latency;
 // ...more stuff related to a ISDN modem here...
};

// A WLAN device
class WirelessLan : public WideAreaNetworkDevice
{
public:
 virtual void connect(std::string& user, std::string& pw)
 {
  // ...
 };

 virtual void sendData(Data& data)
 {
  // ...
 };

 // ...more stuff related to a WLAN device here...
};

// A DSL modem
class DslModem : public WideAreaNetworkDevice
{
public:
 virtual void connect(std::string& user, std::string& pw)
 {
  // ...
 };

 virtual void sendData(Data& data)
 {
  // ...
 };

 // ...more stuff related to a DSL modem here...
};

// A function to establish a connection using a particular WAN device...
void connect(WideAreaNetworkDevice& device,
 std::string& user,
 std::string& pw)
{
 device.connect(user, pw);
}

// A function to send data using a particular WAN device...
void sendData(WideAreaNetworkDevice& device,
 Data& data)
{
 device.sendData(data); 
}

Der Vorteil dieses Designs ist offensichtlich: Müssen neue WAN devices hinzugefügt werden, so kann dieses erfolgen ohne dass bereits existierende WAN devices verändert werden müssen. Damit sinkt das Risiko, das sich in bereits existierendem Code Fehler einschleichen, denn diesen Code kann man unangetastet lassen. Muss der Entwickler - z.B. auf Grund einer sich ändernden Spezifikation - ein WAN device ändern, so geschieht dieses ebenfalls sehr lokal und damit nebenwirkungsfrei.

Heuristiken und Konventionen

Das Open-Closed Principle ist eine wesentliche Grundlage für viele Designheuristiken, die im OOD empfohlen sind:

  • Alle Attribute einer Klasse sollten mit der Sichtbarkeit private deklariert werden.
  • Vermeide globale Variablen.
  • Vorsicht vor falscher Verwendung von runtime type identification (RTTI), da es geeignet ist, das OCP auszuhebeln.

Amazon Ads...

Hosted by...

Publicons

Google Ads...

Creative Commons License

Dieses Werk bzw. dieser Inhalt steht unter einer Creative Commons Namensnennung-Nicht-kommerziell-Weitergabe unter gleichen Bedingungen 3.0 Deutschland Lizenz.

Fork me on GitHub