Les liaisons UML implémentées avec PHP

Image non disponible

9 commentaires Donner une note à l'article (4.5)UML définit plusieurs liens remarquables entre les classes. La totalité de ces liens pondérés permet de mesurer ce que l'on appelle le couplage. Savoir lire un diagramme de classes est aujourd'hui indispensable pour la conception d'une application web. PHP ne fait pas exception à cela, son modèle objet étant très mûr et tout à fait capable.
Nous allons ici présenter les différentes liaisons, leurs caracteristiques ainsi que leurs avantages/inconvénients et quand les utiliser. Association, héritage, agrégation, composition.
Dans cet article, nous allons voir comment utiliser ces liaisons avec PHP.

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Héritage

Une notion que tout le monde connait certainement, nous passerons vite sur sa définition. L'héritage se traduit par un lien fort entre 2 classes dont l'enfant "est un" - "est une sorte de" vis à vis de son parent.
Si l'on ne peut pas dire cela, alors il ne s'agit pas d'un héritage. L'héritage vient rapidement à l'esprit pour résoudre tous les problèmes de conception mais il est rarement la solution et il faut l'utiliser avec modération (une profondeur d'héritage trop importante traduit un problème de conception).
Nous allons voir que l'héritage possède quelques astuces.

En UML, l'héritage se représente par une flèche blanche généralement tournée vers le haut.

Image non disponible
Héritage
Une classe véhicule
Sélectionnez
<?php
class Vehicule { }
Un héritage envers véhicule
Sélectionnez
<?php
require_once 'path/to/Vehicule.php';

class Voiture extends Vehicule { }

Première notion: l'héritage est un lien fort. Une partie du code du parent va être porté dans l'enfant, celui-ci a donc besoin de la définition de la classe de son parent pour exister d'où l'instruction require.

Le programme PHPDepend permet une analyse statique de code et la production de statistique en terme de couplage, de cohésion et d'héritage.

Pour éviter d'avoir à charger les dépendances manuellement (require), PHP propose des mécanismes d'autoload passés en revue dans cet article.

I-A. Privé - protégé

Lorsqu'on parle "d'une partie du code qui va être portée", il s'agit du code non privé. La différence de visibilité entre privé et protégé n'a de sens que pour l'héritage.
Tout ce qui est protégé (et donc aussi tout ce qui est public) va être porté du parent vers l'enfant automatiquement alors que ce qui est privé n'est pas porté.

L'héritage et le portage de la visibilité privée en PHP
Sélectionnez
<?php
class A
{
    private $foo;
}

class B extends A { }

var_dump(new B);

/* affiche
object(B)#1 (1) {
  ["foo":"A":private]=>
  NULL
}
*/

Comme on le voit lorsque PHP dump un objet il utilise une notation "nom-attribut":"nom-classe":private et donc le nom de la classe qui possède l'attribut privé est bien visible. Il se passe la même chose à la sérialisation, PHP ajoute "\0nom-classe\0nom-attribut", ou encore lorsqu'on caste un objet vers un tableau (c'est une astuce utilisée pour modifier un attribut privé lorsqu'on en n'a pas le droit, mais passons sur cette technique très "hack")

Sérialisation et attributs privés
Sélectionnez
<?php
class A
{
    private $foo;
}

class B extends A { }

echo serialize(new B);
var_dump((array)new B);

/* affiche
O:1:"B":1:{s:6:"Afoo";N;}
Un hexdump montre bien les caractères nuls \0 :
00000000  4f 3a 31 3a 22 42 22 3a  31 3a 7b 73 3a 36 3a 22  |O:1:"B":1:{s:6:"|
00000010  00 41 00 66 6f 6f 22 3b  4e 3b 7d                 |.A.foo";N;}|
0000001b

array(1) {
  ["Afoo"]=>
  NULL
}

Un hexdump montre bien les caractères nuls \0 :
00000000  61 72 72 61 79 28 31 29  20 7b 0a 20 20 5b 22 00  |array(1) {.  [".|
00000010  41 00 66 6f 6f 22 5d 3d  3e 0a 20 20 4e 55 4c 4c  |A.foo"]=>.  NULL|
00000020  0a 7d 0a                                          |.}.|
00000023

*/

Comme les chaines PHP sont "binary safe", elles peuvent comporter le caractère "\0" sans gêner cette représentation. \0 a donc été choisi dans le format de sérialisation par défaut.

Preuve du non-portage de la visibilité privée
Sélectionnez
<?php
class A
{
    private $foo   = 'A';
    protected $bar = 'A';
    
    public function dumpPrivate()
    {
        return $this->foo;
    }
    
    public function dumpProtected()
    {
        return $this->bar;
    }
}
class B extends A
{
    private $foo   = 'B';
    protected $bar = 'B';
}

$b = new B;
echo $b->dumpPrivate();
echo $b->dumpProtected();
/* Affiche
A
B
*/

Le code ci-dessus démontre que la visibilité privée n'est pas portée dans l'héritage ce qui est le cas de la visibilité protégée.

Bien sûr, nous ne parlons pas ici de la visibilité publique accessible partout et qui est portée dans l'héritage comme la visibilité protégée.

I-B. Héritage multiple - arbre d'héritage

En PHP l'héritage multiple n'existe pas : une classe ne peut hériter que d'une et une seule classe. Ainsi l'héritage doit se réfléchir car il représente un point de blocage ("attention, je crée un héritage, si dans le futur je veux hériter d'une autre classe: je ne pourrai plus").

Une des solutions face à un problème d'héritage multiple est le design pattern décorateur.

En revanche on peut allonger l'arbre d'héritage - tant qu'on respecte la relation "est un", souvent vue comme "généralisation - spécialisation" - et tant que l'on ne bloque pas l'arbre au moyen du mot-clé final.

Non respect de la relation d'héritage : fatal error
Sélectionnez
<?php
class A
{
    public function foo() { }
}

class B extends A
{
    private function foo() { }
}

/* Affiche
Fatal error: Access level to B::foo() must be public (as in class A)
*/

L'exemple ci-dessus brise la relation d'héritage car B hérite de A mais B n'est pas un A car il ne peut pas faire ce que A sait faire (on ne peut appeler la méthode foo() sur lui mais on le peut sur son père). Idem si l'on utilise protected dans le parent et private dans l'enfant.

La règle d'héritage est simple: l'enfant doit savoir faire tout ce que sait faire le père et éventuellement plus, mais en aucun cas moins ("est-un")

Si vous voulez bloquer l'héritage d'une classe (et donc terminer l'arbre d'héritage), PHP vous le permet au moyen du mot-clé final

Utilisation de final pour bloquer l'héritage
Sélectionnez
<?php
class A { }
class B extends A { }
final class C extends B { }

class D extends C { } // Fatal error: Class D may not inherit from final class (C)

Dans l'exemple ci-dessus, nous sommes en présence d'un arbre d'héritage d'une profondeur de 3 niveaux dont le dernier niveau est final: toute tentative de prolongement de l'arbre via sa branche C se soldera par une erreur fatale (la classe D dans l'exemple), il reste possible par contre de prolonger l'arbre en recréant une branche à partir de B.
L'utilisation de final devant une classe est rare mais peut s'avérer pratique.

final peut être utilisé devant une méthode empêchant ainsi sa surcharge dans une classe fille. Cette utilisation est rare, mais intéressante dans des cas très spécifiques.

I-C. Substitution de Liskov

Le principe de subsitution est clair: Un objet utilisateur de A doit pouvoir manipuler un fils de A sans s'en rendre compte. Si vous brisez ce principe, PHP vous avertira mais contrairement à l'héritage, il n'enverra qu'une erreur de niveau E_STRICT si vous avez activé ce rapport d'erreur (qui ne l'est pas par défaut pour PHP<5.4).

Cassure du principe de Substitution de Liskov
Sélectionnez
<?php
/* indispensable sinon l'erreur n'apparait pas, en général
E_STRICT est activé avec E_ALL dans php.ini pour le développement */
error_reporting(E_STRICT);

class A
{
    public function foo($a, $b) { }
}

class B extends A
{
    public function foo($a, $b, $c) { }
}

/* Affiche
Strict standards: Declaration of B::foo() should be compatible with that of A::foo()
*/

On voit clairement ici qu'un objet (C par exemple) qui utilisait A ne peut pas utiliser B à sa place car il devrait alors passer un paramètre en plus lors de l'appel de la méthode foo() sur celui-ci. Le principe de substitution rejoint celui de l'arbre d'héritage mais le pousse un peu plus loin.
Dans l'exemple ci-dessus, le simple fait de passer le paramètre additionnel $c en facultatif (c'est-à-dire en lui donnant une valeur par défaut), rend la signature compatible avec celle du parent, et fait ainsi disparaitre l'erreur.

Briser le principe de substitution de Liskov avec une interface génèrera une erreur de niveau fatal et non plus strict.

II. Association

L'association est une utilisation ponctuelle ou permanente d'une méthode d'un objet au sein d'un autre.

Image non disponible
Association
Image non disponible
Association ponctuelle

Si l'utilisation est permanente, l'inclusion de la classe utilisée est obligatoire. Dans le cas contraire elle pourra être chargée à l'utilisation et le trait UML devient alors pointillé.

Une association permanente
Sélectionnez
<?php
require_once 'path/to/Log.php';

class Data
{
    public function __construct()
    {
        Log::write('text');
        // ...
    }
    // ...
}

Ici, dès la création de l'objet Data la classe Log va être utilisée, elle est donc absolument nécessaire. On aurait pu imaginer d'autres cas identiques fonctionnellement : toutes les méthodes de Data utilisent Log par exemple.

Une association ponctuelle
Sélectionnez
<?php
class Data
{
    public function foo(Log $l)
    {
        $l->method();
        // ...
    }
    public function bar() { }
}

Dans le cas ci-dessus, on peut utiliser l'objet Data sans jamais avoir besoin de Log (on peut par exemple n'appeller que la méthode bar() et jamais foo()).
Il s'agit donc d'une association mais ponctuelle. On aurait pu imaginer d'autres exemples où une méthode de Data utilise la classe Log en l'incluant plutôt que d'en recevoir un objet via une méthode.

Rappel: PHP n'a pas besoin de connaitre la définition de la classe lorsqu'on type un paramètre de méthode sur cette classe-là. Aucune instruction require n'est donc nécessaire.

III. Agrégation

L'agrégation est une association particulière dans laquelle un objet est encapsulé dans un autre avec possibilité d'entrée-sortie. C'est une association permanente mais plus précise qui se traduit par la présence de getters/setters. On l'utilise pour effectuer de la délégation de responsabilités et de l'étude de variabilités/communalités.
En UML, la relation est caractérisée par un losange blanc qui va de l'objet agrégant vers l'objet agrégé. On traduit l'agrégation par une relation "a un", "est composé d'un", "utilise les services d'un".

Image non disponible
Agrégation
Une agrégation simple en PHP
Sélectionnez
<?php
class Log
{
    protected $file;
    
    public function setFile(File $file)
    {
        $this->file = $file;
    }
    
    public function getFile()
    {
        return $this->file;
    }
    
    public function event($priority, $message)
    {
        if ($this->file) {
            $this->file->write(sprintf("Priority: %d, message: %s", $priority, $message));
        }
    }
}
class File
{
    public function write($message) { }
}

Il est très important de noter que bien que les 2 objets soient liés, ils ne le sont que si on le désire et si l'on prend la peine de les construire et les assembler à la main (principe de l'inversion de contrôle).
C'est LA différence avec la relation de composition (chapitre suivant). Dans notre exemple, l'objet agrégé File est partageable et les accesseurs get/set de Log permettent de partager ou de récupérer l'objet.

Ici il s'agit de set/get : la cardinalité est donc de 1: l'objet Log ne peut posséder en lui qu'un et un seul objet File. En général on matérialise cela sur le schéma UML directement en indiquant la cardinalité près de la relation. Personnelement je préfère utiliser des conventions : si la cardinalité avait été de plusieurs (plusieurs objets File peuvent être ajoutés/retirés à Log), alors j'aurais écrit des méthodes addFile() et removeFile() et non get/set qui laissent suggérer "un et un seul".

III-A. Agrégateur automatisé

Créer toutes les méthodes get/set des agrégats dans une classe peut devenir pénible. A titre d'exemple voici une utilisation des méthodes magiques et de la Reflection en PHP pour créer une agrégateur automatisé en interceptant les appels de méthodes set/get.

La classe agrégateur
Sélectionnez

class Agregator
{
    public function __call($meth, $args)
    {
        if (preg_match("#(g|s)et(\w)+#", $meth, $matches)) {
            list(,$getOrSet,$class) = $matches;
            try {
                $param = new ReflectionProperty($this, $paramName = strtolower(substr($meth, 3)));
            } catch(ReflectionException $e) {
                $this->fail("Unknown attribute", $e);
            }
            if (!$param->isPublic()) {
                switch ($getOrSet) {
                    case 'g':
                        return $this->$paramName;
                    break;
                    case 's':
                        if (array_key_exists(0, $args) && $args[0] instanceof $paramName) {
                            $this->$paramName = $args[0];
                        }
                        return $this;
                    break;
                }
            }
            $this->fail("Aggregate attribute should not be public");
       }
   }
   
   private function fail($message, Exception $e = null)
   {
       throw new RuntimeException($message, null, $e);
   }
}

Dans cette exemple, une méthode __call() est utilisée pour intercepter les appels aux méthodes get*() ou set*().
On analyse ensuite le paramètre passé à ces méthodes et on regarde si dans la classe il existe un attribut non public ayant le même nom (par exemple setA(new A)). Si c'est le cas on s'execute, sinon on renvoie une erreur.

Ce type d'artefact "magique" est utilisé sous une forme déformée dans certains frameworks comme Zend Framework ou Symfony.
L'exemple montré ci-dessus n'est là qu'à titre de démonstration de la fléxibilité de PHP, il n'est pas utilisé dans des cas réels car il apporte de nombreux inconvénients.

IV. Composition

La composition est une agrégation non-partageable (non-isolable). Concrètement, cela signifie que comme pour l'agrégation, un objet va être contenu dans un autre. La différence est qu'il n'y a aucun moyen de créer l'objet contenu, ni de le récupérer.

Image non disponible
Composition
Exemple de composition
Sélectionnez
<?php
require_once 'path/to/File.php';

class Log
{
    protected $file;
    
    public function __construct($path)
    {
        $this->file = new File($path);
    }

    public function event($priority, $message)
    {
        $this->file->write(sprintf("Priority: %d, message: %s", $priority, $message));
    }
}
class File
{
    public function write($message) { }
}

La composition se reconnait par rapport à l'agrégation par l'absence de get/set (non-partageable) et par la présence d'un "new" au sein d'une classe (ou d'une fabrique).
Dans l'exemple ci-dessus, la création d'un objet Log entraine la création d'un objet File (celle-ci peut cependant être différée) et surtout la destruction de l'objet Log entraine la destruction de l'objet File le composant.

La composition est peu recommandée car elle rend le couplage trop fort, elle est l'inverse du principe d'inversion de contrôle et rend ainsi les programmes difficilement testables.

V. Implémentation

L'implémentation est une liaison qui fait intervenir une classe et une interface. Une classe implémente une interface. La relation se décrit comme "permet", "admet", "a la capacité de".
Contrairement à l'héritage, une classe peut implémenter autant d'interfaces qu'elle le souhaite. Une implémentation n'est pas un héritage, même si ça y ressemble dans la forme.
C'est un lien fort qui nécessite l'inclusion du code de l'interface dans le code de la classe l'implémentant (présence d'une instruction require). En UML, cela se traduit par la même flèche qu'un héritage, mais le trait est discontinu.

Image non disponible
Implémentation
Une interface quelconque
Sélectionnez
<?php
interface Vendable
{
    function vendre($nb);
}
Implémentation
Sélectionnez
<?php
require_once 'path/to/Vendable.php';

class Produit implements Vendable
{
    public function vendre($nb) { }
    public function foobar() { }
}

Si une classe implémente 2 interfaces définissant une même méthode, il y a alors conflit, PHP renverra une erreur fatale et vous demandera de vous arranger (renommer une méthode par exemple).

Conflit d'interfaces
Sélectionnez

interface Foo
{
    function baz();
}
interface Bar
{
    function baz();
}

class A implements Foo, Bar
{
    public function baz() { }
}

/* Affiche
Fatal error: Can't inherit abstract function Bar::baz() (previously declared abstract in Foo)
*/

Bien que ce soit rarement utilisé, une interface peut hériter (extends) d'une autre et la surcharger ou la compléter.

Une interface par définition représente des méthodes publiques. Vous pouvez préciser le mot-clé 'public', mais c'est facultatif.
Le corps des fonctions ne doit pas être écrit, il ne faut même pas ouvrir les accolades, mais bien fermer la déclaration immédiatement avec un point-virgule.
Une interface ne peut, par définition, pas contenir d'attributs.
Une interface peut contenir des constantes.
Vous devez définir concrêtement la méthode de l'interface, dans la classe en question : vous ne pouvez pas la déclarer "abstract".

VI. Lecture d'un schéma UML

Entrainons-nous à la lecture d'un diagramme de classes UML simplifié dans lequel n'apparaissent que des relations.

Image non disponible
Schéma général

A la lecture de ce schéma simplifié nous pouvons affirmer (chaque mot des phrases ci-après est réfléchi) :

  1. La classe "principale" est Magasin, on peut lui passer une Personne et des Consommables ;
  2. Il existe 2 types concrêts de Personne : Client et Anonyme ;
  3. Il existe 2 types abstraits de Consommable, Service et Produit chacun ayant des sous-types concrêts ;
  4. Le Magasin gère un Ticket.

Attention à "un" et "des". Si c'est "un" et qu'il s'agit d'une agrégation, des méthodes get/set seront présentes. Si c'est "des", pour une agrégation toujours, alors des méthodes add/remove seront présentes.
"On peut lui passer" définit l'agrégation, en revanche "Foo gère un Bar" laisse suggérer une composition de Bar dans Foo.

VII. Conclusion

Les liaisons UML dans les diagrammes de classes sont très importantes. La compréhension d'un système d'information orienté objet se décompose en 2 parties: la compréhension de la responsabilité de chaque objet ET la manière dont cet objet est lié au système général. Exactement comme en mécanique, ou de manière plus générale en "étude de systèmes" (dans notre cas informatique).
UML est un langage de modélisation permettant de dessiner des schémas représentant une partie précise d'un système informatique. Il existe des tonnes de schémas, pas que le diagramme de classes même si celui-ci est le plus utilisé.
Il existe aussi des logiciels de modélisation, certains sont capables de générer le squelette du code à partir du diagramme de classes. Cette utilisation est peu répandue dans le monde PHP, mais très utilisée en Java ou encore en C++.
Notre rubrique générale UML
Les patterns de tests avancés avec PHPUnit

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2010 . Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.