Objektorientierung (OOP) – Abstract, Interface, Trait

Nachdem ich kürzlich ein Beispiel zu OOP gegeben habe, möchte ich nun ein paar Konzepte weiter ausführen. Zum Beispiel wann man ein Interface oder eine abstrakte Klasse verwenden sollte. „Neumodisches Zeug“ wie Traits werden auch berücksichtigt.

Abstrakte Klasse

In einer abstrakten Klasse werden Funktionen und Logiken zusammengefasst, die zwar allgemein gültig, aber für sich genommen noch nicht sinnvoll sind.
Nehmen wir als Beispiel an, dass wir eine Suche benötigen. Es gibt aber je nach Wunsch des Benutzers verschiedene Varianten (z. B. nach Stadt oder Telefonnummer). Die Vorgehensweise bleibt aber bei allen Varianten identisch: Es wird gesucht und die Ergebnisse anschließend verarbeitet. Wir erstellen also ein abstrakte Klasse, die dieses Verhalten festschreibt und für jede Suchvariante eine konkrete Klasse, die die Feinheiten regelt.

Der Kindklasse werden hier verschiedene Dinge vorgeschrieben bzw. ermöglicht. Sie bekommt über sogenannte „Hooks“ die Möglichkeit Einfluss auf die Suche zu nehmen. Für beforeHandle() erwarten wir, dass die Kindklasse etwas tut und definieren die Methode daher abstract. Weniger streng sind wir mit afterHandle(). Hier kann die Kindklasse zwar eingreifen, muss es aber nicht, da bereits ein Standardverhalten (nichts tun) eingebaut ist.
Des Weiteren wollen wir nicht, das die Kindklasse etwas am Ablauf ändert, daher ist die einzige von außen zugängliche (public) Methode als final definiert. Dafür bieten wir mit beforeHandle() und afterHandle() zwei sogenannte Hooks an.
Die Verarbeitung der Ergebnisse schließlich, wird durch private zur Chefsache erklärt und ist unveränderlich.

Mit einer abstrakten Klasse wird also die Grundfunktionalität einer Komponente festgelegt. Konkrete Kindklassen müssen dann nur noch die Unterschiede ausarbeiten.


Interface

Ein Interface definiert das „Aussehen“ einer Klasse und damit die verfügbaren Methoden. Es enthält im Gegensatz zur abstrakten Klasse keine Logiken. Bei unserem Beispiel der Suche bleibend, könnte das entsprechende Interface wohl so aussehen.

Ein Interface enthält ausschließlich die public Methoden.

Ein gutes Beispiel für ein Interface wäre ein Cache-Adapter. Die für APC und XCache notwendigen Logiken sind komplett unterschiedlich – eine abstrakte Elternklasse ist also eher sinnlos. Durch ein Interface kann aber sicher gestellt werden, dass die beiden Adapter eine save($key, $value, $ttl) Methode implementieren, die dann universell genutzt werden kann.
Überall dort wo ein Cache verwendet wird, wird also lediglich das Interface überprüft:

Wenn dann mit der nächsten Programmversion ein völlig neuer Cache implementiert wird, muss lediglich ein entsprechender Adapter angelegt werden mit implements CacheAdapter und der Rest funktioniert weiter wie bisher.


Trait

Ein Trait kann als „von der Programmiersprache verwaltetes Copy-Paste“ verstanden werden. Es ermöglicht also voneinander unabhängigen Klassen die Nutzung des selben Programmcodes.

Wir benötigen in verschiedensten Klassen den Cache, wollen aber zukunftssicher keine konkrete Klasse (XCache) instanzieren, sondern das zentral verwalten, falls es sich doch mal ändert. Jede Klasse, die den Cache nutzen möchte, bindet einfach nur den Trait ein. Stellen wir später auf einen anderen Cache um, muss das nur einmal im Trait geändert werden.

Wichtig: Traits sind nur in seltenen Fällen die richtige Wahl! Meistens ist es besser die Funktionalität des Traits in eine eigenständige Klasse zu packen, evtl. ist ein Singleton oder eine Factory besser geeignet als ein Trait.