13- Les traits

En PHP, il est impossible de faire de l'héritage multiple, c'est à d'ire, qu'une classe ne peut hériter que d'une seule autre classe (et des classes dont cette classe hérite) en même temps. Cela est lié aux problématiques posées par l'héritage multiple, notamment le fameux "diamond problem", ou "deadly diamond of death" dans lequel une classe qui héritent de deux classes qui ont le même parent ne saurait quelle méthode héritée de ses deux parents utiliser si elle ne la réécrit pas.

Par exemple :

// côté classes
class Vehicule{
    public function avancer(){
        echo "Vroum !";
    }
}

class VehiculeDeCourse extends Vehicule {
    public function avancer(){
        echo "Braaap !";
    }
}

class VehiculeARoues extends Vehicule {
    public function avancer(){
        echo "Vrrrrrr !";
    }
}

class Voiture extends VehiculeDeCourse, VehiculeARoues{

}

// côté objets
$voiture = new Voiture();
$voiture->avancer();

Dans ce cas, que fera la méthode avancer() de l'objet $voiture ?

Ce genre d'architecture ambigüe posant trop de problèmes de design, beaucoup de langages modernes ont abandonné l'héritage multiple, dont le PHP. Néanmoins, il est parfois très utile pour une classe de pouvoir hériter des comportements d'autres classes qui ont des fonctions totalement différentes, et de pouvoir utiliser directement leur code sans forcément le réécrire comme ce serait le cas lors d'une implémentation d'interface.

Pour répondre à ce besoin, nous utilisons les traits, qui sont des entités entre les classes et les interfaces. Un trait peut avoir des attributs et des méthodes, comme une classe, mais ne peut être instancié, comme une interface. Une classe peut utiliser n traits en utilisant le mot-clé use.

Les modificateurs d'accès entre les traits et les classes qui les utilisent fonctionnent comme entre classes : les attributs public et protected se transmettent entre classes et traits, alors que les private restent dans la classe ou le trait.

#### Exemple d'utilisation de trait :
// définition des classes
trait VehiculeDeCourse{
    private $bruit = "BRAAP !";
    public $becquet = null;

    public function avancer()
    {
        echo $this->bruit;
    }
}

class Voiture{
    use VehiculeDeCourse;
}

// côté objets
$voiture = new Voiture();
$voiture->avancer(); // écrit BRAAP !
var_dump($voiture->becquet); // NULL

Pour comprendre d'avantage à quel point la classe et les traits sont liés, il faut rentrer un peu plus dans la complexité avec notamment les modificateurs d'accès private et protected, qui vont pouvoir être utilisés d'un côté et de l'autre.

// définition des classes
trait VehiculeDeCourse{
    private $becquet = null;

    public function ajouterBecquet(Becquet $becquet){
        $this->becquet = $becquet;
    }

    public function avancer()
    {
        // si on a un becquet, le bruit change
        if($this->becquet !== null){
            echo $this->becquet->faireDuBruit($this->bruit);
        }

        // sinon le bruit reste le même
        else{
            echo $this->bruit;
        }
    }
}

class Becquet{
    public function faireDuBruit(string $bruit)
    {
        return $bruit . "!!!!!!!!";
    }
}

class Voiture{
    use VehiculeDeCourse;

    protected $bruit = "BRAAP !";
}

// côté objets
$becquet = new Becquet();
$voiture = new Voiture();
$voiture->avancer(); // écrit BRAAP !

$voiture->ajouterBecquet($becquet);
$voiture->avancer(); // écrit BRAAP !!!!!!!!!

Actuellement, notre classe Voiture qui utilise le trait VehiculeDeCourse hérite de son comportement faireDuBruit() qui dépend entièrement de son propre attribut $bruit utilisé par le trait alors qu'il n'est que dans la classe Voiture. Le trait VehiculeDeCourse peut avoir un becquet qui peut modifier son comportement, ici sa capacité à faire du bruit. Ainsi, cela nous évite de mettre dans Voiture des attributs et comportements qui n'ont à y figurer que si la voiture devient une voiture de course, et qui nous évitent au maximum d'utiliser l'héritage classique.

Mais pourquoi éviter d'utiliser l'héritage classique ? Tout simplement car une architecture basée sur l'héritage est beaucoup moins souple qu'une architecture basée sur l'implémentation. Pouvoir ajouter des comportements à chaque classe est beaucoup plus flexible que d'hériter de toute l'ascendance de la classe parente et de subir les changements qu'on y apportera dans le futur. Il est toujours préférable lorsque plusieurs classes peuvent avoir des comportements similaires de leur faire implémenter des interfaces plutôt que de créér des tonnes d'enfants à un parent qui factorisera mal certains comportements.

Pour aller plus loin :

  • https://en.wikipedia.org/wiki/Design_Patterns
  • http://www.blackwasp.co.uk/gofpatterns.aspx
  • http://wiki.c2.com/?DesignPatternsBook

La plupart des design patterns préconisent l'implémentation plutôt que l'héritage lorsqu'il s'agit de définir les comportements des classes.

Par exemple, dans le cas de notre usine, si les véhicules réparables implémentent une interface Reparable, il suffit de créer une méthode reparer(Reparable $vehicule) dans notre atelier pour ne réparer que les véhicules réparables. Ceux qui n'implémentent pas cette interface peuvent être des véhicules mais pas être réparés.

Attention cependant, un trait n'est pas une interface, et ne participe pas au polymorphisme de la classe qui l'utilise. Pour cela, il faut utiliser l'héritage ou l'implémentation.

Ceci déclenchera une fatal error :

// définition des classes
trait VehiculeDeCourse{
}

class Voiture{
    use VehiculeDeCourse;
}

class Atelier{
    public static function reparerVehiculeDeCourse(VehiculeDeCourse $vehicule)
    {
    }
}

// côté objets
$voiture = new Voiture();
Atelier::reparerVehiculeDeCourse($voiture);

Ici, notre script crash et renvoie l'erreur suivante : PHP Fatal error: Uncaught TypeError: Argument 1 passed to Atelier::reparerVehiculeDeCourse() must be an instance of VehiculeDeCourse, instance of Voiture given.

Il est évident que certaines collisions ou ambigüités peuvent se présenter lorsque la classe et le trait définissent la même méthode ou le même attribut, ou que la classe utilise plusieurs traits qui ont les mêmes méthodes et attributs. Pour cela, PHP utilise un ordre de précédence des attributs et méthodes :

  • Une méthode d'un trait écrase celle de la méthode héritée de la mère d'une classe
  • Une méthode écrite dans la classe n'est pas écrasée par celle d'un trait
  • Un trait peut contenir une méthode qui surcharge les méthodes héritées par la classe qui utilise le trait.
  • Pour résoudre un conflit dans le cas ou deux traits ont la même méthode, il faut utiliser le mot-clé insteadof ou renommer les méthodes avec le mot-clé as dans la classe qui utilise les traits.
  • On peut réécrire les modificateurs d'accès des méthodes d'un trait dans une classe
  • Un trait peut utiliser d'autres traits
  • un attribut défini par une classe et par un trait utilisé déclenchent une erreur fatale sauf si il est de même nature et avec la même valeur initiale.
  • Les conflits insolubles des méthodes émettent une erreur fatale.
  • Un trait peut définir des attributs et méthodes statiques et des méthodes abstraites.

Exemple de résolution de conflit:

// définition des classes
trait VehiculeDeCourse{
    public function faireDuBruit()
    {
        echo "Vroum !";
    }
}

trait VehiculeElectrique{
    public function faireDuBruit()
    {
        echo "Bzzzz !";
    }
}

class Voiture{
    use VehiculeDeCourse, VehiculeElectrique{
        VehiculeElectrique::faireDuBruit insteadof VehiculeDeCourse;
        VehiculeDeCourse::faireDuBruit as faireBruitDeCourse;
    }
}

// côté objets
$voiture = new Voiture();
$voiture->faireDuBruit(); // Bzzzz !
$voiture->faireBruitDeCourse(); // Vroum !

Pour aller plus loin sur les traits :

http://php.net/manual/fr/language.oop5.traits.php