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

Je n'ai pas de citation pour terminer ma réponse, mais il me semble tout de même que exemple 1 est bien plus souple que exemple 2.
Sauf si tu arrives à adresser les problèmes cités de façon plus élégante.

Job done.

@geraldcroes
Copy link

Si la tarification est gratuite pour le 3ème enfant de la commande, que fais-tu ? (pour exemple 1, 
quotation ne tarifiera que les 3 premièr enfants) 

$this->personns
           ->select(function($personn) { return $personn->getAge() < 18; }, 3)

Dans cette solution (qui est du coup globalement identique à celle qu'on pourrait mettre dans l'exemple 1, tu utilises une technique que tu juges mauvaise, à savoir "un objet tiers utilise des données exposées par personne pour décider". (Si j'ai bien compris).

Si tel est le cas, pourquoi serait-ce ici justifié, et pas dans l'objet "tarificateur" ?

On voit bien qu'il est légitime pour un objet tiers de manipuler l'interface publique d'un autre, c'est le principe même de l'interface publique.

@mageekguy
Copy link
Author

La signature n'est pas changée puisque le nouveau paramètre étant optionnel, la rétro-compatibilté est conservé.
De plus, ce nouveau paramètre fait sens vu que le prix dépend/est fonction du nombre de personnes accompagnant la personne.
J'ajoute que le prix de la commande étant fonction du nombre de personne composant le groupe, on pourrait (et on devrait) en fait déléguer ce calcul à la collection de personnes.
La méthode order::getPrice()deviendrait alors :

class order
{
    /* ... */

    public function getPrice()
    {
        $price = $this->personns->getPrice($this->date);

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

@geraldcroes
Copy link

En fonction des saisons ou de périodes de promotions le tarif enfant est divisé par 2

    Cela implique de définir une classe promotion et d'ajouter dans la classe order la date de la réservation 
    ainsi que le code permettant de discounter le prix du ticket dans la classe ticket.

$this->personns
          ->select(function($personn) { return $personn->getAge() < 18; }, 3)
           ->apply(function($child) use (& $price, $groupSize) {
              $ticket = $child->createTicket(null, $groupSize);
              $this->promotions->apply(function($promotion) use ($ticket) { $promotion->discount($ticket, $this->date); });
              $price += $ticket->getPrice();
           }
        )
    }

Ici, tu as donc une personne qui pense connaitre le prix de son ticket, mais qui en fait ne le connais pas car sa connaissance est "partielle".
=> C'est deux euros
==> Non non monsieur, c'est 1

On voit bien que la classe personne "ne sait plus"

Outre le fait que le code devient nettement plus compliqué que le suivant (ou n'importe qui devinne le prix en moins de 10 secondes.

class tarification
{
    public function getPersonPrice(person $personn)
    {
        if (this->isConsideredABaby($personn)) {
          if ($this->promotionManager->promotionActive()) 
             return 1;
          } else {
             return 2;
          }
        } elseif ($this->isConsideredAChild($personn)) {
          return 10;
        } else {
          return 30;
        }
    }

@mageekguy
Copy link
Author

Je ne dis pas qu'il est illégitime de manipuler une interface publique, je dis que les règles permettant de calculer un prix sont de la responsabilité de l'entité qui doit être tarifée et non de celle d'un objet indépendant qui doit exploiter tout ou partie de leur propriété interne pour le faire.
Dans le code dont tu parles, je demande à ma collection de sélectionner tous ses éléments correspondant à un certain critères puis de leur appliquer la fonction passée en argument, et je ne parcours pas ma collection pour interroger chacun de ces éléments afin de savoir quoi faire.
En clair, je demande à l'objet de faire le travail, et je ne le fais pas pour lui.
C'est exactement la même philosophie que l'encapsulation du calcul du tarif, je n'interroge pas les caractéristiques de mes objets pour ensuite en déduire leur tarif, je leur demande de me donner leur tarif, tout comme je demande à ma collection de se débrouiller pour appliquer une fonction uniquement sur les objets qui m'intéresse.
Pour plus d'information, cet article est à mon sens la meilleure référence en la matière en ce qui concerne la différence entre commande et requête.

@geraldcroes
Copy link

La signature n'est pas changée puisque le nouveau paramètre étant optionnel

La signature change, que le paramètre soit optionnel ou non. Si tu as une interface et que dans le contrat tu définis un paramètre optionnel, tes implémentations devront implémenter cette nouvelle signature (avec paramètre optionnel en plus).

, la rétro-compatibilté est conservé.

Le code continue de tourner en effet.

Par contre, c'est très dangereux car un code client qui ne sait pas que, maintenant, il faut aussi indiquer "combien on est au global" pour bénéficier d'une réduction... et bien il ne bénéficiera pas de la réduction, et obtiendra un faux tarif.

Donc je dis à personne de calculer le tarif, et je lui dit un peu "comment".

J'ajoute que le prix de la commande étant fonction du nombre de personne composant le groupe, 
on pourrait (et on devrait) en fait déléguer ce calcul à la collection de personnes.

Oui ! Et comme du nombre de personne dépendent les tranches de prix, on sort de personne la connaissance du prix des tranches, et on en revient toujours à l'exemple 1 ou un objet tiers détermine le prix des tranches "en fonction" (entre autres) de leur age / nombre.

@mageekguy
Copy link
Author

La personne peut connaître son tarif hors promotion relative à la configuration de la tarification au moment de la commande, et comme le prix final dépend à la fois de la date de la commande et des caractéristiques de la personne, ça n'a rien de choquant.

Oui ! Et comme du nombre de personne dépendent les tranches de prix, on sort de personne la connaissance du prix des
tranches, et on en revient toujours à l'exemple 1 ou un objet tiers détermine le prix des tranches "en fonction" (entre autres) de
leur age / nombre.

Non, le code de la collection personns serait alors :

   public function getPrice(\date $date, \collection $promotions)
   {
      $price = 0;

      $this
           ->select(function($personn) use ($promotions) { return $personn->getAge() < 18; }, 3)
            ->apply(function($child) use (& $price) {
                  $ticket = $child->createTicket(null, sizeof($this));
                  $promotions->apply(function($promotion) use ($ticket) { $promotion->discount($ticket, $this->date); });
                  $price += $ticket->getPrice();
               }
            )
     ;

     $this
        ->select(function($personn) { return $personn->getAge() >= 18; })
           ->apply(function($adult) use (& $price) { {$price += $adult->getTicketPrice(); })
     ;

     return $price;
  }

Enfin, concernant la modification de signature, la solution 2 n'utilisant pas d'interface, le problème ne se pose pas et de toute façon, le nombre de personne EST un paramètre de la fonction permettant de calculer le prix (le x de f(x)).

@geraldcroes
Copy link

je dis que les règles permettant de calculer un prix sont de la responsabilité de l'entité 
qui doit être tarifée  et non de celle d'un objet indépendant qui doit exploiter tout ou partie 
de leur propriété interne pour le faire.

Je vais transposer l'exemple à une machine à laver pour montrer la situation sous un autre angle.

Cela donne :

je dis que les règles permettant de rendre propre sont de la responsabilité du pantalon qui doit
être lavé et non de celle de la machine à laver qui doit exploiter la lessive 
pour laver le pantalon s'il est en laine ou en coton, en couleurs vives ou non.

@geraldcroes
Copy link

Enfin, concernant la modification de signature, la solution 2 n'utilisant pas d'interface, le problème ne se pose pas

Mais tu es bien d'accord qu'il aurait fallu, en toute rigueur, disposer d'une interface ? (pour permetre de modifier les implémentations, et transmettre par exemple une carte d'identité de la personne (qui contient les infos), et non la personne elle même (qui contient aussi les infos))

A moins que tu ne conseille de ne pas utiliser d'interface ?

et de toute façon, le nombre de personne EST un paramètre de la fonction permettant de calculer le prix (le x de f(x)).

Donc tu admet modifier la signature ? (alors que l'exemple 1, à son bénéfice, ne modifie pas la signature)

@mageekguy
Copy link
Author

Parlons maitenant des tests.
Voici le test unitaire de la méthode order::getPrice() écrit avec atoum :

public function testGetOrder()
{
    $this->given($this->newTestedInstance($date = new \mock\date())
        ->then
            ->float($this->testedInstance->getPrice())->isZero

        ->if(
            $this->testedInstance
                ->setPersonns($personns = new \mock\billeteries\personns()),
                ->setPromotions($promotions = new \mock\billeterie\collection()),
            $this->calling($personns)->getPrice = $price = (float) rand(1, PHP_INT_MAX),
            $this->calling($date)->getMonth = rand(8, 12)
        )
        ->then
            ->float($this->testedInstance->getPrice())->isEqualTo($price)
            ->mock($personns)->call('getPrice')->withArguments($date, $promotions)->once

        ->if($this->calling($date)->getMonth = rand(1, 7))
        ->then
            ->float($this->testedInstance->getPrice())->isEqualTo($price / 2)
            ->mock($personns)->call('getPrice')->withArguments($date, $promotions)->twice
    ;
}

Le test montre bien que le calcul d'un tarif dépend des personnes ayant besoin d'un ticket, de la date de la commande ainsi que des éventuelles promotions activées sur la commande.
Désolé, je n'ai pas le courage de faire les tests de la classe tarificateur vu la complexité de sa combinatoire.
Et non, l'interface n'a rien d'obligatoire dans ce cas, je n'en vois pas l'utilité.

@mageekguy
Copy link
Author

Et quand à ta transposition, pour toi, c'est de la responsabilité d'un objet imprimante de savoir imprimer tous les types de document, ou c'est de la responsabilité du document de savoir s'imprimer sur une imprimante ?
Et pour reprendre ton histoire de machine à laver, c'est effectivement au pantalon d'utiliser l'interface mise à sa disposition pour dire à la machine à laver comment il doit être lavé, parce que dans le cas contraire, la machine à laver va devoir connaître TOUS les vêtements du monde pour savoir comment tous les laver correctement.
D'ailleurs, ce n'est pas pour rien qu'il y a une étiquette sur tous les vêtements indiquant comment les laver : c'est pour que l'interface qui permet aux vêtements de communiquer avec la machine, à savoir l'humain, puisse dire à la machine comment laver correctement ce qu'il y met (à 40° sans prélavage avec essorage à moins de 1000 tours).
C'est donc bien de la responsabilité du pantalon de dire comment il doit être lavé, puisque c'est lui qui a la connaissance de sa nature.

<?php

class vetement
{
    private $maxEssorage = 0;
    private $maxTemperature = 0;

    public function __construct($maxTemperature, $maxEssorage)
    {
        $this->maxEssorage = $maxEssorage;
        $this->maxTemperature = $maxTemperature;
    }

    public function getEssorageMaxi()
    {
        return $this->maxEssorage;
    }

    public function getTemperatureMaxi()
    {
        return $this->maxTemperature;
    }
}

class machine
{
    private $lessive = null;
    private $temperature = null;
    private $essorage = null;
    private $vetements = array();

    function setTemperature($temperature)
    {
        $this->temperature = $temperature;

        return $this;
    }

    function setEssorage($essorage)
    {
        $this->essorage = $essorage;

        return $this;
    }

    function add(vetement $vetement)
    {
        $this->vetements[] = $vetement;

        return $this;
    }

    function wash(lessive $lessive)
    {
        foreach ($this->vetements as $vetement)
        {
            $vetement->wash($lessive, $this->temperature, $this->essorage);
        }
    }
}

class human
{
    private $vetements = null;
    private $lessive = null;

    public function __construct()
    {
        $this->vetements = new collection();
    }

    public function useLessive(lessive $lessive)
    {
        $this->lessiveMachine = $lessive;

        return $this;
    }

    public function putInMachine(vetement $vetement)
    {
        $this->vetements->add($vetement);

        return $this;
    }

    public function startMachine(machine $machine)
    {
        $this->vetements
            ->apply(function($vetement) use ($machine, & $maxTemp, & $maxEssorage) {
                    $maxTemp = $maxTemp === null ? $vetement->getTemperatureMaxi() : min($maxTemp, $vetement->getTemperatureMaxi());
                    $maxEssorage = $maxEssorage === null ? $vetement->getEssorageMaxi() : min($maxEssorage, $vetement->getEssorageMaxi());

                    $machine
                        ->setTemperature($maxTemp)
                        ->setEssorage($maxEssorage)
                        ->add($vetement)
                    ;
                }
            )
        ;

        $machine->wash($this->lessive);

        return $this;
    }
}

$machine = new machine();
$mageekguy = new human();
$mageekguy
    ->useLessive(new \lessive())
    ->putInMachine(new vetement(40, 1000))
    ->putInMachine(new vetement(30, 1400))
    ->startMachine($machine)
;

@geraldcroes
Copy link

Désolé, je n'ai pas le courage de faire les tests de la classe tarificateur vu la complexité de sa combinatoire.

C'est la même combinatoire si ce n'est que tu passes en paramètre order au tarificateur.

Et non, l'interface n'a rien d'obligatoire dans ce cas, je n'en vois pas l'utilité.

Donc tu admets que la signature change, pas dans l'exemple 1 (à son bénéfice) qui se manipule toujours de la même façon, qui donne toujours le bon tarif dans toutes les situations, qui est interchangeable à souhait car il peut faire l'objet d'une interface, donc de changement d'implémentations à souhait.

Ensuite, que l'on écrive ou non une interface, on devrait "toujours" penser en interface, les interfaces définissant les responsabilités des objets.

@geraldcroes
Copy link

Et quand à ta transposition, pour toi, c'est de la responsabilité d'un objet imprimante de savoir imprimer 
tous les types de document, ou c'est de la responsabilité du document de savoir s'imprimer sur 
une imprimante ?

L'imprimante regarde le texte du document (getTexte(), comme getAge()) et l'imprime en choisissant l'encre en fonction de (getTextColor()). Si l'imprimante est noir et blanc, elle regarde pas la couleur...

Et pour reprendre ton histoire de machine à laver, c'est effectivement au pantalon d'utiliser 
l'interface mise à sa disposition pour dire à la machine à laver comment il doit être lavé, 

Ben t'as de la chance, chez moi, le pantalon appuye pas sur les boutons de la machine à laver.

parce que dans le cas contraire, la machine à laver va devoir connaître TOUS les vêtements 
du monde pour savoir comment tous les laver correctement.

Non, elle sait laver en laine (if $this->enLaine() then 30 degrés) ou en coton if $this->enCoton() then 60 degrés

D'ailleurs, ce n'est pas pour rien qu'il y a une étiquette sur tous les vêtements 
indiquant comment les laver

En effet, le pantalon expose publiquement ses caractéristiques (getAge(), getMatière(), ...) principales permettant à l'opérateur de la machine à laver de régler correctement la machine.

Toutefois, l'intelligence de décision finale est bien coté "Laveur" => ok, ils disent 30 degrés, mais je sais qu'a 40 c'est très bien. Ou alors => Ils disent 40 degrés, mais je vais mettre le mode éco, c'est pas trop sale.

@geraldcroes
Copy link

D'ailleurs, ce n'est pas pour rien qu'il y a une étiquette sur tous les vêtements 
indiquant comment les laver

Pour reprendre un de tes exemples :

I happen to know where the best laundry place in San Francisco is. 
And I speak English, and I have dollars in my pockets. So I go out and hail a taxicab 
and tell the driver to take me to this place in San Francisco. I go get your clothes laundered, 
I jump back in the cab, I get back here. I give you your clean clothes and say, 
Here are your clean clothes

Une implémentation CopainSympa de l'interface MachineALaver" sait tout ça... pas le pantalon. Le pantalon ne sait pas ou est la laverie, ne sait pas qu'il sera transporter dans le coffre de la voiture ni même ouvrir le coffre ou aller sur la plage arrière, il n'a pas d'argent pour payer et ne sait pas parler anglais.... pourtant, c'est comme ça qu'il sera, au final, lavé.

@mageekguy
Copy link
Author

Cf mon exemple de code…
Et si ton document est une image (donc getTexte() retourne… tiens d'ailleurs, elle retourne quoi dans ce cas cette fonction ?) ?
Et si ton document est une image vectorielle ?
Et si ton document est du texte, mais Postcript ? Tu imprimes le Postscript ?
Et si ton document est du texte, mais HTML ? Tu imprimes le HTML ?
Et si…
Ton document est le seul, en fonction de sa nature, à avoir comment s'imprimer sur imprimante en fonction des outils (son interface) qu'elle lui met à sa disposition.
Le contraire implique que l'imprimante doit savoir imprimer tous les types de documents existants.
Autre question, si un jour une nouvelle matière textile voit le jour qui ne soit pas de la laine, du coton ou du synthétique et qui demande des paramètres spécifiques, tu fait comment pour faire évoluer ta machine à laver en fonction ? tu patches le firmware de toutes les machines à laver du monde pour qu'il gère le $this->isEnNouvelleMatiere() (hum…) ou tu dis à tout le monde de changer de machine pour que le firmware soit à jour et gère $this->isEnNouvelleMatiere() afin de la laver correctement (re-hum…) ?
Et j'attends le test du tarificateur dans le cas correspondant à la dernière version de mon code.

@mageekguy
Copy link
Author

Et si vraiment la signature de la méthode personn::getPrice() te dérange, une autre solution est de faire :

``` php

class personn
{
    /* ... */
    private $group = null;

    public function setGroup(personns $personns)
    {
        $this->group = $personns;

        return $this;
    }

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

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

        $age = $this->getAge();

        if (sizeof($this->group) < 10)
        {
            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;
            }
        }
        else
        {
            switch (true)
            {
                case $age < 2:
                    $ticketPrice = 0;
                    break;

                case $age < 4:
                    $ticketPrice = 1;
                    break;

                case $age < 16:
                    $ticketPrice = 5;
                    break;

                default:
                    $ticketPrice = 15;
            }
        }

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

class order
{
    /* ... */

    public function addPerson(personn $personn)
    {
        $this->personns->add($personn->setGroup($this->personns));

        return $this;
    }

    /* ... */
}

Ainsi, pas de modification de signature et si interface il y a, il n'y aura pas de problème.
Après, il reste évidemment la possibilité que le développeur ne fasse pas appel à personn::setGroup() dans la classe order tout comme il est possible qu'il omette de passer l'argument $groupSize avec ma première solution, et tout comme il peut oublier de faire appel à sizeof($order->getPersonns()) dans tarificateur.
À un moment, l'Humain et ses faiblesses entre en jeu et le code ne pourra jamais rien y changer (à moins que l'Humain ne soit plus l'auteur du code…).

@geraldcroes
Copy link

Et si ton document est une image (donc getTexte() retourne… tiens d'ailleurs, elle retourne quoi dans ce cas cette fonction ?) ?

Si mon implémentation d'imprimante ne sait pas gérer d'image, soit, elle n'imprime rien. Je prends une autre implémentation, ou je fais évoluer mon imprimante pour qu'elle prenne en charge mon image.

Exactement comme dans la vraie vie : J'ai une imprimante noir et blanc, j'ai un doc couleur je le veux en couleur ? il me faut une imprimante couleur.

Et si ton document est une image vectorielle ?
Et si ton document est du texte, mais Postcript ? Tu imprimes le Postscript ?
Et si ton document est du texte, mais HTML ? Tu imprimes le HTML ?
Et si…

Si "tout ça", au pire, je peux créer un adaptateur que je donnerais à mon imprimante

Le truc, c'est que avec exemple 1, je peux avoir "n" implémentations de cette chose la.
Si je délègue cela à personne, je vais avoir personne capable de gérer "tous les matériels du monde", cela revient au même.
Et si j'ai une imprimante 3D, je fais comment ?

Ton document est le seul, en fonction de sa nature, à avoir comment s'imprimer 
sur imprimante en fonction des outils (son interface) qu'elle lui met à sa disposition.

Et mon pantalon sait prendre le taxi pour aller à la laverie, tout à fait... (non ?)
NON, mon document sait exposer ses caractéristiques pour les mettre à disposition de quelqu'un qui sait l'utiliser.

Le piano propose des touches, le pianiste "sait jouer du piano". Pas l'inverse. Le piano ne sait pas jouer du pianiste.

C'est quand même dingue de soutenir l'inverse.

Une feuille de papier sait dessiner ? ou le dessinateur sait dessiner sur la feuille de papier ? D'ailleurs dessinateur, c'est un peu le contrat "imprimante" des fois non ?

L'anglais sait être lu ? ou le lecteur à la capacité de lire l'anglais (ou des fois pas) ?

C'est pas la faute du langage chinois si "Robert Durant" ne sait pas le lire, c'est Robert qui n'en a pas la capacité, bien qu'il soit un "lecteur" potentiel.

Le contraire implique que l'imprimante doit savoir imprimer tous les types de documents existants. 

Non, le contraire implique qu'il existe un composant capable de certaines tâches, avec un ensemble limité de possibilités. S'il faut changer, ça tombe bien, on a des interfaces qui décrivent un capacité, on change d'implémentation (ou on adapte).

LecteurDeVideo => un magnétoscope prends des cassettes, pas des DVD. La cassette expose sa bande magnétique, elle ne sait pas appuyer sur "lecture" de toutes les marques de lecteur de cassette.

Autre question, si un jour une nouvelle matière textile voit le jour qui ne soit pas de la laine, 
du coton ou du synthétique et qui demande des paramètres spécifiques, tu fait comment 
pour faire évoluer ta machine à laver en fonction ?

Je dis aux gens d'acheter une machine capable de laver la nouvelle matière. C'est d'ailleurs pour cela qu'il existe des laves "linges" et des laves "vaisselles". Tous les deux lavent.... pas la même chose.

Un nouveau mode de communication arrive => Internet. Il faut acheter un modem, ben oui, c'est comme ça.

Et j'attends le test du tarificateur dans le cas correspondant à la dernière version de mon code.

Je pensais avoir expliqué correctement, je vais te faire l'implémentation si tu veux.

@geraldcroes
Copy link

{ person::setGroup, ... } Ainsi, pas de modification de signature et si interface il y a, il n'y aura pas de problème.

La, tu changes le contrat... ça revient au même. Il ne suffit plus d'appeler getTarif, il faut faire des choses avant. Donc tu exposes, pour une même tâche, des étapes supplémentaires. Tu expose une complexité inutile et on rentre dans du procédural (le client manipule directement les opérations).

@mageekguy
Copy link
Author

Je dis aux gens d'acheter une machine capable de laver la nouvelle matière.
C'est d'ailleurs pour cela qu'il existe des laves "linges" et des laves "vaisselles". Tous les deux lavent.... pas la même chose.

oO.
Des vêtements n'ayant pas les même caractéristiques qu'un élément de vaisselle (on ne s'habille pas avec une assiette…), le fait fait d'avoir deux équipement différents pour les laver est parfaitement normal.
Par contre, devoir changer de machine à laver parce qu'il existe des vêtements dans une nouvelle matière alors que les vêtements concernés savent exposer les contraintes qu'ils imposent au lavage via leur étiquette… WHOAAAAA, je ne sais même pas comment exprimer ce que je ressens en lisant ça, à part que tu devrais bosser dans une entreprise qui a fait de l'obsolescence programmée sa principale source de revenus.
Allez, tout de même, une dernière pour la route :

<?php

class language
{
    private $name = '';

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

    public function __toString()
    {
        return (string) $this->name;
    }

    public function readBook(book $book)
    {
        echo $book;
    }
}

class book
{
    private $contents = '';
    private $language = null;

    public function __construct($contents, language $language)
    {
        $this->contents = $contents;
        $this->language = $language;
    }

    public function __toString()
    {
        return (string) $this->contents;
    }

    public function setLanguage(human $human)
    {
        $human->useLanguage($this->language);

        return $this;
    }
}

class human
{
    private $firstname = '';
    private $lastname = '';
    private $languages = array();
    private $currentLanguage = null;

    public function __construct($firstname, $lastname, language $nativeLanguage)
    {
        $this->firstname = $firstname;
        $this->lastname = $lastname;
        $this->languages[] = $this->currentLanguage = $nativeLanguage;
    }

    public function __toString()
    {
        return $this->firstname . ' ' . $this->lastname;
    }

    public function knowLanguage(language $language)
    {
        $this->languages[] = $language;

        return $this;
    }

    public function useLanguage(language $language)
    {
        if (in_array($language, $this->languages) === false)
        {
            throw new \exception($this . ' does not know ' . $language);
        }

        $this->currentLanguage = $language;

        return $this;
    }

    public function read(book $book)
    {
        $this->currentLanguage->readBook($book->setLanguage($this));

        return $this;
    }
}

$book = new book('La programmation orientée objet pour les nuls', new language('chinois'));
$robertDurant = new human('Robert', 'Durant', new language('français'));

try
{
    $robertDurant->read($book);
}
catch (\exception $exception)
{
    echo $exception->getMessage() . PHP_EOL;
}

$robertDurant->knowLanguage(new language('chinois'))->read($book);

@geraldcroes
Copy link

Des vêtements n'ayant pas les même caractéristiques qu'un élément de vaisselle 
(on ne s'habille pas avec une assiette…), le fait fait d'avoir deux équipement différents 
pour les laver est parfaitement normal.

Alors expliques moi en quoi un l'exemple DVD / Cassette est différent. Pourquoi devrais-je dire à DVD comment se servir du lecteur de DVD. Le DVD va juste exposer ses données. Si je met mon DVD dans un lecteur de CD, ça marche pas...

Par contre, devoir changer de machine à laver parce qu'il existe des vêtements dans 
une nouvelle matière alors que les vêtements concernés savent exposer les contraintes 
qu'ils imposent au lavage… WHOAAAAA, je ne sais même pas comment exprimer ce 
que je ressens en lisant ça

Que c'est le principe de l'interface ? de pouvoir changer d'implémentation sans impact sur le contrat final attendu ?

interface SystemDeLavage {
    public function lave(UnTruc $unTruc)
}

class LaveLinge implements SystemDeLavage {
}
class UnePersonne implements SystemDeLavage {
}
class LaveVaisselle implements SystemDeLavage {
}

class Humain implements ExpertLavage {
     public function donneMoiSystemeDeLavage(UnTruc $unTruc);
}

$systemeDeLavage = $moi->donneMoiSystemeDeLavage($unTruc);
$systemeDeLavage->lave($unTruc);

Et mon pantalon de tout à l'heure, tu ne m'as pas dit, il sait parler anglais et prends le taxi ?

Pour ton RobertDurant, tu montre "par l'exemple" que ce n'est pas le Chinois (le ticket de disneyland) qui indique au "Lecteur" (celui qui agit sur l'objet) comment être compris... mais bien le lecteur qui sait interpréter les caractéristiques (getIdeogrammes() ou getAge()) pour avoir un résultat (le sens du texte / le prix).

Qui plus est, le lecteur (Robert) DOIT CHANGER (ou évoluer) s'il ne sait pas lire le chinois, comme le Système de lavage.

@mageekguy
Copy link
Author

Je me demande ou tu vois des getters dans mon exemple sur le livre (à part celui de l'exception, évidemment)…
Aucun objet n'expose quoique ce soit de sa structure à l'extérieur, directement ou indirectement (pas de getter et les propriétés sont privées).
Le lecteur n'interprète rien, il ne demande pas sa langue au livre pour ensuite se configurer en conséquence.
Il laisse au livre le soins de lui indiquer la langue qu'il doit utiliser pour qu'il puisse être lu, et c'est la langue défini par le livre pour le lecteur qui lit effectivement le livre, puisque c'est elle qui dispose de la grammaire et du vocabulaire nécessaire pour l'interpréter.
Avec une collection dans la propriété human::$languages à la place d'un tableau, le code pourrait même ne contenir absolument aucun if alors que le fait que le lecteur ne connaît pas toutes les langues est parfaitement géré.

public function useLanguage(language $language)
{
    $this->languages
        ->select(function($knownLanguage) use ($language) { return $language == $knownLanguage; }, 1, function() use ($language) { throw new \exception($this . ' does not know ' . $language); })
        ->apply(function($language) { $this->currentLanguage = $language; })
    ;

  return $this;
}

Et toute la différence entre nos "approches" est là !
Pour le reste, désolé, mais je n'ai pas le temps de me fendre d'un exemple supplémentaire pour te démontrer encore une fois ton erreur (mais promis, si je trouve la motivation pour prendre le temps de le faire, je n'y manquerais pas).

@geraldcroes
Copy link

Le lecteur n'interprète rien, il ne demande pas sa langue au livre pour ensuite se configurer en conséquence.
Il laisse au livre le soins de lui indiquer la langue qu'il doit utiliser pour qu'il puisse être lu, et c'est la langue 
défini par le livre pour le lecteur qui lit le livre, puisque c'est elle qui dispose de la grammaire et du 
vocabulaire nécessaire pour l'interpréter.

Ah oui, pardon, j'avais mal lu...
c'est la langue Chinoise qui lit le livre... bien... et elles les écrits aussi donc, enfin j'imagine, sur n'importe quel support, de même qu'elle le parle. C'est dans ta logique de départ, en effet.

Bon... donc, encore une fois, confirmes moi que pantalon sait parler anglais et prendre le taxi car cela fait parti du processus de lavage ?

@geraldcroes
Copy link

C'est donc toi qui a écrit cela :

public function read(book $book)
{
        $this->currentLanguage->readBook($book);
        return $this;
}

En gros : la personne passe par un système de lecture et dit que ce n'est pas de sa responsabilité, ni de sa compétence. C'est un "accesseur".

On paraphrase :

public function tarification(order $order)
{
        $this->systemeDeTarification->tarifie($order);
        return $this;
}

TADAAAAAAA voilà notre système de tarification de exemple 1

La personne ne sait pas tarifer, elle demande à un système dédié.... donc on peut raisonnablement se demander ce que ça fou la, mais après tout, pourquoi pas, elle peut consulter le catalogue de disney.

Merci de t'être enfin rangé à mon avis.
Ce fut long, mais l'essentiel est là.

@mageekguy
Copy link
Author

c'est la langue Chinoise qui lit le livre...

Non, la langue chinoise permet d'interpréter le contenu d'un livre écrit dans cette langue.
Le lecteur lit donc le livre en utilisant sa connaissance de la langue chinoise, ou ne parvient pas à le lire car il ne dispose pas de l'interpréte (ie. la langue) permettant de le faire.
La langue est l'interface entre le lecteur et le livre.
Et je pense que ce qui biaise ton raisonnement est que tu vois systématiquement une relation directe entre les entités concernées (pour toi la machine doit savoir laver un vêtement sans aide et le lecteur doit savoir lire un livre sans en avoir appris la langue) alors qu'elle est en réalité indirecte (il y la langue entre le livre et le lecteur, l'humain entre le vêtement et la machine), conformément au D de SOLID, d'ailleurs…

@mageekguy
Copy link
Author

Et non, nous ne sommes pas du tout du même avis, puisque ton tarificateur utilise des getter, des if et des else pour savoir quoi faire : en clair, il interroge l'état de un ou plusieurs objets pour déterminer ce qu'il doit faire, donc il brise l'encapsulation et ce n'est donc pas de la POO.

That is, you should endeavor to tell objects what you want them to do; do not ask them questions about their state, make a decision, and then tell them what to do.
The problem is that, as the caller, you should not be making decisions based on the state of the called object that result in you then changing the state of the object.
The logic you are implementing is probably the called object’s responsibility, not yours. For you to make decisions outside the object violates its encapsulation.

Et ce n'est pas moi qui le dit, mais peut être que Sharp et Appleton sont eux aussi à côté de la plaque tout comme les rédacteurs de Pragmatic Bookshelf, sait-on jamais…

@geraldcroes
Copy link

tu as écrit ça oui ou non ?

public function read(book $book)
{
        $this->currentLanguage->readBook($book);
        return $this;
}

Si oui, on est d'accord, car c'est exactement transposable comme ça :

public function tarification(order $order)
{
        $this->systemeDeTarification->tarifie($order);
        return $this;
}

Implémente système de tarification comme tu veux par la suite, sans if si cela te plait.

@geraldcroes
Copy link

That is, you should endeavor to tell objects what you want them to do; do not ask them 
questions about their state, make a decision, and then tell them what to do.

Traduisons

Ce qui signifie que vous devez dire aux objet ce qu'ils doivent faire, ne leur demandez 
pas leur état pour prendre une décision et ensuite leur dire quoi faire.

La phrase n'a de sens que si elle est lue en entier.

Nulle part "tarificateur" ne prends de décision pour "Personne", ni ne modifie "Personne". Elle regarde "Personne" pour lui calculer un "Tarif".

The problem is that, as the caller, you should not be making decisions based on 
the state of the called object that result in you then changing the state of the object.

Encore une fois, traduisons tes sources :

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 de l'objet.

Nulle part tarificateur ne modifie l'état de l'objet.

The logic you are implementing is probably the called object’s responsibility, 
not yours. For you to make decisions outside the object violates its encapsulation.

Traduisons :

 La logique que vous implémentez est probablement celle de l'objet appelé, pas la votre.
 Si vous faites des désisions pour l'objet "en dehors de l'objet", cela viole l'encapsulation.

Encore une fois, le tarificateur prends des décisions sur le tarif, pas sur la personne.

Je pense juste qu'il faut que tu comprennes mieux les nuances de l'anglais avant de lire des livres complexes en anglais. Tes sources ont tout à fait raison, je suis de leur avis.

@mageekguy
Copy link
Author

Je te le dis et te le redis, ton tarifcateur n'a pas lieu d'exister, c'est à order de savoir calculer son prix, et ce n'est certainement pas la responsabilité d'une entité externe.
Par contre, c'est de la responsabilité de la langue de savoir interpréter un livre écrit dans cette langue, et ce n'est pas de la responsabilité du lecteur de savoir le faire s'il n'a pas appris cette langue.
Le contexte entre les deux exemples n'est pas comparable.

@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