Interfaces und abstrakte Klassen

Thursday, 23 August 2007, 11:08 von Blackflash

Vor einigen Tagen hatte ich das Vergnügen, einem Freund bei einem Detail zu helfen: Wann verwendet man Interfaces und wann abstrakte Klassen? Diese Fragestellung brachte mich erstmal ins Grübeln, aber kurz darauf sind mir einige Argumente eingefallen, wann man welchen Typ benutzt.

Zuerst müssen wir nochmal die Unterscheidung beider Ansätze heraustellen:

  • Abstrakte Klassen
    • können Eigenschaften enthalten
    • können bereits implementierte Methoden enthalten
    • in den meisten Sprachen ist es nicht möglich von mehreren Klassen abzuleiten
  • Interfaces
    • kann nur Methodenköpfe definieren
    • eine Klasse kann beliebig viele Interfaces implementieren - sofern sie nicht ihren definierten Methoden kollidieren

Nun nähern wir uns deduktiv einer Lösung.
Für unser Vorhaben machen wir virtuelle Tierversuche - ja, viele mögen solch ein reales Beispiel nicht, weil sie es nie so einsetzen können, aber dafür ist es leicht verständlich, worauf wir später aufbauen werden. Wir nehmen an, dass jedes Tier einen bestimmten Laut machen kann.

interface Animal { public function makeSound (); } abstract class Animal { abstract public function makeSound (); }

Man sehe erstmal von dem Namenskonflikt ab, der ist in diesem Fall irrelevant.
Beide Konstrukte bieten dasselbe: eine Abstraktion eines Tieres. Die Frage ist, welche Lösung ist in diesem Fall vorzuziehen?
Ich würde bereits nach Gefühl, die abstrakte Klasse vorziehen. Wieso? Ein Tier ist für mich etwas zum Anfassen - okay, manche Tiere sollte man nicht unbedingt anfassen - und deshalb finde ich eine abstrakte Klasse passender.
Versuchen wir es mit einem Gegenbeispiel, in dem ich Interfaces präferiere.

interface Hearable { public function makeSound (); } abstract class Hearable { abstract public function makeSound (); }

Ich habe Hearable in diesem Fall gewählt, da jeder, der einen Laut von sich geben kann, auch hörbar ist. Warum würde ich diesmal das Interface präferieren? Weil das Hörbar-Sein eine Eigenschaft oder eine Fähigkeit ist.
Ich konstatiere: Abstrakte Klassen bei "Dingen". Interfaces bei Fähigkeiten oder Eigenschaften.
Setzen wir dieses System mal bei einem komplexeren Beispiel um. Es gibt einen Wolf und ein Krokodil - beides Fleischfresser -, die aber unterschiedlichen Gruppen - mir fehlt das zoologisch korrekte Wort - angehören.

abstract class Animal { abstract public function makeSound (); } abstract class Reptile extends Animal { } abstract class Dog extends Animal { } interface Carnivore { public function hunt (); } class Crocodile extends Reptile implements Carnivore { // Implementation } class Wolf extends Dog implements Carnivore { // Implementation }

Warum ist ein Carnivore (Fleischfresser) kein Ding? Weil es in diesem Fall eine Eigenschaft ist, die verschiedenen Tieren verschiedener Typen zu eigen sein kann. Es gibt fleichfressende Reptilien, wie auch Hunde oder Fische. Ergo ist es eine Eigenschaft. Natürlich wäre auch eine weitere Unterteil möglich, sodass "CarnivoreAnimal extends Animal" und "CarnivoreReptile extends CarnivoreAnimal" sowie "Crocodile extends CarnivoreReptile" gilt, aber das ist sehr aufwändig und definitiv nicht zweckmäßig.
Ich muss zugeben, dass es verschiedene logische Einteilungen geben kann.
Man muss sich jedoch nur überlegen, wie man die logische Einteilung effektiv gestaltet. In diesem Fall ist der Fleischfresser als Verhaltensweise eine Eigenschaft eines Tieres. Für andere wiederum könnte es die Einteilung "Fleischfressendes Tier" geben. Und dieses Wort "Fleischfressendes" zieht sich durch die gesamte Klassenhierarchie.
Ich komme somit zu meiner zweiten Feststellung: Suche die effektivste logische Einteilung zwischen Eigenschaft/Fähigkeiten und Dingen anhand deren Konsequenzen.
Es gibt, wie bei allen Entscheidungen, keine richtigen und deshalb auch keine falschen Lösungen, es gibt nur Konsequenzen. Fleischfresser als abstrakte Klasse zu implementieren hat die Konsequenz, dass die Anzahl der Klassen unweigerlich steigt, aber dafür kann man einige andere Methoden, die für jeden Fleischfresser gleich sind, bereits implementieren. Mit dem Type-Hinting kann man dann auch einfacher verfügen, dass nur fleischfressende Reptilien als Parameter akzeptiert werden (function foo (CarnivoreReptile $reptile)). Man spart sich also Arbeit.
Auf der anderen Seite bieten Interfaces eine granulierte Lösung zum Hinzufügen von Fleischfressenden. Die Codebasis wird kleiner und somit auch übersichtlicher. Außerdem kann man vorhandene Tiere bereits sehr einfach erweitern, ohne die Klassenstruktur zu ändern.

Jetzt wollen wir zu schwierigeren Beispielen kommen, die einem eher unterkommen könnten als die Tiere.
Das Beispiel stammt aus meiner ACL-Implementation, die ich in den letzten Tagen entwickelt habe. Da diese ACL-Implementation mit einem Datenmodell zusammenarbeiten muss, damit sie über eine Datenbank kommunizieren kann, musste ich eine Lösung dafür finden. Der erste Gedanke war natürlich eine abstrakte Klasse, denn ein Modell ist ein Ding. Das Problem dabei ist jedoch die Vererbung: Es ist teilweise notwendig oder auch nur sinnvoll, dass ein Modell von einer bestimmten Klasse ableitet, die nicht meine Klasse Alveran_ACL_Model ist. Da meine Implementation für MVCLite geschrieben wurde, war es notwendig von der Klasse MVCLite_Model_Abstract abzuleiten. Sollte ich deshalb den Umweg über die Vererbung (Alveran_ACL_Model_MVCLite_Abstract extends MVCLite_Model_Abstract; Alveran_ACL_Model_MVCLite extends Alveran_ACL_Model_Abstract) gehen? Nein, das wäre einfach nur aufwändig. Deshalb habe ich das Modell als Eigenschaft betrachtet ("kann mit einer Datenquelle interagieren") und habe deshalb die Interfaces benutzt. Nun kann ich das Interface in bereits bestehende Klassen implementieren ohne etwas an der Klassenhierarchie zu ändern.
Wieso ist MVCLite_Model_Abstract kein Interface? Ganz einfach, denn das ist eine Konsequenz aus der Natur der abstrakten Klassen: Sie kann Code enthalten und das tut sie!
Wie man sehen kann, sind Konsequenzen das wichtigste bei der Entscheidung, die man beachten muss, ansonsten schleichen sich leicht Fehler in das System ein.

Um die Entscheidungsfindung zu erleichtern, stelle ich mal eine kurze Liste von Fragen auf, die man sich stellen sollte:

  • habe ich Code, den ich wiederverwenden kann?
  • wie hoch ist der Wiederverwendungswert?
  • wie hoch ist die Wahrscheinlichkeit, dass die Abstraktion in anderen Teilen wiederzufinden ist?
  • in welchen anderen Codeteilen sind diese Abstraktionen denkbar?
  • ist es eher ein Ding oder eine Fähigkeit/Eigenschaft?
  • sind Konflikte im Systen zu erwarten? (Stichwort: Mehrfachvererbung)

Natürlich ist diese Liste nicht vollständig, aber es sind schon wesentliche Punkte enthalten. Wichtig ist nur, dass man nicht dogmatisch vorgeht und strikt auf Interfaces oder abstrakte Klassen setzt. Jede Entscheidung hat Konsequenzen, die bedacht werden müssen, handle dementsprechend!

Kommentare


Kommentiere!

Your Name:


Your Email:


Your URL:


Spam Prevention:
Enter the text above into the box below.
If you are unable to read it, refresh the page.


Your Comment: