Description
Le design pattern factory a pour but de résoudre les problèmes d'instanciations : on créé en général l'objet dans la classe qui va l'utiliser et cela créé de l'inflexibilité. Cette inflexibilité est due au fait que l'objet est lié à la classe qui l'instancie, ce qui rend les modifications impossibles. Pour contrer ce problème, le pattern factory va donc déléguer l'instanciation de l'objet dans une sous-classe remplaçant le constructeur de la classe de l'objet. Nous gagnons alors en flexibilité !
Mise en situation
🍕 Nous allons développer une pizzeria, et plus précisément une fonctionnalité de cette pizzeria qui simule l'élaboration d'une pizza. Cela nous permettra de mettre en avant ce design pattern. L'idée sera que notre programme simule la commande de pizza, et que notre factory nous retourne les pizzas. Dans un cas présent, l'on pourrait se dire que cela n'est pas très compliqué : on crée deux pizzas et c'est gagné. Mais lorsque vous allez dans votre pizzeria vous avez plusieurs pizzas, on pourrait utiliser des structures conditionnelles mais cela deviendrait :
- Illisible.
- Maintenabilité difficile.
- Flexibilité très nul.
- Et surtout créerait des frais de maintenances énormes. Voilà pourquoi notre factory nous permettra, par la factory, d'instancier les différents objets pizza, qui seront utilisés à travers une classe abstraite.
Avantages et inconvénients
Avantages | Inconvénients |
---|---|
✔️Isolation des classes concrètes, la factory encapsule la responsabilité et le processus de création des objets, alors que le client manipule les objets par une classe abstraite. Ainsi chaque classe est isolée et ne dépend pas d'autre classe. | ❌Augmentation du nombre d'objet , on ne sait pas taille qu'une famille d'objet peut prendre et cela peut mener à énormément de classes. |
✔️Facilite le changement d'objet, la classe concrète n'apparait que là où elle est instanciée. Ainsi il suffit de changer le type de classe concrète sur la classe abstraite pour obtenir un objet de la même famille. | ❌Peut devenir compliqué dans le cas où l'ajout de fonctionnalité dans la classe abstraite se complexifie par l'ajout de méthode et ne caractérise plus certain enfant. Cet inconvénient apparait en cas d'une mauvaise implémentation ou architecture. |
✔️Facilité à tester, étant donné que chaque classe est séparée, tester les classes est plus facile. |
Quand l'utiliser ?
💡 Vous avez des objets dont leurs natures ne peuvent pas être connues à l'avance. 💡 Cas concrets : Framework, bibliothèque de classes, mécanisme d'authentification,... 💡 Vous savez que de nouvelles classes vont être ajoutées plus tard et seront de la même famille.
Diagramme UML
Dans le cas de cet exemple, le développement de ce système de commande/préparation de pizza se décompose en deux catégories :
- Les pizzas végétariennes.
- Les pizzas avec de la viande.
Face à cette situation, nous remarquons qu'un pattern factory peut être ajouté.
Mais pourquoi ? Vous le savez, les restaurateurs ne cessent de changer leur carte. Dans le cas où ils viendraient à rajouter une nouvelle pizza végétarienne :
- Il suffira d'ajouter une nouvelle classe pour cette pizza et qui sera reliée dans la factory végétarienne.
Dans le cas où ils décideraient de rajouter une catégorie comme des pizzas luxueuses :
- Il suffira de créer une nouvelle factory et créer les classes que la factory retournera.
Bref, vous l'aurez compris, nous ne savons pas comment les nouvelles classes peuvent être ajoutées, et nous avons besoin de flexibilité. Voilà pour ce design pattern est une excellente solution.
Implémentation
Commençons par créer la base de notre design package : l'interface Pizza
. Cette interface est un peu le centre de notre application puisqu'elle va être utilisée dans toutes les classes que nous allons créer. Que ce soit pour l'implémentation, le type d'objet de retour, ...
Pizza
package com.design.factory.model;
public abstract class Pizza {
protected String name;
protected String description;
public void prepare() {
System.out.println("Start preparing: " + name);
System.out.println("Preparing pizza dough");
}
public void cook() {
System.out.println("Cooking...");
}
public void box() {
System.out.println("Boxing...");
}
public String getName() {
return name;
}
}
Ici, le choix d'une classe abstraite se fait pour plusieurs raisons :
- L'héritage : nous voulons override la classe
Prepare
qui permettra d'ajouter notre logique pour chaque pizza. - Nous ne voulons pas instancier directement la classe
Pizza
, chose que l'on ne fait pas avec une classe abstraite, mais une sous-classe.
Dans cette classe nous avons diverses méthodes :
- 🔪
Prepare
: ici on prépare les différents éléments que l'on mettra sur notre pizza. - 🍳
Cook
: ici la pizza est mise dans le four à pizza. - 📦
Box
: ici la pizza est mise dans son carton à pizza, prête à partir.
Ok maintenant nous devons implémenter notre factory, pour cela nous allons commencer par créer notre classe abstraire qui va définir la méthodologie de nos factorys. Cette méthodologie est le déroulement de la procédure utilisée pour préparer notre pizza.
PizzaStore
package com.design.factory.store;
import com.design.factory.model.Pizza;
public abstract class PizzaStore {
public Pizza orderPizza(String type) {
Pizza pizza;
pizza = createPizza(type);
pizza.prepare();
pizza.cook();
pizza.box();
return pizza;
}
abstract public Pizza createPizza(String type);
}
Notre classe de base est définie, nous avons deux méthodes :
- 🤵
OrderPizza
: une personne X commande une pizza qui a un type : le nom de la pizza. La commande est réalisée, et la création de la pizza se fait jusqu'à la livraison de celle-ci. - 👨🍳
CreatePizza
: le cuisinier crée la pizza, ici nous allons instancier nos pizzas par la factory qui la désigne.
Nous avons nos deux bases, mais avant d'implémenter nos factorys je vous propose de créer les classes qui désigneront les pizzas. Dans le cas des 4 classes que nous allons implémenter, nous devrons initialiser le nom et la description de nos pizzas. Dans notre cas, nous allons le faire dans le constructeur afin de simplifier le process. Ensuite, nous allons surcharger la méthode Prepare
, qui s'occupera de mettre les différents ingrédients sur la pizza.
CheesePizza
package com.design.factory.model;
public class CheesePizza extends Pizza{
public CheesePizza() {
name = "Cheese";
description = "Pizza with four different cheese";
}
@Override
public void prepare() {
super.prepare();
System.out.println("Adding tomato sauce");
System.out.println("Adding mozzarella cheese");
System.out.println("Adding parmesan cheese");
System.out.println("Adding gorgonzola cheese");
System.out.println("Adding pecorino cheese");
}
}
FourSeasonPizza
package com.design.factory.model;
public class FourSeasonPizza extends Pizza{
public FourSeasonPizza() {
name = "Four season";
description = "Pizza with ham, mushroom, artichoke and olive";
}
@Override
public void prepare() {
super.prepare();
System.out.println("Adding tomato sauce");
System.out.println("Adding ham");
System.out.println("Adding mushroom");
System.out.println("Adding artichoke");
System.out.println("Adding olive");
}
}
MargharitaPizza
package com.design.factory.model;
public class MargharitaPizza extends Pizza{
public MargharitaPizza() {
name = "Margharita";
description = "Pizza with tomato sauce and mozzarella cheese";
}
@Override
public void prepare() {
super.prepare();
System.out.println("Adding tomato sauce");
System.out.println("Adding mozzarella cheese");
}
}
ProscuitoPizza
package com.design.factory.model;
public class ProscuitoPizza extends Pizza{
public ProscuitoPizza() {
name = "Proscuito";
description = "Pizza with ham and mozzarella";
}
@Override
public void prepare() {
super.prepare();
System.out.println("Adding tomato sauce");
System.out.println("Adding ham");
System.out.println("Adding mozzarella cheese");
}
}
Nos classes sont réalisées et vous remarquez de la redondance concernant l'affichage de "Adding tomato sauce". Ici, c'est un choix personnel, j'ai déjà vu des pizzas avec une base sans sauce tomate, voilà la pourquoi je ne l'ai pas mis dans la classe abstraite parent. Bien sûr il existe des moyens pour éviter cela, mais ici cela n'a pas un énorme impact.
Bien, nous avons nos 4 types de pizza, maintenant il nous faut implémenter les factorys qui instancieront nos objets. Nous allons créer les constantes qui représenteront le type de pizza :
PizzaType
package com.design.factory.type;
public abstract class PizzaType {
public static final String MARGHARITA="Margharita";
public static final String CHEESE="Cheese";
public static final String FOURSEASON="Four season";
public static final String PROSCUITO="Proscuito";
}
Maintenant que nos factorys sont implémentés, nous allons tester nos 4 pizzas dans notre méthode main : e servira à retourner la pizza ciblée qui sera filtrée par le type demander. Par mesure de précaution nous allons utiliser un switch, cela évitera d'avoir une série de "if/else" qui deviendrait vite illisible. Voilà pourquoi nous avons créés notre classe PizzaType
, qui nous servira à identifier le type de pizza.
MeatPizzaFactory
package com.design.factory.factory;
import com.design.factory.model.FourSeasonPizza;
import com.design.factory.model.Pizza;
import com.design.factory.model.ProscuitoPizza;
import com.design.factory.store.PizzaStore;
import com.design.factory.type.PizzaType;
public class MeatPizzaFactory extends PizzaStore {
@Override
public Pizza createPizza(String type) {
switch (type) {
case PizzaType.FOURSEASON: return new FourSeasonPizza();
case PizzaType.PROSCUITO: return new ProscuitoPizza();
default: return null;
}
}
}
VegetarianPizzaFactory
package com.design.factory.factory;
import com.design.factory.model.CheesePizza;
import com.design.factory.model.Pizza;
import com.design.factory.model.MargharitaPizza;
import com.design.factory.store.PizzaStore;
import com.design.factory.type.PizzaType;
public class VegetarianPizzaFactory extends PizzaStore {
@Override
public Pizza createPizza(String type) {
switch (type) {
case PizzaType.CHEESE: return new CheesePizza();
case PizzaType.MARGHARITA: return new MargharitaPizza();
default: return null;
}
}
}
Maintenant que nos factorys sont implémentées, nous allons tester nos 4 pizzas dans notre méthode main :
Main
package com.design.factory;
import com.design.factory.factory.MeatPizzaFactory;
import com.design.factory.factory.VegetarianPizzaFactory;
import com.design.factory.model.Pizza;
import com.design.factory.store.PizzaStore;
import com.design.factory.type.PizzaType;
public class Main {
public static void main (String[] args) {
PizzaStore meatPizzaFactory = new MeatPizzaFactory();
PizzaStore veggiePizzaFactory = new VegetarianPizzaFactory();
Pizza pizza = meatPizzaFactory.orderPizza(PizzaType.PROSCUITO);
System.out.println("Pierre ordered " + pizza.getName() + " \n ");
pizza = meatPizzaFactory.orderPizza(PizzaType.FOURSEASON);
System.out.println("Coraline ordered " + pizza.getName() + " \n ");
pizza = veggiePizzaFactory.orderPizza(PizzaType.MARGHARITA);
System.out.println("Jordan ordered " + pizza.getName() + " \n ");
pizza = veggiePizzaFactory.orderPizza(PizzaType.CHEESE);
System.out.println("Marine ordered " + pizza.getName() + " \n ");
}
}
Résultat :
Start preparing: Proscuito
Preparing pizza dough
Adding tomato sauce
Adding ham
Adding mozzarella cheese
Cooking...
Boxing...
Pierre ordered Proscuito
Start preparing: Four season
Preparing pizza dough
Adding tomato sauce
Adding ham
Adding mushroom
Adding artichoke
Adding olive
Cooking...
Boxing...
Coraline ordered Four season
Start preparing: Margharita
Preparing pizza dough
Adding tomato sauce
Adding mozzarella cheese
Cooking...
Boxing...
Jordan ordered Margharita
Start preparing: Cheese
Preparing pizza dough
Adding tomato sauce
Adding mozzarella cheese
Adding parmesan cheese
Adding gorgonzola cheese
Adding pecorino cheese
Cooking...
Boxing...
Marine ordered Cheese
Ici, nous avons donc fini notre implémentation. Ce que nous pouvons remarquer :
- Nous avons deux factorys qui représentent des catégories de l'objet pizza, nous pourrons facilement en rajouter une autre.
- Nous avons des classes de pizzas qui ne sont pas liées l'une à l'autre, en rajouter une sera simple.
- Nos factorys sont chargées d'instancier les objets, ce qui permet une flexibilité quant au fait qu'ils ne sont pas directement instanciés directement dans la classe qui les utilise.