PHP hat sich in den vergangenen Jahren insbesondere im Bereich der objektorientierten Programmierung (OOP) erheblich weiterentwickelt. Dazu gehört die Einführung verschiedener OOP naher Konzepte, wie Traits und die Weiterentwicklung von Vererbungen (Elternklassen). Traits als auch Parent Classes ermöglichen es, den Code besser zu strukturieren, zu kapseln und wiederzuverwenden. Dies sorgt für eine Verbesserung bei der Wartbarkeit und reduziert Fehler. Doch, wann sollte man Traits und wann Elternklassen nutzen? In diesem Beitrag werde ich einen Blick auf die Unterschiede, die Vor-/Nachteile und Alternativen.
Was sind Traits?
Innerhalb von Traits können Methoden und Eigenschaften gesammelt und so wiederverwendet werden, um Don't Repeat Yourself (DRY) zu erreichen und Code-Redundanzen zu minimieren. Die Traits lassen sich in Klassen einbinden (mittels use
), wodurch alle Methoden und Eigenschaften aus dem Trait in der Klasse zur Verfügung stehen. Einer der zentralen Vorteile von Traits gegenüber einer konventionellen Vererbung innerhalb von Klassen (extends
) ist, dass eine Klasse beliebig viele Traits integrieren kann und es keine direkte Vererbungsbeziehung zwischen der Klasse und dem eingebunden Trait gibt.
Letztendlich führt die Verwendung von Traits dazu, dass die darin enthaltenen Methoden/Eigenschaften in die Klasse eingefügt (quasi kopiert) werden, so als ob sie Teil der Klasse selbst wären.
Vorteile
- Traits sind flexibel nutzbar; es muss nicht auf Klassenhierarchien geachtet werden und lassen sich auch in thematisch sehr unterschiedlichen Klassen nutzen.
- Klassen können beliebig viele Traits einbinden
- Traits können wiederum andere Traits einbinden
- Traits können auch abstrakte Methoden definieren: Diese abstrakten Methoden müssen dann von der Klasse implementiert werden, welche den Trait einbinden. Dies ist eine Möglichkeit, um Methoden Signaturen innerhalb von Traits – auch ohne Interface – zu spezifizieren.
- Methoden in Traits können verschiedene Sichtbarkeiten besitzen (nicht nur
public
, sondern auchprotected
undprivate
; im Gegensatz zu Interfaces).
Nachteile
- Traits können selbst keine Interfaces implementieren (
implements
) - Es ist nicht möglich, die Sichtbarkeit von Methoden oder Eigenschaften auf den Trait selbst zu beschränken. Auch wenn Methoden/Eigenschaften innerhalb des Traits als
private
oderprotected
definiert werden, sind sie für die Klasse sichtbar und nutzbar (die Sichtbarkeit bezieht sich immer auf die Klasse, welche den Trait einbindet). - Traits können zu unübersichtlichen Strukturen führen. Insbesondere, wenn Traits nicht nur Methoden, sondern auch Eigenschaften definieren oder auf Eigenschaften der Klasse zugreifen (Stichwort: Dependency Hell). Richtig verrückt wird es, wenn Traits auf Traits basieren.
Nur weil etwas mit einem Trait möglich ist, ist dies nicht immer die beste Lösung. Daher sollte der Einsatz oder die Erweiterung von Trait immer kritisch hinterfragt werden.
Traits oder Elternklassen
Die Vererbung mittels Parent Classes ist sozusagen der Klassiker innerhalb der objektorientierten Programmierung. Eine Elternklasse erbt alle Eigenschaften und Methoden aus der Elternklasse (sowie dessen Elternklasse usw.), kann diese erweitern oder überschreiben. Der große Unterschied (je nach Sichtweise auch Nachteil) hierbei ist, dass eine Klasse nur eine Elternklasse besitzen kann und sich hieraus eine feste Hierarchie ergibt.
Die Verwendung von Elternklassen bietet sich insbesondere immer dann an, wenn sich Klassen sehr ähnliche Methoden/Eigenschaften teilen. Sind die Methoden eher Utilities und sollen in sehr unterschiedlichen Kontexten verwendet werden, ist ein Trait (oder Service, dazu etwas später mehr) eine bessere Lösung.
Besonderheit: Abstract Classes
Abstract Classes sind eine besondere Form von Klassen. Grundsätzlich haben Sie die gleichen Vor-/Nachteile wie normale Parent Classes. Was sie jedoch besonders macht (bzw. ihnen ihren Namen gibt), ist der Umstand, dass sie Methoden enthalten, die als abstract
definiert sind.
Diese Abstract Classes können ganz normal Eigenschaften und Methoden definieren, aber auch Methoden Signaturen (also wie die Methode heißt, welche Parameter und Typen sie erwartet und wie der Rückgabetyp ist), ohne die Logik für diese Methoden selbst zu implementieren. Eine weitere Besonderheit ist, dass als abstract
definierte Klassen direkt instanziiert werden können, sondern immer eine Kindklasse benötigen, welche dann auch die Logik für die abstrakten Methoden bereitstellt.
Abstract Classes sind damit relativ ähnlich zu Interfaces, nur mit dem Unterschied, dass Interfaces ausschließlich public
Methodensignaturen definieren können (und sich damit eher wie eine Schnittstellenspezifikation verhalten).
Beispiel für eine Abstract Class:
abstract class Animal {
protected $name;
public function __construct($name) {
$this->name = $name;
}
abstract public function makeSound();
}
class Cat extends Animal {
public function makeSound() {
echo "Meow";
}
}
class Dog extends Animal {
public function makeSound() {
echo "Woof";
}
}
$cat = new Cat("Kitty");
$cat->makeSound(); // Ausgabe: Meow
$dog = new Dog("Buddy");
$dog->makeSound(); // Ausgabe: Woof
Composing und Inheriting Behavior
Hinter den beiden Begriffen Composing und Inheriting verbergen sich zwei unterschiedliche Konzepte/Muster in der objektorientierten Programmierung, die jeweils verschiedene Ansätze zur Wiederverwendung von Code und zur Strukturierung von Klassen und deren Verhalten aufweisen. Klassenhierarchien und Vererbung (Inheriting) sowie Traits und Komposition (Composing) stellen die praktischen Anwendungen davon innerhalb von PHP dar.
Für das Composing stehen neben Traits auch andere Konzepte wie Services (mittels Service Container oder Dependency Injection [DI]) zur Verfügung.
Prioritäten und Konflikte beim Umgang mit Traits
Methoden Hierarchie
Insbesondere beim Einsatz von Kombinationen auf Vererbung und Traits in einer Anwendung ist es wichtig zu wissen, welche Methode die höhere Priorität bei der Ausführung hat. Nur die Methode mit der höchsten Priorität wird ausgeführt, alle anderen werden ignoriert.
Die Wichtigkeit der Priorität zeigt hier auch einen Schwachpunkt, wenn Traits unbedacht eingesetzt werden: Ist eine Methode an verschiedenen Stellen definiert und die Struktur zwischen Klassen und Traits ist nicht sauber, kann dies schnell zu Verwirrung und Fehler führen, wenn Methoden an anderer Stelle ausgeführt werden als erwartet.
Hierarchie: Class Method → Trait Method → Parent Class Method
- Ist eine Methode in einem Trait und einer Klasse definiert (welche den Trait einbindet), dann wird immer die Methode der Klasse ausgeführt.
- Erweitert eine Klasse eine Elternklasse und integriert gleichzeitig einen Trait, der eine identische Methode wie die Elternklasse implementiert, so wird die Methode des Traits ausgeführt (da diese eine höhere Priorität gegenüber der Methode der Elternklasse aufweist).
Konflikte
Konflikte können dann auftreten, wenn in einer Klasse mehrere Traits eingebunden werden, die einen identischen Methodennamen aufweisen. Dies kann PHP nicht selbstständig auflösen (da beide Traits die gleiche Priorität besitzen), sondern wir müssen in diesem Fall angeben, welche Methode bzw. welcher Trait hier Vorrang hat. Dies geschieht mittels der insteadof
Angabe.
Ein Beispiel:
trait TraitA {
public function someMethod() {
echo "This is someMethod from TraitA.";
}
}
trait TraitB {
public function someMethod() {
echo "This is someMethod from TraitB.";
}
}
class MyClass {
use TraitA, TraitB {
TraitA::someMethod insteadof TraitB;
}
}
$obj = new MyClass();
$obj->someMethod(); // Ausgabe: "This is someMethod from TraitA."
Alternativen zu Traits
- Service Klassen: Dies bietet sich insbesondere dann an, wenn in einer Anwendung eh schon PHP Libraries bzw. Frameworks verwendet werden. Diese stellen sehr oft bereits Implementierungen für Service Container oder Dependency Injection (DI) bereit.
- Abstrakte Klassen
- Value Objects: Das sind Klassen, die Werte oder Mengen von Werten beschreiben. Nach der Definition verändern sich diese Werte in einem Value Object nicht und die Klassen implementieren auch keine Methoden, welche die Werte verändern (sie sind immutable).
- Copy & Paste: Manchmal kann simples Kopieren und Einfügen die bessere Lösung sein, als das Motto "Don't Repeat Yourself" allzu wörtlich zu nehmen – vor allem dann, wenn der Code zwar gleich, aber die zugrundeliegenden Kontexte unterschiedlich sind. Dadurch können die Methoden in Zukunft besser angepasst oder erweitert werden, ohne die Abhängigkeit (zu anderen Codestellen) berücksichtigen zu müssen oder ungewünschte Seiteneffekte hervorzurufen.
Beispiel für einen Value Object:
class Coordinate {
private float $latitude;
private float $longitude;
public function __construct(float $latitude, float $longitude) {
$this->latitude = $latitude;
$this->longitude = $longitude;
}
public function getLatitude(): float {
return $this->latitude;
}
public function getLongitude(): float {
return $this->longitude;
}
public function isEqualTo(self $other): bool {
return ($this->latitude === $other->latitude && $this->longitude === $other->longitude);
}
public function formatGeoUri(): string {
return "geo:{$this->getLatitude()},{$this->getLongitude()}";
}
}