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

7 réflexions sur « PHP : classes abstraites et interfaces »

  1. julien

    Super !

    Petite coquille il me semble :
    C’est EtreHumain qui implémente dorénavant AnimalNageur et AnimalCoureur, pas EtreVivant.

    Répondre

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.