Skip to content

Instantly share code, notes, and snippets.

@mageekguy
Last active August 29, 2015 13:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mageekguy/9942188 to your computer and use it in GitHub Desktop.
Save mageekguy/9942188 to your computer and use it in GitHub Desktop.

Imaginons un systéme de billeterie obéissant aux rêgles suivantes :

  1. Un client peut acheter de 1 à n ticket;
  2. Un ticket est valable pour une et une seule personne;
  3. Le prix du ticket doit être 2 si la personne à moins de 4 ans, 10 si la personne à moins de 12 ans et 30 pour toute personne agée de plus de 12 ans.

Il nous est demandé de développer le code correspondant à cet ensemble de régles en obéissant aux paradigme de la programmation orientée objet.
Une implémentation possible pourrait être la suivante :

<?php

namespace billeterie;

class date
{
	/* ... */
}

class birthdate extends date
{
	/* ... */

	public function getAge()
	{
		/* ... */
	}
}

class personn
{
	private $birthdate = null;

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

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

class order
{
	private $personns = null;

	public function addPerson(personn $personn)
	{
		$this->personns[] = $personn;

		return $this;
	}

	public function getPersonns()
	{
		return $this->personns;
	}
}

class tarification
{
	public function getPersonPrice(person $personn)
   {
		switch (true)
		{
		  case $this->isConsideredABaby($personn):
			  return 2;

		  case $this->isConsideredAChild($personn):
			  return 10;

		  default:
			  return 30;
		}
	}

   protected function isConsideredABaby(personn $personn)
   {
		return $person->getAge() < 4;
   }

   protected function isConsideredAChild(personn $personn)
	{
		return $person->getAge() < 12;
   }
}

class quotation
{
	private $tarification = null;

	public function __construct(tarification $tarification = null)
	{
		$this->tarification = $tarification ?: new tarification();
	}

	public function getPrice(order $order)
	{
		$price = 0;

		foreach ($order->getPersonns() as $personn)
		{
			$price += $this->tarification->getPersonnPrice(personn);
		}

		return $price;
	}
}

echo (new quotation())->getPrice((new order())->addPersonn(new personn(…))->addPersonn(new personn(…))->addPersonn(new personn(…)));

Cette solution définie une classe nommée quotation dont la responsabilité est de calculer le prix de la commande représentée par la classe order en fonction d'une tarification.
Cette tarification est modélisée à l'aide de la classe éponyme qui fait appel à des méthodes protégées pour définir le prix, le développeur ayant anticipé une éventuelle évolution des règles de calcul.
Cependant, une autre implémentation pourrait être :

<?php

namespace billeterie;

class ticket
{
	private $price = 0;

	public function setPrice($price)
	{
		$this->price = $price;

		return $this;
	}
}

class date
{
	/* ... */
}

class birthdate extends date
{
	/* ... */

	public function getAge()
	{
		/* ... */
	}
}

class personn
{
	public function __construct(birthdate $birthdate)
	{
		$this->birthdate = $birthdate;
	}

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

	public function createTicket(ticket $ticket = null)
	{
		if ($ticket === null)
		{
			$ticket = new ticket();
		}

		$age = $this->getAge();

		switch (true)
		{
			case $age < 4:
				return $ticket->setPrice(2);

			case $age < 12:
				return $ticket->setPrice(10);

			default:
				return $ticket->setPrice(30);
		}
	}

	public function getTicketPrice(ticket $ticket = null)
	{
		return $this->createTicket($ticket)->getPrice();
	}
}

class order
{
	private $personns = array();

	public function addPerson(personn $personn)
	{
		$this->personns[] = $personn;

		return $this;
	}

	public function getPrice()
	{
		$price = 0;

		foreach ($this->personns as $personn)
		{
			$price += $personn->getTicketPrice();
		}

		return $price;
	}
}

echo (new order())
	->addPersonn(new personn(…))
	->addPersonn(new personn(…))
	->addPersonn(new personn(…))
		->getPrice();

La philosophie de cette solution est complétement différente de la première conceptuellement parlant.
En effet, le tarificateur n'existe plus car il n'y en a pas besoin, le calcul du prix étant délégué à la fois à la classe order et à la classe personn.
Alors, quelle est la meilleure solution ?
Pour répondre à cette question, imaginons une évolution des règles de calculs.
Ainsi, le prix d'un billet est maintenant de 45 si la personne a entre 35 et 65 ans et le billet est gratuit le jour de l'anniversaire de la personne.
En outre, si la commande est réalisée avant le 01/07, alors une réduction de 50% doit être appliquée.
Dans le cas de la première solution, le code doit être remanié de la manière suivante :

class date
{
	/* ... */
}

class birthdate extends date
{
	/* ... */

	public function getAge()
	{
		/* ... */
	}
}

class personn
{
	private $birthdate = null;

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

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

class order
{
	private $personns = null;

	public function addPerson(personn $personn)
	{
		$this->personns[] = $personn;

		return $this;
	}

	public function getPersonns()
	{
		return $this->personns;
	}
}

class tarification
{
	public function getPersonPrice(personn $personn)
   {
		$price = 0;

		switch (true)
		{
		  case $this->isConsideredABaby($personn):
			 $price = 2;

		  case $this->isConsideredAChild($personn):
			  $price = 10;

		  case $this->hasAgeBetween35And65($personn):
			  $price = 45;

		  default:
			  $price = 30;
		}

		return $this->isBirthdate($personn) ? 0 : $price;
	}

	public function getOrderPrice(order $order, $price)
	{
		$now = new date();

		if ($now->getMonth() < 7)
		{
			$price /= 2;
		}

		return $price;
	}

   protected function isConsideredABaby(person $personn)
   {
		return $person->getAge() < 4;
   }

   protected function isConsideredAChild(person $personn)
	{
		return $person->getAge() < 12;
   }

	protected function hasAgeBetween35And65(personn $personn)
	{
		return $person->getAge() >= 35 && $personn->getAge() <= 65;
	}

	protected function isBirthdate(personn $personn)
	{
		$now = new date();

		return $person->getBirthdate()->getDay() == $now->getDay() && $person->getBirthdate()->getMonth() == $now->getMonth();
	}
}

class quotation
{
	private $tarification = null;

	public function __construct(tarification $tarification = null)
	{
		$this->tarification = $tarification ?: new tarification();
	}

	public function getPrice(order $order)
	{
		$price = 0;

		foreach ($order->getPersonns() as $personn)
		{
			$price += $this->tarification->getPersonnPrice(personn);
		}

		return $this->tarification->getOrderPrice($order, $price);
	}
}

Dans le cas de la première solution, il est donc nécessaire d'intervenir :

  1. Au niveau de la classe quotation pour pouvoir gérer la nouvelle règle relative à la date de la commande;
  2. Au niveau de la classe tarification pour pouvoir gérer les nouvelles règles relative à la date de la commande et à l'âge des personnes;

L'évolution nécessaire au niveau de la seconde solution serait :

<?php

namespace billeterie;

class ticket
{
	private $price = 0;

	public function setPrice($price)
	{
		$this->price = $price;

		return $this;
	}
}

class date
{
	/* ... */
}

class birthdate extends date
{
	/* ... */

	public function getAge()
	{
		/* ... */
	}
}

class personn
{
	public function __construct(birthdate $birthdate)
	{
		$this->birthdate = $birthdate;
	}

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

	public function createTicket(ticket $ticket = null)
	{
		$ticketPrice = 0;

		if ($ticket === null)
		{
			$ticket = new ticket();
		}

		$age = $this->getAge();

		switch (true)
		{
			case $age < 4:
				$ticketPrice = 2;
				break;

			case $age < 12:
				$ticketPrice = 10;
				break;

			case $age >= 35 && $age <= 65:
				$ticketPrice = 45;
				break;

			default:
				$ticketPrice = 30;
		}

		return $ticket->setPrice($ticketPrice);
	}

	public function getTicketPrice(ticket $ticket = null)
	{
		return $this->createTicket($ticket)->getPrice();
	}
}

class order
{
	private $personns = array();

	public function addPerson(personn $personn)
	{
		$this->personns[] = $personn;

		return $this;
	}

	public function getPrice()
	{
		$price = 0;

		foreach ($this->personns as $personn)
		{
			$price += $personn->getTicketPrice();
		}

		$now = new date();

		return ($now->getMonth() < 7 ? $price / 2 : $price);
	}
}

Dans le cas de la seconde solution, il est donc nécessaire d'intervenir :

  1. Au niveau de la classe order pour pouvoir gérer la nouvelle règle relative à la date de la commande;
  2. Au niveau de la classe personn pour pouvoir gérer la nouvelle règle à l'âge des personnes;

Les modification à effectuer dans le cas de la seconde solution sont donc à la fois moins complexes et plus logiques puisque seul les classes concernées par les nouvelles règles sont modifiées.
De plus, la classe tarificateur de la première solution devient une classe monolithique et fourre-tout, puisqu'elle a pour responsabilité de calculer à la fois le prix en fonction des personnes qui auront des billets, mais aussi en fonction de la date de la commande.
Par ailleurs, la première solution implique d'exposer fortement dans la classe tarification les propriétés de la classe personn.
Le couplage entre ces deux classes est donc très fort et il y a de plus une rupture de l'encapsulation.
De plus, si le nombre de propriétés de la classe order ou personn devant être pris en compte par tarificateur augmente, le code de cette classe va devenir de plus en plus complexe.
J'ajoute que si le tarificateur est amené à utiliser des instances de la classe personn et des instances d'une ou plusieurs classes dérivant de personn, donc avec potentiellement des propriétés supplémentaires, il va être très difficile de conserver un code générique au niveau de la classe tarificateur.
Enfin, vu que les propriétés de order ou personn sont très largement exposées dans la classe tarificateur, toute modification de l'API (ce qui n'arrive jamais, les BC break étant des légendes urbaines) de ces deux classes devra y être répercutée.
La seconde solution respecte quand à elle l'encapsulation car aucune propriété des différentes classes n'est exposé au cours du calcul et le couplage est très faible.
En terme de programmation orientée objet, la secode solution est donc de meilleure qualité, car la première est en effet une utilisation procédurale de la programmation orientée objet.
Comme le dit Alan Sharp :

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

@mageekguy
Copy link
Author

Bientôt tu vas me dire que ton tarificateur ne demande pas l'âge de la personne, aussi, via personn::getAge() ?
Et qu'il ne décide pas du prix en fonction ? Donc que son état ne change pas (ie $price ne change pas de valeur) ?
Et que pour toutes les autres règles que tu as énoncé, il ne vas pas avoir besoin de demander des informations à order, personn, etc ?

@geraldcroes
Copy link

Bientôt tu vas me dire que ton tarificateur ne demande pas l'âge de la personne, 
aussi, via personn::getAge() ? Et qu'il ne décide pas du prix en fonction ? Donc que 
son état ne change pas (ie $price ne change pas de valeur) ? Et que pour toutes les autres 
règles que tu as énoncé, il ne vas pas avoir besoin de demander des informations à order, 
personn, etc ?

Lorsque tu seras reposé, relis la phrase en entier.

Le problème est que en tant que appelant, vous ne devriez pas prendre de décisions 
basées sur l'état de l'objet qui     résultent en la modification de l'état du même l'objet.

Il est toutefois légitime de regarder l'état d'un objet pour prendre une décision sur un autre sujet.

 La circulation est importante ? je prends le train. => je ne modifie pas la circulation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment