Voici par exemple le code qui m’a été présenté dans l’optique de justifier le test des méthodes protégées :

interface Personn
{
	public function getBirthdate();
}

class Francais implements Person
{
	private $birthdate = null;

	public function __construct(JulienDate $date)
	{
		$this->birthdate = $date;
	}

    public function getBirthdate()
    {
		 return $this->birthdate;
    }
}


class DisneyLandParisPricingSystem implements TicketPricingSystem
{
    public function getPrice(Person $person)
    {
		 $now = new julianDate();

        if ($now - $person->getBirthdate() <= 2) {
            return 0;
        } elseif ($now - $person->getBirthdate() <= 4) {
            return 10;
        } else {
            return 30;
        }
    }
}

Il n’a rien d’exceptionnel et il est certainement semblable à celui que vous ou moi avons pu écrire ou écrivons quotidiennement.

Pourtant, il ne s’agit pas de programmation orientée objet.

En effet, ce style de programmation a pour but de masquer aux yeux du monde le fonctionnement interne d’un objet.

Un objet est donc une boite noire capable d’effectuer des actions en fonction de son état interne et des informations qui lui sont communiquées.

La boite noire est donc la classe, les actions qu’elle peut effectuer sont définies par les méthodes de cette classe, tandis que son état interne est représenté par les propriétés de la classe.

Or, dans le code ci-dessous, une propriété de la classe, donc une information relative à l’état interne de la boite noire est utilisée dans une méthode externe à la classe.

En effet, la date de naissance de la personne est utilisée pour calculer son âge afin de décider de la valeur d’une variable.

La boite noire qu’est censée être l’objet ne l’est donc plus réellement, puisqu’elle expose des informations sur sa structure interne.

Donc, même si ce code atteint son objectif, à savoir calculer une valeur en fonction de l’âge de la personne, il ne s’agit pas de programmation orientée objet, mais bien de programmation procédurale, puisqu’il y a rupture de l’encapsulation.

Il arrive cependant que certains ne soient pas convaincus par cet argument, et il est donc nécessaire de pousser la démonstration un cran plus loin.

Quel est le résultat fourni par le code précédent si la valeur de la date de naissance est modifiée entre le premier et le second appel à Person::getBirthdate() ?

Car même si le code ne semble pas le permettre, c’est tout de même possible, puisqu’il suffit pour cela que l’âge soit par exemple stocké dans un fichier, une base de données ou tout autre contenant modifiable indépendamment de ce code.

Si nous supposons par exemple que le premier appel permet de calculer un âge de 10 ans, puis que le second permet de calculer un âge de 1 an, car quelqu’un a entre temps édité le fichier ou la base de données correspondante pour par exemple corriger une faute de frappe, la méthode va retourner la valeur 10 au lieu de 30.

On va alors me répondre que pour éviter cela, il suffit de n’appeler qu’une seule fois la méthode Person::getBirthdate() et de stocker sa valeur de retour dans une variable.

C’est tout à fait exact, mais encore faut-il y penser, et y penser systématiquement à chaque fois qu’on utilise cette méthode pour prendre une décision…

De plus, le nom de la méthode laisse à penser qu’elle renvoie la date de naissance, mais rien n’impose le type dans lequel elle sera exprimée.

En fonction des différentes implémentations possibles de l’interface Person, la date de naissance pourra donc être exprimée dans un calendrier qui n’est pas celui utilisé par DisneyLandParisPricingSystem.

Et dans ce cas, le résultat de la méthode DisneyLandParisPricingSystem ::getPrice() pourra être erroné :

class Maya implements Person
{
	private $birthdate = null;

	public function __construct(MayaDate $date)
	{
		$this->birthdate = $date;
	}

    public function getBirthdate()
    {
		 return $this->birthdate;
    }
}

De plus, rien n’impose à Person ::getBirthdate() de renvoyer un objet, et elle pourrait donc très bien renvoyer un timestamp UNIX, par exemple, ou le nombre de jour écoulé depuis l’an 0.

Certes, il est possible de déléguer ce calcul à des méthodes qui s'appeleraient par exemple Person::isBaby(), Person::isChild() ou Person::isAdult(), mais outre le fait qu'il va être difficile de trouver un nom pour une méthode permettant de détecter les personnes ayant entre 37 et 42 ans, seul la bonne volonté de leur développeur fera qu'elles renverront effectivement un booléen.

De plus, au fur et à mesure de l'évolution de vos règles de tarification, il faudra ajouter de plus en plus de méthodes de ce type à l'interface Person, donc les implémenter systèmatiquement pour chaque classe qui l'implémentera.

Donc en admettant que votre système de tarification utilise 20 critères, il faudra implémenter 20 méthodes…

Or, il se trouve que si la classe Person n’expose pas son état interne via la méthode getBirthdate(), tous ces problèmes ne se posent plus :


class Maya implements Person
{
	private $birthdate = null;

	public function setBirthdate(MayaDate $date) // Oui, Maya, pourquoi pas :)
	{
		$this->birthdate = $date;

		return $this;
	}

	public functionc giveInformationsTo(DisneyLandParisPricingSystem $pricingSystem)
	{
		$pricingSystem->setBirthdate($this->birthdate->toJulianDate());
	}
}

class Français implements Person
{
	private $birthdate = null;

	public function setBirthdate(JulianDate $date)
	{
		$this->birthdate = $date;

		return $this;
	}

	public functionc giveInformationsTo(DisneyLandParisPricingSystem $pricingSystem)
	{
		$pricingSystem->setBirthdate($this->birthdate);
	}
}

class DisneyLandParisPricingSystem implements TicketPricingSystem
{
	private $birthdate = null;

	public function setBirthdate(JulianDate $date)
	{
		$this->birthdate = $date;

		return $this;
	}

	public function getPrice(Personn $person)
	{
		$person->giveInformationTo($this);

		$age = $this->computeAge();

	        if ($age <= 2) {
			return 0;
		} elseif ($age <= 4) {
			return 10;
		} else {
 			return 30;
		}
	}

	private function computeAge()
	{
		return new julianDate() - $this->birthdate;
	}
}

Avec cette solution qui suit les paradigmes de la programmation orientée objet, les problèmes évoqués précédemment n’existent plus.

En effet, c’est la classe implémentant Person qui se charge de renseigner la classe chargée de calculer le prix du billet et le développeur n’a ainsi plus à se soucier d’appeler une seule fois une méthode pour être certain d’avoir un résultat cohérent.

De plus, le système de tarification exprime ses besoins à l’aide du typage des arguments de ses méthodes, puisque DisneyLandParisPricingSystem::setBirthdate() précise qu’elle n’accepte que les dates au format julien.

Le problème posé par le calendrier utilisé pour exprimer l’âge disparait donc automatiquement et de plus, les classes implémentant l’interface Person sont à même de faire les éventuelles conversions nécessaires au diaglogue avec une instance de DisneyLandParisPricingSystem.

Think declaratively instead of procedurally!

Enfin, cerise sur le gâteau, si les règles de tarification évoluent, pour par exemple prendre en compte le fait que la personne est handicapée, il suffit de faire évoluer DisneyLandParisPricingSystem pour que les instances de Person soit à même de lui fournir l'information ou non en fonction de leur état :

class Handicape implements Person
{
	private $birthdate = null;

	public function setBirthdate(JulianDate $date)
	{
		$this->birthdate = $date;

		return $this;
	}

	public functionc giveInformationsTo(DisneyLandParisPricingSystem $pricingSystem)
	{
		$pricingSystem
			->setBirthdate($this->birthdate)
			->setHandicape(true)
		;
	}
}

class DisneyLandParisPricingSystem implements TicketPricingSystem
{
	private $birthdate = null;
	private $handicape = false;

	public function setBirthdate(JulianDate $date)
	{
		$this->birthdate = $date;

		return $this;
	}

	public function setHandicape($boolean)
	{
		$this->handicape = ($boolean == true);
	}

	public function getPrice(Personn $person)
 	{
		$person->giveInformationTo($this);

		$age = $this->computeAge();

        	if ($age <= 2) {
            		return 0;
        	} elseif ($age <= 4) {
            		return 10;
        	} else {
            		return ($this->handicape ? 15 : 30);
        	}
	}

	private function computeAge()
	{
		return new julianDate() - $this->birthdate;
	}
}

Lorsque notre code demande une information à une instance de classe et prend une décision en fonction, nous ne faisons donc pas de la programmation orientée objet.

Afin de profiter des avantages de cette dernière, il est bien plus efficace de respecter ses paradigmes en n’exposant pas les propriétés de nos objets aux yeux de l’extérieur et en demandant à nos objets de faire des actions.

En anglais, ce concept est connu sous le nom de Tell, Don't as.

Procedural code gets information then makes decisions. Object-oriented code tells objects to do things.

PS : Cet article est également une bonne ressource, en plus d'être un excellent exercice pour améliorer ses compétences en programmation orientée objet.