Das Interface Segregation Principle

Definition

Das Interface Segregation Principle (Schnittstellenaufteilungsprinzip, ISP) ist eines der SOLID-Prinzipien. Es lautet:

„Clients should not be forced to depend upon Interfaces that they do not use.“
„Clients sollten nicht gezwungen werden von Schnittstellen abhängig zu sein, die sie nicht verwenden.“

Klassen mit „überladenen Schnittstellen“ sind nicht kohäsiv. Die Schnittstellen solcher Klassen können in Gruppen von Methoden aufgeteilt werden. Das hat den Vorteil, dass ein Client dann nur eine Schnittstelle, mit einer Gruppe von Methoden nutzt, die es auch wirklich braucht.

Beschreibung

Wenn Clients gezwungen sind von Schnittstellen abhängig zu sein, die sie nicht benutzen, dann sind diese Clients auch von den Änderungen an diesen Schnittstellen betroffen. Dies führt zu einer unbeabsichtigten Kopplung zwischen den Clients. Anders ausgedrückt, wenn ein Client C1 von einer Klasse abhängig ist, die Schnittstellen enthält die der Client C1 nicht nutzt, aber ein anderer Client C2 es tut, dann ist der Client C1 von den Änderungen betroffen die der Client C2 von der Klasse erfordert. Solche Kopplungen sollten soweit es möglich ist vermieden werden, weshalb man die Schnittstellen dort wo es möglich ist, aufteilen sollte.

Aber wie kann man das Schnittstellenaufteilungsprinzip einhalten, wenn ein Client Zugriff auf mehrere Gruppen von Methoden benötigt? Die Antwort liegt in der Tatsache, dass ein Client nicht unbedingt über die Schnittstelle des Objekts auf dieses zugreifen muss. Es kann vielmehr durch Delegation oder durch eine Basisklasse auf das Objekt zugreifen.

Beispiel

Schauen wir uns ein Sicherheitssystem an. In einem solchen System gibt es Door Objekte, die geschlossen und geöffnet werden können, und die wissen, ob die Tür geöffnet ist oder nicht.

abstract class Door
{
     public abstract bool IsOpen { get; }

     public void Lock() { ... }
     public void Unlock() { ... }
}

Diese Klasse ist abstrakt, so dass Clients Objekte die der Door Schnittstelle entsprechen verwenden können, jedoch ohne von den konkreten Implementierungen der Door Klasse abhängig zu sein.

Eine konkrete Implementierung wäre z.B. eine TimedDoor, die einen Alarm auslöst, wenn die Tür zu lange offen war. Um dies zu tun, kommuniziert das TimedDoor Objekt mit einem Timer Objekt.

public class Timer
{
     public void Register(int timeout, TimerClient client) { ... }
}

public abstract class TimerClient
{
     public abstract void Timeout();
}

Wie kann man aber sicherstellen, dass die TimerClient Klasse mit der TimedDoor Klasse kommuniziert, um diese über einen Timeout zu informieren? Eine übliche Lösung könnte darin bestehen die Door Klasse, und damit die TimedDoor Klasse, direkt vom TimerClient abzuleiten. Damit wäre sichergestellt, dass der TimerClient sich am Timer registrieren kann und eine Nachricht über einen Timeout erhält.

Diese Lösung mag durchaus üblich sein, ist jedoch nicht ganz unproblematisch. Das problematischste dabei ist, dass die Door Klasse nun direkt von der TimerClient Klasse abhängig ist. Dabei benötigen nicht alle Variationen der Door Klasse ein Timing. Tatsächlich hat die ursprüngliche Abstraktion der Door Klasse überhaupt nichts mit dem Timing zu tun. Das hat zur Folge, dass jede Anwendung in gewisser weise von der TimerClient Klasse abhängig ist, auch wenn diese gar nicht genutzt wird.

Das Diagramm zeigt ein typisches Syndrom eines objektorientierten Entwurfs. Es ist das Syndrom der Schnittstellenbelastung. Die Schnittstelle der Door Klasse wurde mit einer Schnittstelle belastet, die es überhaupt nicht benötigt. Es wurde gezwungen diese Schnittstelle nur zum Nutzen einer ihrer Unterklassen zu integrieren. Würde diese Praxis fortgesetzt, so müsste jedes mal, wenn eine abgeleitete Klasse eine neue Schnittstelle benötigt, diese in die Basisklasse eingefügt werden. Das würde die Schnittstelle der Basisklasse weiter belasten, so dass diese überladen wird.

Darüber hinaus müsste jedes mal, wenn der Basisklasse eine neue Schnittstelle hinzugefügt wird, diese unter Umständen auch in den abgeleiteten Klassen implementiert werden. Eine gängige Praxis ist es, solche Schnittstellen in der Basisklasse als virtuelle null Funktionen anstatt abstrakter Funktionen hinzuzufügen. Damit wird die Notwendigkeit vermieden, diese Funktionen auch in den abgeleiteten Klassen zu implementieren. Eine solche Praxis verletzt jedoch das Liskovsche Substitutionsprinzip, welches zu Wiederverwendbarkeit und Wartungs Problemen führt.

Für die Lösung dieser Probleme stehen zwei Möglichkeiten zur Verfügung: Trennung durch Delegation und Trennung durch multiple Schnittstellen.

Trennung durch Delegation

Um das TimedDoor Problem zu lösen, wenden wir das Adapter Entwurfsmuster in der Objekt-Form an. Die Lösung besteht darin ein Adapter Objekt zu erstellen, welches sich von der TimerClient Klasse ableitet und die Nachricht weiter an die TimeDoor Klasse delegiert.

Wenn die TimerDoor Klasse eine Timeout Abfrage am Timer registrieren möchte, erstellt sie einen DoorTimerAdapter und registriert diesen bei dem Timer. Wenn der Timer eine Timeout Nachricht an den DoorTimerAdapter sendet, delegiert dieser die Nachricht an die TimedDoor weiter.

Diese Lösung entspricht dem Schnittstellenaufteilungsprinzip und verhindert die Kopplung der Door Clients mit dem Timer. Sollten Änderungen am Timer durchgeführt werden, wäre keiner der Door Clients betroffen. Außerdem muss die TimedDoor Klasse nicht die exakt gleiche Schnittstelle wie der TimerClient haben. Der DoorTimerAdapter übersetzt die TimerClient Schnittstelle in die der TimedDoor Schnittstelle.

public class TimedDoor : Door
{
     public void DoorTimeOut() { ... }
}

public class DoorTimerAdapter : TimerClient
{
     private TimedDoor _timedDoor;

     public DoorTimerAdapter(TimedDoor timedDoor)
     {
          _timedDoor = timedDoor;
     }

     public override void TimeOut()
     {
          _timedDoor.DoorTimeOut();
     }
}

Das Beispiel ist zwar eine gute Allzwecklösung, erfordert aber jedes mal die Erstellung eines neuen Objekts, wenn man einen Timeout registrieren möchte. Darüber hinaus benötigt die Delegation eine längere Laufzeit und höheren Speicherbedarf. Auch wenn diese Punkte auf den ersten Blick unbedeutend scheinen, gibt es Anwendungsbereiche in denen sie eine wichtige Rolle spielen könnten, z.B. in Echtzeit Kontrollsystemen.

Trennung durch multiple Schnittstellen

Das folgende Beispiel zeigt, wie man das Adapter Entwurfsmuster in der Klassen-Form verwendet, um das Schnittstellenaufteilungsprinzip anzuwenden. In diesem Model leitet sich die TimeDoor Klasse von der abstrakten Klasse Door ab und implementiert gleichzeitig das ITimerClient Interface. Somit können Clients der beiden Basisklassen die TimedDoor Klasse nutzen, ohne von ihr überhaupt abhängig zu sein. Sie verwenden dasselbe Objekt über separate Schnittstellen.

public interface ITimerClient
{
     void TimeOut();
}

public class TimedDoor : Door, ITimerClient
{
     public void TimeOut() { ... }
}

Fazit

In diesem Artikel wurden die Nachteile von „überladenen Schnittstellen“ besprochen, d.h. Schnittstellen die nicht spezifisch für einen Client sind. „Überladene Schnittstellen“ führen zu unbeabsichtigten Kopplungen zwischen Clients, die eigentlich getrennt sein sollten. Durch die Verwendung des Adapter Entwurfsmusters, entweder durch Delegation (Objekt-Form) oder durch die Implementierung von Interfaces (Klasse-Form), können „überladene Schnittstellen“ in abstrakte Basisklassen aufgeteilt und damit die ungewollte Kopplung zwischen den Clients aufgebrochen werden.

Quellen

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.