Trait ou héritage, il faut choisir

Cet article a été publié depuis plus de 6 mois, cela signifie que le contenu peut ne plus être d'actualité.

C'est avec la version 5.4.0 que les Traits sont apparus en PHP. Il s'agit d'un mécanisme de réutilisation de code, introduit dans le but de réduire certaines limites de l'héritage simple. Je constate que de plus en plus de développeurs se mettent à utiliser les Traits à tel point que ces certains n'utilise même plus l'héritage et préfère avoir recours à ce mécanisme. Que se soit avant de faire un héritage ou avant d'utiliser un trait, il convient de se poser la question de quand utiliser l'un ou l'autre ?

Revenons rapidement sur ce qu'est un héritage. L'héritage permet d'établir une relation (de type parent/enfant) entre des classes. Lorsque l'on étend une classe, la classe fille hérite alors de toutes les méthodes publiques et protégées de la classe parente. Il est également possible que la classe fille redéfinisse certaines de ces méthodes pour les adapter à ces spécificités.

Les traits quand à eux ont comme seul objectif de partager du code entre classes sans structuration hiérarchique. Il n'y a donc ici pas de relation entre les classes qui utilisent les traits. Il partage seulement un comportement.

Il faut bien avoir cette distinction lorsque vous écrivez du code. Voici par exemple, du code que j'ai pu voir récemment sur un projet :

trait RepositoryTrait
{
    public function find($id)
    { /* ... */ }

    // ...
}

class UserRepository
{
    use RepositoryTrait;

    // ...
}

class GroupRepository
{
    use RepositoryTrait;

    // ...
}

Il faut faire attention à l'utilisation des traits car ces derniers peuvent facilement casser les aspects classiques de la programmation objet. Dans ce bout de code, les classes UserRepository et GroupRepository n'implémente pas d'interface commune, elle utilise un trait. De ce fait, le principe de programmation SOLID peut facilement être rompu.

Dans ce cas-là, un héritage semble plus adapté :

abstract class Repository
{
    public function find($id)
    { /* ... */ }

    // ...
}

class UserRepository extends Repository
{
    use RepositoryTrait;

    // ...
}

class GroupRepository extends Repository
{
    use RepositoryTrait;

    // ...
}

De plus en utilisant un héritage, il est plus facile de faire du typage explicite. Il est alors possible de vérifier que la classe fournit en paramètre est bien de type Repository, ce qui n'est pas possible avec un trait. De plus il sera plus facile de respecter le principe de substitution de Liskov.

Il est préférable d'utiliser les traits pour partager du code avec des classes qui n'ont pas de lien entre elles. Prenons comme exemple :

trait TextFormater
{
    public function sanitize($text)
    {
        return strip_tags($text);
    }
}

class Mailer
{
    use TextFormater;

    public function send($subject, $text)
    {
        mail('user@mail.com', $this->sanitize($subject), $this->sanitize($text));
    }
}

class ConsoleWriter
{
    use TextFormater;

    public function write($text)
    {
        echo $this->sanitize($text), PHP_EOL;
    }
}

Dans l'exemple précédent, la classe Mailer a pour but d'envoyer un mail à un utilisateur et la classe Writer de générer un affichage à l'écran. Ces deux classes n'ont aucun rapport et pourtant elles ont toutes les deux besoins de traiter du texte fourni en paramètre. L'héritage n'a ici pas sa place et il convient donc de partager le code via l'utilisation d'un trait.

Un autre exemple concret de l'utilisation des traits est l'implémentation du pattern Singleton. Lorsque l'on souhaite utiliser ce dernier, le premier réflexe pourrait être de créer une classe Singleton et de faire hériter nos objets de cette dernière. Mais ce n'est pas toujours possible à cause de l'impossibilité de faire de l'héritage multiple. Partant du principe qu'il est préférable de favoriser un héritage "fonctionnel", il convient dans ce cas-là, d'avoir recours aux traits pour résoudre le problème :

trait Singleton
{
    protected static $instance = null;

    public static function getInstance()
    {
        if (null === $this->instance) {
            $this->instance = new self;
        }

        return $this->instance;
    }
}

class Example extends Article
{
    use Singleton;
}

$expl = Example::getInstance();

Héritage et Trait, les deux concepts ont leur place dans nos projets, utilisons-les à bon escient et au bon moment.