logo
    • Home
    • Categories
    • About
  • en-languageEnglish
Design patternBy Pierre Colart

Design Pattern Factory

Description

The factory design pattern aims to solve instantiation problems: typically, we create the object in the class that will use it, which creates inflexibility. This inflexibility is due to the fact that the object is linked to the class that instantiates it, which makes modifications impossible. To counter this problem, the factory pattern delegates the object instantiation to a subclass that replaces the constructor of the object's class. This way, we gain flexibility!

Scenario

🍕 Let's develop a pizzeria, and more specifically a functionality of this pizzeria that simulates the creation of a pizza. This will allow us to highlight this design pattern. The idea will be that our program simulates the pizza order, and that our factory returns the pizzas. In a simple case, one could say that this is not very complicated: we create two pizzas and we're done. But when you go to your pizzeria you have several pizzas, we could use conditional structures but it would become:

  • Unreadable.
  • Difficult to maintain.
  • Very inflexible.
  • And above all, would create huge maintenance costs. That's why our factory will allow us, through the factory, to instantiate the different pizza objects, which will be used through an abstract class.

Advantages and disadvantages

Advantages Disadvantages
✔️Isolation of concrete classes, the factory encapsulates the responsibility and the object creation process, while the client manipulates the objects through an abstract class. Thus each class is isolated and does not depend on other classes. ❌Increase in the number of objects, we do not know the size that a family of objects can take and this can lead to a lot of classes.
✔️Facilitates the change of object, the concrete class only appears where it is instantiated. Thus, changing the type of concrete class on the abstract class is sufficient to obtain an object from the same family. ❌Can become complicated if adding functionality to the abstract class is complexified by adding methods and no longer characterizes certain children. This disadvantage appears in case of a bad implementation or architecture.
✔️Ease of testing, given that each class is separated, testing the classes is easier.

When to use it?

💡 You have objects whose natures cannot be known in advance. 💡 Concrete cases: Framework, class library, authentication mechanism,... 💡 You know that new classes will be added later and will be from the same family.

UML Diagram

In the case of this example, the development of this pizza order/preparation system is divided into two categories:

  • Vegetarian pizzas.
  • Pizzas with meat.

In this situation, we see that a factory pattern can be added.

But why? You know, restaurateurs keep changing their menu. In the event that they were to add a new vegetarian pizza:

  • It will suffice to add a new class for this pizza and it will be linked in the vegetarian factory.

In the event that they decide to add a category like luxury pizzas:

  • It will suffice to create a new factory and create the classes that the factory will return.

In short, as you have understood, we do not know how new classes can be added, and we need flexibility. That's why this design pattern is an excellent solution.

Implementation

Let's start by creating the base of our design package: the Pizza interface. This interface is a bit the center of our application since it will be used in all the classes that we will create, whether for implementation, return object type, ...

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;
    }
}

Here, the choice of an abstract class is made for several reasons:

  • Inheritance: we want to override the Prepare class, which will allow us to add our logic for each pizza.
  • We do not want to directly instantiate the Pizza class, which we do not do with an abstract class, but with a subclass.

In this class we have various methods:

  • 🔪 Prepare: here we prepare the different elements that we will put on our pizza.
  • 🍳 Cook: here the pizza is put in the pizza oven.
  • 📦 Box: here the pizza is put in its pizza box, ready to go.

Okay, now we need to implement our factory, for that we will start by creating our abstract class which will define the methodology of our factories. This methodology is the procedure used to prepare our 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);
}

Our base class is defined, we have two methods:

  • 🤵 OrderPizza: a person X orders a pizza which has a type: the name of the pizza. The order is placed, and the pizza is created until it is delivered.
  • 👨‍🍳 CreatePizza: the cook creates the pizza, here we will instantiate our pizzas by the factory which designates it.

We have our two bases, but before implementing our factories, I suggest creating the classes that will designate the pizzas. In the case of the 4 classes that we are going to implement, we will need to initialize the name and description of our pizzas. In our case, we will do this in the constructor to simplify the process. Then, we will override the Prepare method, which will be responsible for putting the different ingredients on the 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");
    }
}

Our classes are created and you notice redundancy regarding the display of "Adding tomato sauce". Here, it is a personal choice, I have already seen pizzas with a base without tomato sauce, that's why I did not put it in the parent abstract class. Of course, there are ways to avoid this, but here it does not have a huge impact.

Well, we have our 4 types of pizza, now we need to implement the factories that will instantiate our objects. We will create the constants that will represent the type of 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";
}

Now that our factories are implemented, we will test our 4 pizzas in our main method: it will be used to return the targeted pizza which will be filtered by the requested type. As a precaution, we will use a switch, which will avoid having a series of "if/else" which would quickly become unreadable. That's why we created our PizzaType class, which will be used to identify the type of 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;
        }
    }
}

Now that our factories are implemented, we will test our 4 pizzas in our main method:

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 ");

    }
}

Result :

 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

Here, we have finished our implementation. What we can notice:

  • We have two factories representing categories of the pizza object, we can easily add another one.
  • We have pizza classes that are not linked to each other, adding another one will be simple.
  • Our factories are responsible for instantiating the objects, which allows flexibility in the sense that they are not directly instantiated in the class that uses them.

Pierre Colart

Passionate developer and architect who wants to share their world and discoveries in order to make things simpler for everyone.

See profil

Latest posts

Sequences, Time Series and Prediction

© 2023 Switch case. Made with by Pierre Colart