Voyons ensemble le Design Pattern "Factory"
Qui suis-je pour parler en ces lieux ?
Je suis le susnommé Kevin Nadin
Twitter : @kevinjhappy
Je travaille à Darkmira en tant que développeur PHP, c'est une boîte avec plein de dev cools ;)
Design Pattern, quésaco ?
Bonnes pratiques de conception de code
23 Design patterns en tout,
regroupés en 3 grandes parties :
-
Création
-
Architecture
-
Comportement
Aujourd'hui on voit...
(roulement de tambour)
de la catégorie création
(bon, en même temps, c'est le titre de la conf... )
La Factory !!
Définition
Fonction qui crée un objet en fonction de paramètres
Exemples:
- Fonction qui va créer l'objet adapté pour la base de donnée
- Fonction qui va créer un objet qui enregistre une donnée dans un format spécifique
- Fonction qui va créer un objet avec des levées d'exception en fonction des paramètres
On va se focaliser sur un exemple concret :
intégrer une Factory avec deux personnages qui se combattent
Nous avons pour commencer un seul type de personnage :
-
Guerrier
Deux personnages se combattent, donc nous avons besoin que ces personnages soient capable de :
-
donner des dégâts
-
prendre des dégâts
-
mourir (il faut bien un vainqueur :D )
<?php
class Warrior
{
private $lifePoint;
private $hitPoint;
public function __construct(int $lifePoint, int $hitPoint)
{
$this->lifePoint = $lifePoint;
$this->hitPoint = $hitPoint;
}
}
On crée une classe Warrior
Elle possède 2 attributs :
-
points de vie
-
points de dégât
<?php
class Warrior
{
// ...
public function isAlive(): bool
{
return 0 <= $this->lifePoint;
}
public function attack(): int
{
if ($this->isAlive()) {
return $this->hitPoint;
}
return 0;
}
public function takeDamage(int $damage): void
{
$this->lifePoint -= $damage;
}
}
A la classe Warrior, on va ajouter 3 fonctions pour combattre
Et maintenant on procède au combat dans le script Battle
Je reçois des données
<?php
// je recois les données en tableau, mais je ne sais pas dans quel ordre
$entryData = [
[
'type' => 'Warrior',
'lifePoint' => 20,
'hitPoint' => 5,
],
[
'type' => 'Warrior',
'lifePoint' => 15,
'hitPoint' => 8,
]
];
Je crée les deux guerriers
// je crée le premier personnage
$data = $entryData[0];
if ('Warrior' === $data['type']) {
$firstCharacter = new Warrior($data['lifePoint'], $data['hitPoint']);
}
// je crée le second personnage
$data = $entryData[1];
if ('Warrior' === $data['type']) {
$secondCharacter = new Warrior($data['lifePoint'], $data['hitPoint']);
}
Ils s'affrontent !
// Battle
while ($secondCharacter->isAlive() && $firstCharacter->isAlive()) {
$secondCharacter->takeDamage($firstCharacter->attack());
$firstCharacter->takeDamage($secondCharacter->attack());
}
if (!$secondCharacter->isAlive()) {
echo 'Victory to the first character';
} elseif (!$firstCharacter->isAlive()) {
echo 'Victory to the second character';
}
VS
Le souci...
C'est d'utiliser "new"
Dans un gros projet, si la manière de créer un objet change, alors vous allez devoir modifier tous les "new Object" du projet.
Solution : on implémente une fonction dans Warrior qui va créer Warrior
class Warrior
{
private $lifePoint;
private $hitPoint;
public function __construct(int $lifePoint, int $hitPoint)
{
//...
}
//...
public static function create(array $data): self
{
return new Warrior($data['lifePoint'] ?? 1, $data['hitPoint'] ?? 1);
}
}
Et là, nous avons une méthode de Factory :)
La création de personnages dans Battle devient :
// je crée le premier personnage
$data = $entryData[0];
if ('Warrior' === $data['type']) {
$firstCharacter = Warrior::create($data);
}
// je crée le second personnage
$data = $entryData[1];
if ('Warrior' === $data['type']) {
$secondCharacter = Warrior::create($data);
}
Maintenant, on augmente la complexité
On ajoute un nouveau type de personnage :
-
Magicien
<?php
class Wizard
{
private $lifePoint;
private $hitPoint;
private $magicPoint;
public function __construct(int $lifePoint, int $hitPoint, int $magicPoint)
{
$this->lifePoint = $lifePoint;
$this->hitPoint = $hitPoint;
$this->magicPoint = $magicPoint;
}
}
Je crée la classe Wizard
Elle possède 3 attributs :
-
points de vie
-
points de dégât
-
points de magie
<?php
class Wizard
{
// ...
public function isAlive(): bool
{
return 0 <= $this->lifePoint;
}
public function attackWithMagic(): int
{
if ($this->isAlive() && $this->magicPoint >= 2 ) {
$this->magicPoint -= 2;
return $this->hitPoint;
}
return 0;
}
public function takeDamage(int $damage): void
{
$this->lifePoint -= $damage;
}
}
J'ajoute des fonctions de combat
<?php
class Wizard
{
// ...
public static function create(array $data): self
{
// on peut aussi utiliser le mot clé self au lieu de Wizard
return new self(
$data['lifePoint'] ?? 1,
$data['hitPoint'] ?? 1,
$data['magicPoint'] ?? 1
);
}
}
Et une fonction de Factory
<?php
$entryData = [
[
'type' => 'Warrior',
'lifePoint' => 20,
'hitPoint' => 5,
],
[
'type' => 'Wizard',
'lifePoint' => 15,
'hitPoint' => 8,
'manaPoint' => 10,
]
];
Et Battle change...
// je crée le premier personnage
$data = $entryData[0];
switch ($data['type']) {
case 'Warrior':
$firstCharacter = Warrior::create($data);
break;
case 'Wizard':
$firstCharacter = Wizard::create($data);
break;
}
// je crée le second personnage
$data = $entryData[1];
switch ($data['type']) {
case 'Warrior':
$secondCharacter = Warrior::create($data);
break;
case 'Wizard':
$secondCharacter = Wizard::create($data);
break;
}
// Battle
while ($secondCharacter->isAlive() && $firstCharacter->isAlive()) {
if ($firstCharacter instanceof Wizard) {
$secondCharacter->takeDamage($firstCharacter->attackWithMagic());
} else {
$secondCharacter->takeDamage($firstCharacter->attack());
}
if ($secondCharacter instanceof Wizard) {
$firstCharacter->takeDamage($secondCharacter->attackWithMagic());
} else {
$firstCharacter->takeDamage($secondCharacter->attack());
}
}
if (!$secondCharacter->isAlive()) {
echo 'victory to the first character';
} elseif (!$firstCharacter->isAlive()) {
echo 'victory to the second character';
}
VS
Le souci...
Pour être certain du type de l'objet, on doit vérifier les types à chaque fois
Une fois, deux fois, .... et on se retrouve à le faire partout dans le projet.
De plus, si on ajoute un autre type de personnage, on doit modifier tous les switch concernés.
... c'est le switch
La solution...
Le switch va être centralisé dans une classe dont le seul but est de sélectionner le bon type de personnage
... toujours la Factory,
mais sous une autre forme :
l'Abstract Factory
Classe FightingCharactersFactory avec une seule fonction create()
<?php
class FightingCharactersFactory
{
public static function create(string $parameter, array $data)
{
switch ($parameter) {
case 'Warrior':
return new Warrior($data['lifePoint'], $data['hitPoint']);
case 'Wizard':
return new Wizard($data['lifePoint'],
$data['hitPoint'], $data['manaPoint']);
}
}
}
Problème :
on ne connaît plus le type de l'objet retourné
Nous n'avons pas l'assurance que les fonctions :
-
isAlive()
-
takeDamage()
-
attack()
existent dans chaque cas
Solution :
Utilisation d'une Interface
L'interface va nous forcer à définir les fonctions qui seront demandées dans Battle
Je crée CanBattleInterface
<?php
interface CanBattleInterface
{
public function isAlive(): bool;
public function attack(): int;
public function takeDamage(int $damage): void;
}
En utilisant cette interface, je serai forcé d'avoir ces fonctions disponibles dans ma classe, tout en respectant les paramètres et le type de retour
Je l'implémente à Warrior
class Warrior implements CanBattleInterface
{
private $lifePoint;
private $hitPoint;
public function __construct(int $lifePoint, int $hitPoint)
{
$this->lifePoint = $lifePoint;
$this->hitPoint = $hitPoint;
}
public function isAlive(): bool
{
//...
}
public function attack(): int
{
//...
}
public function takeDamage(int $damage): void
{
//...
}
}
Je l'implémente à Wizard
class Wizard implements CanBattleInterface
{
private $lifePoint;
private $hitPoint;
private $magicPoint;
public function __construct(int $lifePoint, int $hitPoint, int $magicPoint)
{
$this->lifePoint = $lifePoint;
$this->hitPoint = $hitPoint;
$this->magicPoint = $magicPoint;
}
public function isAlive(): bool
{
//...
}
// attackWithMagic() devient simplement attack()
public function attack(): int
{
//...
}
public function takeDamage(int $damage): void
{
//...
}
}
Je peux mettre à jour ma Abstract Factory
<?php
class FightingCharactersFactory
{
public static function create(string $parameter, array $data): CanBattleInterface
{
switch ($parameter) {
case 'Warrior':
return new Warrior($data['lifePoint'], $data['hitPoint']);
case 'Wizard':
return new Wizard($data['lifePoint'],
$data['hitPoint'], $data['manaPoint']);
}
}
}
Ici, indiquer CanBattleInterface en type de retour est très important !
Abstract signifie justement que l'on renvoi l'abstraction d'un objet
Je passe la classe en final car il n'y a aucune raison d'autoriser l'héritage.
Et par sécurité, je lève une exception si j'atteins le default.
<?php
final class FightingCharactersFactory
{
public static function create(string $parameter, array $data): CanBattleInterface
{
switch ($parameter) {
case 'Warrior':
return new Warrior($data['lifePoint'], $data['hitPoint']);
case 'Wizard':
return new Wizard($data['lifePoint'],
$data['hitPoint'], $data['manaPoint']);
default:
throw new \Exception();
}
}
}
Mieux : je crée une exception dédiée
<?php
class UnknownCharacterTypeException extends InvalidArgumentException
{
private const MESSAGE = 'Invalid character type : %s';
public function __construct(string $parameter, $code = 0,
Throwable $previous = null)
{
parent::__construct(sprintf(self::MESSAGE, $parameter), $code, $previous);
}
}
Je l'implémente au default
<?php
final class FightingCharactersFactory
{
public static function create(string $parameter, array $data): CanBattleInterface
{
switch ($parameter) {
case 'Warrior':
return new Warrior($data['lifePoint'], $data['hitPoint']);
case 'Wizard':
return new Wizard($data['lifePoint'],
$data['hitPoint'], $data['manaPoint']);
default:
throw new UnknownCharacterTypeException($parameters);
}
}
}
Et le script Battle change :
<?php
// je recois les données en tableau, mais je ne sais pas dans quel ordre
$entryData = [
[
'type' => 'Warrior',
'lifePoint' => 20,
'hitPoint' => 5,
],
[
'type' => 'Wizard',
'lifePoint' => 15,
'hitPoint' => 8,
'manaPoint' => 10,
]
];
$characters = [];
foreach ($entryData as $data) {
$characters[] = FightingCharactersFactory::create($data['type'], $data);
}
// Battle
while ($characters[0]->isAlive() && $characters[1]->isAlive()) {
$characters[1]->takeDamage($characters[0]->attack());
$characters[0]->takeDamage($characters[1]->attack());
}
if (!$characters[1]->isAlive()) {
echo 'victory to the first character';
} elseif (!$characters[0]->isAlive()) {
echo 'victory to the second character';
}
Et dans mon script Battle, je sais que les fonctions de combat sont disponibles car Warrior et Wizard implémentent CanBattleInterface
<?php
$characters = [];
foreach ($entryData as $data) {
$characters[] = FightingCharactersFactory::create($data['type'], $data);
}
// tous les éléments de $characters sont des objet de type CanBattleInterface
/** @var CanBattleInterface[] $characters */
while ($characters[0]->isAlive() && $characters[1]->isAlive()) {
$characters[1]->takeDamage($characters[0]->attack());
$characters[0]->takeDamage($characters[1]->attack());
}
Et si on rajoute un type de personnage ?
Par exemple, Archer
Rien de plus simple, on crée la classe Archer qui implémente CanBattleInterface
class Archer implements CanBattleInterface
{
private $lifePoint;
private $hitPoint;
private $arrowNumber;
public function __construct(int $lifePoint, int $hitPoint, int $arrowNumber)
{
$this->lifePoint = $lifePoint;
$this->hitPoint = $hitPoint;
$this->arrowNumber = $arrowNumber;
}
public function isAlive(): bool
{
//...
}
public function attack(): int
{
//...
}
public function takeDamage(int $damage): void
{
//...
}
}
On l'ajoute dans la Factory
<?php
final class FightingCharactersFactory
{
public static function create(string $parameter, array $data): CanBattleInterface
{
switch ($parameter) {
case 'Warrior':
return new Warrior($data['lifePoint'], $data['hitPoint']);
case 'Wizard':
return new Wizard($data['lifePoint'],
$data['hitPoint'], $data['manaPoint']);
case 'Archer':
return new Archer($data['lifePoint'],
$data['hitPoint'], $data['arrowNumber']);
default:
throw new UnknownCharacterTypeException($parameter);
}
}
}
Et le tour est joué !
Le script Battle n'a besoin d'aucune modification !
Et croyez moi, c'est très utile :)
Il ne vous reste plus qu'à vous laisser tenter !
Des questions ?
Voyons ensemble le Design Pattern "Factory"
By Kevin JHappy
Voyons ensemble le Design Pattern "Factory"
AFUP Day Lille 2020
- 1,400