Teile und Herrsche

Hallo Spaß-Coder.

Wer von euch hat schon einmal ein Interface implementiert und findet anschließend ein oder mehrere leere Methoden oder Methoden mit throw new NotImplementedException() in seiner neuen Klasse wieder? Warum? Weil einfach zu viele Methoden im Interface vorgesehen sind, die mit der eigentlichen Aufgabe gar nichts zu tun haben. Hier werden wir immer wieder dazu gezwungen, viele Methoden in der Implementierung anbieten zu müssen, was auf der anderen Seite auch die Verwendung des Interfaces auf Grund dieser hohen Anzahl von Methoden nicht gerade einfach macht. Schauen wir uns daher das vierte Prinzip der SOLID-Prinzipien an.

 

Interface Segregation Principle (ISP)

„Clients sollten nicht dazu gezwungen werden, von Interfaces abzuhängen, die sie nicht verwenden.“

Robert C. Martin: The Interface Segregation Principle (Quelle: Wikipedia)

 

Erläuterung des Prinzips

Im Wesentlichen geht es beim ISP mal wieder um eine gute Namensgebung und der Umsetzung von einer konkreten Anforderung. Ein schlecht gewählter Name eines Interfaces führt unweigerlich dazu, dass hier viele Methoden landen werden, die möglicherweise wenig gemein haben.

Zu was führt denn ein umfangreiches Interface, welches Problem entsteht dabei? Bei einer Klasse streben wir lose Kopplung und eine hohe Kohäsion an. Lose Kopplung bedeutet hier, die Abhängigkeiten zwischen Klassen so gering wie möglich zu halten, damit die Austauschbarkeit gegeben ist. Die Kohäsion gibt an, wie sehr eine Klasse tatsächlich zusammengehörig ist. Wenn nun alle Methoden der Klasse alle Klassenvariablen verwenden, so ist die Kohäsion hoch. Verwendet ein Teil der Methoden einen Teil der Klassenvariablen und alle anderen Methoden genau die anderen Klassenvariablen, so ist dies ein Indikator (Code-Smell) dafür, dass die Klasse sich sinnvoll auf zwei Klassen aufteilen lässt – die beiden Teile haben offensichtlich wenig miteinander zu tun.

Ein Code-Smell für zu große Interfaces findet ihr auf der Seite des Clients, also bei der Verwendung des Interfaces. Werden regelmäßig nur wenige bestimmte Methoden eines Interfaces verwendet oder in bestimmten Situationen nur eine kleine Auswahl von Methoden benötigt, so zeigt dies an, dass das Interface möglicherweise zu umfangreich gewählt ist. Habt ihr schon einmal durch die lange Liste von Methoden eines Interfaces geblättert um die richtige Methoden zu finden, die gerade aufzurufen ist? Lästig.

Schauen wir uns ein kleines Beispiel aus der OSGI-Implementierung mit Felix an.

Für das Interface BundleActivator werden die beiden Methoden start(…) und stop(…) implementiert, für das Interface ServiceListener die Methode serviceChanged(…). Nun hätten beide Methoden auch in einem einzigen Interface bereitgestellt werden können, jedoch wäre ich dann gezwungen gewesen, alle drei Methoden in einer Klasse zu implementieren. So habe ich die Wahl, eine separate ServiceListener-Implementierung bereitzustellen. Der BundleActivator fügt den ServiceListener dem BundleContext hinzu bzw. entfernt diesen, das hat nichts mit der Reaktion auf das serviceChanged-Event zu tun.

 

Ist es ein Problem, bei Bedarf zwei oder mehr Interfaces zu implementieren? Natürlich nicht. Dabei kann aber jeder Implementierer entscheiden, welche der Methoden benötigt und demnach welche Interfaces implementiert werden. Bei nur einem umfangreichen Interface besteht diese Wahlmöglichkeit erst gar nicht.

Heißt dies nun, dass in jedem Interface nur eine Methode enthalten sein sollte? Wer gerade seine Schwarz-Weiß-Brille aufgesetzt hat, mag dies so sehen, jedoch ist das nicht die Intention des ISP. Stattdessen sollten alle Methoden in einem Interface zusammengefasst werden, die tatsächlich auch zusammen gehören und eine bestimmte Anforderung abbilden – so klein wie möglich, so groß wie nötig. Diese Idee kommt euch natürlich aus dem Single Responsibility Principle bekannt vor, oder?

 

Unsere Erfahrung mit dem ISP

Bei der Durchsicht unserer Codebasis sind mir tatsächlich sehr wenige Interfaces aufgefallen, die wir aufteilen sollten. Mit kleinen Interfaces zu arbeiten ist aus unserer Sicht deutlich einfacher und macht damit auch viel mehr Spaß. Wie oben beschrieben wird die Definition der Interfaces durch einen aussagekräftigen Namen deutlich unterstützt.

Aber wie gehe ich vor, wenn ich denn nun ein Interface aufteilen möchte, ohne sämtliche bereits vorhandenen Implementierer zu ändern? Das Refaktorisierungs-Muster „Extract Interface“ kann auch in diesem Fall angewendet werden.

  1. Dazu wird ein neues Interface mit gut gewähltem Namen angelegt, welches einen Teil der Methoden aus dem umfangreichen Interface aufnehmen soll.
  2. Im zweiten Schritt erweitert das bisherige Interface das gerade neu angelegte Interface.
  3. Anschließend können die zu extrahierenden Methoden in das neue Interface verschoben werden, ohne dass eine bestehende Implementierung zu ändern ist.
  4. Nun kann das neue kleine Interface implementiert werden, ohne die zusätzlichen Methoden aus dem alten Interface berücksichtigen zu müssen.

 

Zusammenfassung

Auch das Interface Segregation Principle sorgt für eine modularisierte Anwendung mit kleinen, überschaubaren und wiederverwendbaren Einheiten. Eine gute Namensgebung sowie Paket- / Namensraum-Organisation sorgt dafür, dass jeder Entwickler genau die Klassen und Interfaces findet, die er benötigt.

 

Viel Spaß beim Anwenden.

Eure Spaß-Coder.
Dieser Artikel basiert neben unseren Erfahrungen auf verschiedenen Internetquellen.

Schreibe einen Kommentar

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