Archives par étiquette : interface

PHP : le design pattern Observateur

Observateur, un design pattern comportemental

Après avoir vu Adaptateur, Décorateur, Template Method ou Factory, nous allons nous concentrer sur un design pattern comportemental : Observateur. Comme tous les design patterns comportementaux (au sens GoF du terme), Observateur décrit la manière dont des objets interagissent entre eux.

Quels sont ces objets ?

Ce design pattern met en jeu deux types d’objets :

  • un sujet
  • des observateurs

Un sujet notifie un ou plusieurs observateurs qu’un changement d’état vient de se produire chez lui.

Personne observant le ciel aux jumelles

Un observateur attendant avec impatience une notification en provenance d’un sujet.

Dans quel état j’erre ?

Le sujet possède un état interne; lorsque cet état est altéré, il va notifier ses observateurs de ce changement. Prenons un exemple trivial, qui va parler au plus grand nombre : lorsqu’une personne présente sur un réseau social (notre sujet) fête son anniversaire, son état change car sa propriété âge est incrémenté d’une unité et quiconque suit ce sujet (un observateur, donc) reçoit une notification l’avertissant de ce changement d’état. L’observateur se met à jour et affiche la bonne nouvelle à l’écran: « X fête son anniversaire, n’oubliez pas de lui souhaiter ! »…

Structure du design pattern Observateur

Voici la version telle que définie par le Gang of Four :

Structure du design pattern Observateur

Toute la puissance de ce design pattern réside dans le fait que sa structure, à base d’abstractions, induit un couplage faible entre les deux types d’objets qui le composent : un sujet ne sait rien du type concret de ses observateurs, si ce n’est qu’il se conforme en tous points à l’interface Observer, pas plus qu’il n’est au courant de leur nombre. Le sujet n’aura donc aucunement à se risquer à d’hasardeuses hypothèses sur la nature de ceux qui l’observent et n’importe quel objet sera en mesure de l’observer, du moment que la classe concrète dont il est issu implémente l’interface Observer.

Lorsque j’évoque ici la notion d’interface, c’est au sens large du terme et pas stricto sensu. A l’origine – voilà maintenant 20 ans – GoF préconisait l’utilisation de classes abstraites, à dériver en classes concrètes. Mais vous ne le savez que trop bien, l’héritage n’est pas une panacée universelle, loin s’en faut.

Quelques points clés de ce modèle :

  • Sujet connait l’ensemble de ses observateurs. Leur nombre n’est pas limité.
  • Les observateurs s’enregistrent lorsqu’ils veulent observer un sujet et s’en détachent lorsqu’ils ne le veulent plus.
  • Lorsque son état est altéré, Sujet notifie l’ensemble de ses observateurs, sans notion d’ordre et sans distinction !
  • Lorsqu’il est notifié, un observateur peut obtenir des informations en provenance du sujet. Il peut décider de gérer une notification ou bien de l’ignorer.
  • Rien n’interdit à un observateur de suivre plusieurs sujets. Il faut évidemment que cet observateur puisse déterminer l’origine des notifications qu’il reçoit.

Un petit bémol, toutefois : en l’état actuel des choses, la fonction de mise à jour de l’observateur (Update) ne permet pas de savoir ce qui a changé dans le sujet, laissant à la charge de l’observateur la responsabilité de « deviner » ce qui a entrainé une modification.

La mise à jour, côté observateur

Nous allons voir comment à l’autre bout du fil, les observateurs gèrent leur mise à jour d’état avec la fonction Update.

Tu tires ou tu pousses ?

Deux modes de fonctionnement s’offrent à nous :

  • par poussée (push)
  • par traction (pull)

Le mode push – « Tiens, prends ça ! »

Le sujet pousse des informations aux observateurs. L’inconvénient de cette façon de faire est que le lien entre le sujet et ses observateurs se resserre. Le sujet peut se retrouver à pousser des informations dont certains observateurs n’auront pas besoin. Que pousser et à qui ?

Le mode pull – « Viens te servir ! »

Le sujet notifie simplement ses observateurs qu’un changement vient d’avoir lieu, à eux de savoir ce qui a changé !

Des versions basiques d’Observateur

Les contrats à respecter

Vous le savez maintenant, un sujet notifie N observateurs. On lui attache/détache ces observateurs à l’aide des méthodes appropriées et il les informe d’un changement via l’appel à la méthode mettreAJour de chacun d’entre eux. Voici à quoi vont ressembler nos contrats d’interface (dans la version GoF, ces abstractions sont des classes abstraites, ici nous utilisons des interfaces).

interface SujetInterface
{
    public function attacher(ObservateurInterface $observateur): void;
    public function detacher(ObservateurInterface $observateur): void;
    public function notifier(): void;
}

interface ObservateurInterface
{
    public function mettreAJour(SujetInterface $sujet): void;
}

La notification en version pull

Le sujet

Notre sujet stocke l’ensemble de ses observateurs dans une variable d’instance privée prévue à cet effet. Ce tableau est rempli ou vidé au fur et à mesure que les observateurs sont attachés ou détachés. Un setter mettreAJourLesNouvelles est prévu pour modifier la variable d’instance $nouvelles et un getter donnerLesNouvelles servira aux observateurs pour aller récupérer le dernier état du sujet. Souvenez-vous qu’en mode pull, ce sont les observateurs qui doivent se renseigner sur l’état du sujet et éventuellement mettre à jour le leur avec ces informations tirées depuis le sujet.

class Sujet implements SujetInterface
{
    private $nouvelles;

    private $observateurs;
    
    public function attacher(ObservateurInterface $observateur): void
    {
        $this->observateurs[] = $observateur;
    }
 
    public function detacher(ObservateurInterface $observateur): void
    {
        $key = array_search($observateur, $this->observateurs);
 
        if (false !== $key) {
            unset($this->observateurs[$key]);
        }
    }
 
    public function notifier(): void 
    {
        foreach ($this->observateurs as $observateur) {
            $observateur->mettreAJour($this);
        }
    }

    public function mettreAJourLesNouvelles(string $nouvelles): void 
    {
        $this->nouvelles = $nouvelles;
        $this->notifier();
    }
    
    public function donnerLesNouvelles(): string 
    {
        return $this->nouvelles;
    }
}

L’observateur

C’est lui qui fait le travail, une fois qu’il est notifié à travers sa méthode mettreAJour. Il reçoit le sujet à l’origine de la notification de mise à jour car il peut observer plusieurs sujets; il nous faut donc impérativement savoir qui a changé parmi les objets qu’on observe. Ici nous avons réduit la structure de la classe à sa plus simple expression: l’observateur reçoit une notification de nouvelle de la part d’un sujet, il va TIRER l’information depuis ce sujet et mettre son état interne en accord avec celui du sujet, afin d’avoir les toutes dernières nouvelles.

class Observateur implements ObservateurInterface
{
    private $dernieresNouvelles;
    
    public function mettreAJour(SujetInterface $sujet): void
    {
        $this->dernieresNouvelles = $sujet->donnerLesNouvelles();
    }
}

Le code client

Voici le code pour utiliser cette structure en mode pull, il est très simple: nous créons deux observateurs que nous attachons à notre unique sujet et nous mettons à jour l’état de notre sujet, qui va déclencher deux notifications (une pour chaque observateur).

$sujet = new Sujet();
$observateur = new Observateur();
$autreObservateur = new Observateur();
$sujet->attacher($observateur);
$sujet->attacher($autreObservateur);
$sujet->mettreAJourLesNouvelles("La bourse dévisse !");

La notification en version push

Changement d’interface

En mode push, c’est le sujet qui pousse les données modifiées à ses observateurs. Il sait donc ce qu’il leur faut, ce qui induit un couplage plus serré que dans le mode pull. Il peut aussi leur passer son état complet mais attention si les informations sont volumineuses…Les observateurs ont-ils besoin de recevoir un sac postal complet alors qu’ils attendent une carte postale ?

Quoiqu’il en soit, l’interface de l’observateur change et donc l’appel de la méthode aussi, dans le sujet:

interface ObservateurInterface
{
    public function mettreAJour(SujetInterface $sujet, array $donnees): void;
}

Le sujet

Le sujet fera désormais:

    public function notifier(): void 
    {
        foreach ($this->observateurs as $observateur) {
            $observateur->mettreAJour($this, ["nouvelles" => $this->nouvelles]);
        }
    }

L’observateur

L’observateur ira quant à lui se servir à ladite clé dans le tableau $donnees, qui dans la pratique sera évidemment bien plus fourni:

    public function mettreAJour(SujetInterface $sujet, array $donnees): void
    {
        $this->dernieresNouvelles = $donnees["nouvelles"];
    }

Le code client ne bougera pas d’un pouce, c’est la cuisine interne de l’objet qui change très légèrement. Le principe est rigoureusement identique !

Notre version d’Observateur

Qu’est-ce qui interdit à nos sujets d’être des observateurs d’autre sujets ? Rien !
Nous allons simuler un réseau social.

Nos interfaces

Elles ne changent pas beaucoup des précédentes:

interface SujetInterface
{
    public function attacher(ObservateurInterface $observateur): void;
    public function detacher(ObservateurInterface $observateur): void;
    public function notifier(): void;
}

interface SuiveurInterface
{
    public function suivre(SujetInterface $sujet): void;
    public function nePlusSuivre(SujetInterface $sujet): void;
}

interface ObservateurInterface
{
    public function mettreAJour(SujetInterface $sujet): void;
}

Nous avons juste rajouté une interface SuiveurInterface qui liste ce que doit savoir faire un suiveur: suivre et cesser de suivre.

Le membre

Dans notre réseau social, nous avons deux types de membres: des personnes et des entreprises ou des associations qui ont des pages. Ces deux catégories de membres peuvent suivre des membres, comme sur Facebook. Pouvant être à la fois sujet et observateur, ils doivent implémenter l’ensemble des interfaces. Nous factorisons les comportements dans une classe abstraite qui va contenir pour un membre, les membres qu’il suit et les membres qui le suivent. Les membres suivis iront dans $sujets et les suiveurs, dans $observateurs.

Faisons simple: de base, un membre a juste un nom et nous mettons un getter dessus, même pas de setter.
Vous constatez que la méthode mettreAJour ne contient que le sujet, la notification se fera donc en mode pull.

abstract class AbstractMembre implements SujetInterface, SuiveurInterface, ObservateurInterface
{
    protected $nom;
    
    protected $observateurs;
    
    protected $sujets;
    
    public function __construct(string $nom)
    {
        $this->nom = $nom;
    }
    
    public function attacher(ObservateurInterface $observateur): void
    {
        echo $this->donnerNom().PHP_EOL;
        echo "\t".$observateur->donnerNom()." vous à ajouté à ses contacts".PHP_EOL;
        $this->observateurs[] = $observateur;
    }
 
    public function detacher(ObservateurInterface $observateur): void
    {
        $key = array_search($observateur, $this->observateurs);
 
        if (false !== $key) {
            echo $this->donnerNom().PHP_EOL;
            unset($this->observateurs[$key]);
            echo "\tVous avez enlevé ".$observateur->donnerNom()." de vos contacts".PHP_EOL;
        }
    }
 
    public function notifier(): void 
    {
        foreach ($this->observateurs as $observateur) {
            $observateur->mettreAJour($this);
        }
    }

    public function donnerNom(): string
    {
        return $this->nom;
    }
    
    public function suivre(SujetInterface $sujet): void
    {
        // pour éviter qu'un membre puisse s'auto-suivre
        if ($sujet === $this) {
            return;
        }
        
        $this->sujets[$sujet->donnerNom()] = clone $sujet;
        $sujet->attacher($this);
    }
    
    public function nePlusSuivre(SujetInterface $sujet): void
    {
        echo $this->donnerNom().PHP_EOL;
    
        $key = array_search($sujet, $this->sujets);
 
        if (false !== $key) {
            unset($this->sujets[$key]);
        }
        
        echo "\tVous ne suivez plus ".$sujet->donnerNom().PHP_EOL;
    }
    
    public function mettreAJour(SujetInterface $sujet): void
    {
        echo $this->donnerNom().PHP_EOL;
        $nom = $sujet->donnerNom();
        
        if (array_key_exists($nom, $this->sujets)) {
            $sujetStocke = $this->sujets[$nom];
            
            if ($sujet instanceof PageCommerciale) {
                $urlMagasin = $sujet->donnerUrlMagasin();
                $urlMagasinStocke = $sujetStocke->donnerUrlMagasin();
                
                if ($urlMagasin !== $urlMagasinStocke) {
                    echo "\tLe site web de $nom vaut désormais $urlMagasin";
                }

            } elseif ($sujet instanceof Membre) {
                $age = $sujet->donnerAge();
                $ageStocke = $sujetStocke->donnerAge();

                if ($ageStocke !== $age) {
                    echo "\t$nom fête son anniversaire, il a $age ans";
                    $this->sujets[$nom] = clone $sujet;
                }
                
                $hobbies = $sujet->donnerHobbies();
                $hobbiesStocke = $sujetStocke->donnerHobbies();

                if ($hobbiesStocke !== $hobbies) {
                    echo "\t$nom a de nouveaux hobbies: ".implode(", ", $hobbies);
                    $this->sujets[$nom] = clone $sujet;
                }
            }
            echo PHP_EOL;
        }
    }
}

Les classes concrètes

On en compte deux, une pour les membres et une pour les pages commerciales. Un membre a un âge et des hobbies, une page commerciale ne comporte que l’URL du site marchand. Là aussi j’ai vraiment gardé l’essentiel dans chaque classe, qui est sur-simplifiée. Voici notre classe Membre:

final class Membre extends AbstractMembre {
 
    private $age;
     
    private $hobbies;
     
    public function changerAge(int $age): void {
        $this->age = $age;
        $this->notifier();
    }
     
    public function changerHobbies(array $hobbies): void {
        $this->hobbies = $hobbies;
        $this->notifier();
    }
 
    public function donnerAge(): ?int {
        return $this->age;
    }
    
    public function donnerHobbies(): ?array {
        return $this->hobbies;
    }
}

et voici notre classe PageCommerciale:

final class PageCommerciale extends AbstractMembre {
 
    private $urlMagasin;
     
    public function changerUrlMagasin(string $urlMagasin): void {
        $this->urlMagasin = $urlMagasin;
        $this->notifier();
    }
 
    public function donnerUrlMagasin(): ?string {
        return $this->urlMagasin;
    }
}

Chaque classe comporte des setters qui vont modifier l’état des objets et donc générer des notifications aux objets qui les observent.

Retour sur la méthode mettreAJour

    public function mettreAJour(SujetInterface $sujet): void
    {
        echo $this->donnerNom().PHP_EOL;
        $nom = $sujet->donnerNom();
        
        if (array_key_exists($nom, $this->sujets)) {
            $sujetStocke = $this->sujets[$nom];
            
            if ($sujet instanceof PageCommerciale) {
                $urlMagasin = $sujet->donnerUrlMagasin();
                $urlMagasinStocke = $sujetStocke->donnerUrlMagasin();
                
                if ($urlMagasin !== $urlMagasinStocke) {
                    echo "\tLe site web de $nom vaut désormais $urlMagasin";
                }

            } elseif ($sujet instanceof Membre) {
                $age = $sujet->donnerAge();
                $ageStocke = $sujetStocke->donnerAge();

                if ($ageStocke !== $age) {
                    echo "\t$nom fête son anniversaire, il a $age ans";
                    $this->sujets[$nom] = clone $sujet;
                }
                
                $hobbies = $sujet->donnerHobbies();
                $hobbiesStocke = $sujetStocke->donnerHobbies();

                if ($hobbiesStocke !== $hobbies) {
                    echo "\t$nom a de nouveaux hobbies: ".implode(", ", $hobbies);
                    $this->sujets[$nom] = clone $sujet;
                }
            }
            echo PHP_EOL;
        }
    }

Cette méthode est au niveau de la classe abstraite; elle fait une vérification sur le type concret des sujets car les informations à tirer depuis ce sujet ne seront pas les mêmes pour les observateurs concrets. Vous voyez arriver le problème si nous rajoutons des classes concrètes en bas de l’arbre d’héritage, votre if va enfler et il vous faudra peut-être avoir recours à un nouveau design pattern pour gérer ces différentes façons d’opérer selon les types concrets, lequel ?

Notez que nous clonons les objets; il nous faut fossiliser leur état à l’instant t pour pouvoir le comparer avec celui du sujet reçu en paramètre de la méthode.

Le code client

$magasin = new PageCommerciale('ACME Web Store');
$star = new Membre('Rasmus Lerdorf');
$copain = new Membre('Jean-Michel Apeuprey');
$autreStar = new Membre('Novak Djokovic');
$membre = new Membre('Sébastien Ferrandez');

$membre->suivre($magasin);
$membre->suivre($star);
$membre->suivre($autreStar);
$membre->suivre($copain);
$copain->suivre($membre);
$magasin->suivre($autreStar);
$copain->changerAge(41);
$membre->changerHobbies(["guitare"]);
$star->changerAge(36);
$star->changerHobbies(["linux", "c++", "c"]);
$autreStar->changerHobbies(["musculation", "karaoké"]);
$magasin->changerUrlMagasin('http://www.acmestore.com');
// Je n'aime plus le tennis !
$membre->nePlusSuivre($autreStar);
// Je me suis fâché avec Jean-Michel, il n'est plus mon ami !
$membre->detacher($copain);

TADAAAM !

Voici la sortie générée par l’exécution du code client, on n’est pas trop mal on dirait ! N’hésitez pas à proposer des alternatives ou poser des questions dans les commentaires !

Sortie écran pour les observateurs

Où trouve t-on des implémentations d’Observateur ?

Typiquement, dans la programmation par événements où des classes souscrivent auprès d’une classe gestionnaire d’évènements qui les informera lorsque l’évènement auquel elles ont souscrit se produit (en appelant parfois des fonctions de callback).

Lorsque vous cliquez sur un article pour l’ajouter et que votre panier se met à jour, vous avez un bel exemple d’Observateur.

Sur les réseaux sociaux, comme dans notre exemple trivial, nous recevons des notifications dès qu’un événement d’une personne que l’on suit se produit (changement de statut, nouveau post etc.)

En PHP, la SPL propose des classes pour mettre en place le design pattern Observateur avec SPLObserver.

Enfin dans Symfony, les évènements sont gérés selon ce principe…Les observateurs sont des listeners ou des event subscribers auquel on dispatche des notifications contenant des events.

PHP : classes abstraites et interfaces

Les interfaces, comme les classes abstraites, sont des unités de code dont la raison d’être est de contraindre une classe à implémenter des méthodes, c’est à dire à les concrétiser ! Nous allons voir en détail ces différentes manières de contraindre les comportements de nos classes.
connect

La classe abstraite

Une classe abstraite est avant tout une classe. Rien ne l’oblige à posséder des méthodes abstraites ! Elle peut même être vide :

abstract class EtreVivant {
}

Par contre, dès lors que vous mettez une méthode abstraite dans une classe, vous devez déclarer votre classe comme étant abstraite !

abstract class EtreVivant {
    abstract public function respirer();
}

Seules les fonctions membres de votre classe abstraite peuvent être abstraites, jamais les propriétés ! Une classe abstraite ne s’instancie pas ! Si d’aventure vous tentez d’instancier EtreVivant, voici ce qui va vous arriver :

PHP Fatal error:  Cannot instantiate abstract class EtreVivant

Si ma classe abstraite ne peut pas s’instancier, c’est donc que pour l’utiliser il ne me reste plus qu’une option : la dériver !

abstract class EtreVivant {
    abstract public function respirer();
}

class EtreHumain extends EtreVivant {
}

En l’état actuel de notre hiérarchie d’héritage, une erreur va se produire car EtreVivant nous impose d’écrire respirer(), même vide, ce que nous ne faisons pas !

PHP Fatal error:  Class EtreHumain contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (EtreVivant::respirer)
abstract class EtreVivant {
    abstract public function respirer();
}

class EtreHumain extends EtreVivant {
    public function respirer() {
        //TODO...
    }
}

Une classe abstraite peut forcer les classes qui la dérivent à implémenter des fonctions aux modes d’accès autre que public…protégé par exemple :

abstract class EtreVivant {
    abstract protected function _respirer();
}

class EtreHumain extends EtreVivant {
    protected function _respirer() {
        //TODO...
    }
}

Evidemment, aucun intérêt à mettre une méthode privée ET abstraite dans la classe abstraite…Une méthode privée n’est pas transmise dans les classes filles, elle ne peut être appelée que dans cette classe même, ce serait un grave contre-sens ! Si vous tentiez de le faire, vous seriez bien déçu(e)s :

PHP Fatal error:  Abstract function EtreVivant::_respirer() cannot be declared private

Nous avons dit précédemment qu’une classe abstraite ne pouvait pas être instanciée. Mais cependant, rien ne l’empêche d’avoir un constructeur ! C’est évidemment la classe concrète qui la dérive qui pourra l’utiliser :

abstract class Maman {
    public function __construct() {
        echo "Vous êtes chez Maman";
    }
}

class Fiston extends Maman {}
$fiston = new Fiston;

Quel intérêt ?

Vous utilisez des classes abstraites dès lors qu’un comportement est susceptible de varier selon la classe concrète dans laquelle il se trouve…Regardez notre classe EtreVivant : elle est générique, c’est ce que l’on nomme un super-type ! Sa méthode respirer() est elle aussi bien trop « vague », les êtres vivants respirent de bien des façons différentes ! Nous laissons donc le soin aux classes qui vont nous dériver de donner un corps à cette méthode, pire que ça, même: nous l’imposons !

abstract class EtreVivant {
    abstract public function respirer();
}

class EtreHumain extends EtreVivant {
    public function respirer() {
        echo 'Par le nez et/ou la bouche';
    }
}

class Plante extends EtreVivant {
    public function respirer() {
        echo 'Par la photo-synthèse';
    }
}

Il existe bel et bien une notion de contrainte entre une classe abstraite contenant des méthodes abstraites et sa descendance :

Si tu choisis d’être un EtreVivant, tu dois savoir respirer, peu m’importe comment !

Une classe abstraite peut constituer un début d’implémentation : elle possède des propriétés (un état) factorisables à son niveau et des méthodes (un comportement) qui amorcent une implémentation. Par exemple, nous avons rajouté dans la classe abstraite une méthode mourir qui fait appel à une sous-méthode _cesserFonctionsVitales qui sera implémentée dans les classes dérivées :

abstract class EtreVivant {
    protected $_age = 0;
	protected $_poids = 0;

	public function mourir() {
		$this->_cesserFonctionsVitales();
	}

    abstract public function respirer();
    abstract protected function _cesserFonctionsVitales();
}

class EtreHumain extends EtreVivant {
    protected $_nationalite;

    public function respirer() {
        echo 'Par le nez et/ou la bouche';
    }

    protected function _cesserFonctionsVitales() {
        echo 'le coeur s\'arrête de battre...AAAARGH';
    }
}

class HumainFrancais extends EtreHumain {
    public function __construct() {
        $this->_nationalite = 'français';
    }
}

$francais = new HumainFrancais;
$francais->mourir();

Une classe abstraite dérivant une autre classe abstraite n’est pas forcée d’implémenter les méthodes abstraites de sa mère, vu qu’elle est elle-même abstraite…Par contre la première classe concrète rencontrée dans l’arbre d’héritage en supportera les conséquences ! Ceci va fonctionner car les deux classes sont abstraites (attention aux longues lignées d’héritage et aux trop nombreux niveaux d’abstractions – à partir de 3 – qui sont en général le signe d’une conception de piètre qualité) :

abstract class EtreVivant {
    protected $_age = 0;
	protected $_poids = 0;

	public function mourir() {
		$this->_cesserFonctionsVitales();
	}

    abstract public function respirer();
    abstract protected function _cesserFonctionsVitales();
}

abstract class EtreHumain extends EtreVivant {
}

si vous déplacez la responsabilité de l’implémentation dans la première classe rencontrée dans l’arbre d’héritage, vous obtiendrez le code suivant :

abstract class EtreVivant {
    protected $_age = 0;
	protected $_poids = 0;

	public function mourir() {
		$this->_cesserFonctionsVitales();
	}

    abstract public function respirer();
    abstract protected function _cesserFonctionsVitales();
}

abstract class EtreHumain extends EtreVivant {
    protected $_nationalite;

}

class HumainFrancais extends EtreHumain {

    public function respirer() {
        echo 'Par le nez et/ou la bouche';
    }

    protected function _cesserFonctionsVitales() {
        echo 'le coeur s\'arrête de battre...AAAARGH';
    }

    public function __construct() {
        $this->_nationalite = 'français';
    }
}

$francais = new HumainFrancais;
$francais->mourir();

Mais quelle abomination serait un code pareil ! Tout est fourré à grands coups de pied dans la classe concrète HumainFrancais…Si demain nous devions créer HumainCroate, nous devrions dupliquer bêtement (pléonasme) le code contenu dans cette classe.

L’interface

Les interfaces servent à passer des contrats avec des classes, elles impliquent la même notion de contrainte que les classes abstraites (« Si tu veux être comme moi, tu dois faire comme moi ! »). Mais l’interface est un mécanisme plus simple : ce n’est pas une classe, donc inutile de l’instancier ou d’en hériter ! Puis, même si elle peut contenir des constantes comme dans une classe abstraite (ou pas), les fonctions qu’elle impose sont TOUJOURS en mode d’accès public. Le mot clé pour utiliser une interface est implements.

interface AnimalNageur {
	public function nager();
}

abstract class EtreVivant {
    protected $_age = 0;
	protected $_poids = 0;

	public function mourir() {
		$this->_cesserFonctionsVitales();
	}

    abstract public function respirer();
    abstract protected function _cesserFonctionsVitales();
}

class EtreHumain extends EtreVivant implements AnimalNageur {
    protected $_nationalite;

    public function respirer() {
        echo 'Par le nez et/ou la bouche';
    }

    protected function _cesserFonctionsVitales() {
        echo 'le coeur s\'arrête de battre...AAAARGH';
    }

	public function nager() {
		echo 'Je peux aussi nager si on m\'apprend';
	}
}

class HumainFrancais extends EtreHumain {
    public function __construct() {
        $this->_nationalite = 'français';
    }
}

$francais = new HumainFrancais;
$francais->nager();
$francais->mourir();

Dans cet exemple, la classe EtreHumain implémente l’interface AnimalNageur, car un être humain peut nager (je ne dis pas qu’il sait nager…). Si elle implémente cette interface alors elle doit honorer le contrat qui la lie à présent avec AnimalNageur : donner « vie » à la méthode publique nager.

Une classe peut implémenter plusieurs interfaces :

interface AnimalNageur {
	public function nager();
}

interface AnimalCoureur {
	public function courir();
}

abstract class EtreVivant {
    protected $_age = 0;
	protected $_poids = 0;

	public function mourir() {
		$this->_cesserFonctionsVitales();
	}

    abstract public function respirer();
    abstract protected function _cesserFonctionsVitales();
}

class EtreHumain extends EtreVivant
                 implements AnimalNageur, AnimalCoureur {
    protected $_nationalite;

    public function respirer() {
        echo 'Par le nez et/ou la bouche';
    }

    protected function _cesserFonctionsVitales() {
        echo 'le coeur s\'arrête de battre...AAAARGH';
    }

	public function nager() {
		echo 'Je peux aussi nager si on m\'apprends';
	}

	public function courir() {
		echo 'Je passe une jambe devant l\'autre, très vite';
	}
}

class HumainFrancais extends EtreHumain {
    public function __construct() {
        $this->_nationalite = 'français';
    }
}

$francais = new HumainFrancais;
$francais->nager();
$francais->courir();
$francais->mourir();

EtreVivant implémente dorénavant AnimalNageur et AnimalCoureur, il implémente donc l’ensemble des fonctions imposées par ces deux interfaces (qui ont une méthode chacune).

Les interfaces et l’héritage

Les interfaces peuvent hériter d’autres interfaces et la grande différence par rapport aux classes c’est qu’elles peuvent hériter « en losange » :

interface A {
    public function a();
}

interface B {
    public function b();
}

interface C extends A, B {
    public function c();
}

class D implements C {
    public function a() {
    }
    public function b() {
    }
    public function c() {
    }
}

L’interface C « dérive » les interfaces A et B, c’est à dire qu’une classe qui implémente l’interface C va devoir écrire a(),b() et c().

Faut-il privilégier les interfaces ou les classes abstraites ?

Une fois de plus, il n’y a pas de méthode toute prête de conception, c’est à vous de voir, mais tâchons tout de même de voir les différences fondamentales :

  • une classe abstraite oblige à la dériver pour bénéficier de ses fonctionnalités: les méthodes « enfermées » dans une classe abstraite forcent l’héritage, ce qui n’est pas nécessairement bon
  • les méthodes « sorties » dans une ou des interfaces favorisent la ré-utilisabilité (si on enferme la méthode courir dans EtreHumain, seuls les êtres humains pourront courir…Si nous écrivons une interface AnimalCoureur avec cette méthode, une autruche pourra l’implémenter et courir à son tour, sans aboutir à l’ineptie qui consisterait pour Autruche à dériver EtreHumain pour pouvoir courir)

Résumé

  • Une classe abstraite peut contenir du code, pas une interface. Vue sous cet angle,
    une interface est 100% abstraite, elle ne contient que des prototypes (ou signatures)
    de fonctions publiques
  • Une interface, comme une classe, peut posséder des constantes
  • Une interface s’implémente, une classe s’instancie ou se dérive
  • Une classe peut implémenter plusieurs interfaces mais ne peut spécialiser qu’une seule classe (en PHP)
  • Une interface peut être implémentée par plusieurs classes
  • Les méthodes d’une interface sont forcément publiques, celles d’une classe abstraite peuvent être de tout type, comme dans une classe normale et uniquement publiques ou protégées si elles sont abstraites