Hi! Today we will continue to study design patterns and we'll discuss the factory method pattern.
You will find out what it is and what tasks this pattern is suitable for. We will consider this design pattern in practice and study its structure. To ensure everything is clear, you need to understand the following topics:
- Inheritance in Java.
- Abstract methods and classes in Java
What problem does the factory method solve?
All factory design patterns have two types of participants: creators (the factories themselves) and products (the objects created by the factories). Imagine the following situation: we have a factory that produces CodeGym-branded cars. It knows how to create models of cars with various types of bodies:- sedans
- station wagons
- coupes
- CodeGym sedans
- CodeGym station wagons
- CodeGym coupes
- OneAuto sedans
- OneAuto station wagons
- OneAuto coupes
A bit about the factory pattern
Let me remind you that we previously built a small virtual coffee shop. With the help of a simple factory, we learned how to create different types of coffee. Today we will rework this example. Let's recall how our coffee shop looked, with its simple factory. We had a coffee class:
public class Coffee {
public void grindCoffee(){
// Grind the coffee
}
public void makeCoffee(){
// Brew the coffee
}
public void pourIntoCup(){
// Pour into a cup
}
}
And several child classes corresponding to specific types of coffee that our factory could produce:
public class Americano extends Coffee {}
public class Cappuccino extends Coffee {}
public class CaffeLatte extends Coffee {}
public class Espresso extends Coffee {}
We created an enum to make it easy to place orders:
public enum CoffeeType {
ESPRESSO,
AMERICANO,
CAFFE_LATTE,
CAPPUCCINO
}
The coffee factory itself looked like this:
public class SimpleCoffeeFactory {
public Coffee createCoffee(CoffeeType type) {
Coffee coffee = null;
switch (type) {
case AMERICANO:
coffee = new Americano();
break;
case ESPRESSO:
coffee = new Espresso();
break;
case CAPPUCCINO:
coffee = new Cappuccino();
break;
case CAFFE_LATTE:
coffee = new CaffeLatte();
break;
}
return coffee;
}
}
And finally, the coffee shop itself looked like this:
public class CoffeeShop {
private final SimpleCoffeeFactory coffeeFactory;
public CoffeeShop(SimpleCoffeeFactory coffeeFactory) {
this.coffeeFactory = coffeeFactory;
}
public Coffee orderCoffee(CoffeeType type) {
Coffee coffee = coffeeFactory.createCoffee(type);
coffee.grindCoffee();
coffee.makeCoffee();
coffee.pourIntoCup();
System.out.println("Here's your coffee! Thanks! Come again!");
return coffee;
}
}
Modernizing a simple factory
Our coffee shop is running very well. So much so that we are considering expanding. We want to open some new locations. We're bold and enterprising, so we won't crank out boring coffee shops. We want each shop to have a special twist. Accordingly, to begin with, we'll open two locations: one Italian and one American. These changes will affect not only the interior design, but also the drinks offered:- in the Italian coffee shop, we will use exclusively Italian coffee brands, with special grinding and roasting.
- the American location will have larger portions, and we'll serve marshmallows with each order.
public class Americano extends Coffee {}
public class Cappuccino extends Coffee {}
public class CaffeLatte extends Coffee {}
public class Espresso extends Coffee {}
But now we will have 8:
public class ItalianStyleAmericano extends Coffee {}
public class ItalianStyleCappucino extends Coffee {}
public class ItalianStyleCaffeLatte extends Coffee {}
public class ItalianStyleEspresso extends Coffee {}
public class AmericanStyleAmericano extends Coffee {}
public class AmericanStyleCappucino extends Coffee {}
public class AmericanStyleCaffeLatte extends Coffee {}
public class AmericanStyleEspresso extends Coffee {}
Since we want to keep the current business model, we want the orderCoffee(CoffeeType type)
method to undergo as few changes as possible. Take a look at it:
public Coffee orderCoffee(CoffeeType type) {
Coffee coffee = coffeeFactory.createCoffee(type);
coffee.grindCoffee();
coffee.makeCoffee();
coffee.pourIntoCup();
System.out.println("Here's your coffee! Thanks! Come again!");
return coffee;
}
What options do we have? Well, we already know how to write a factory, right? The simplest thing that immediately comes to mind is to write two similar factories, and then pass the desired implementation to our coffee shop's constructor. By doing this, the coffee shop's class will not change.
First, we need to create a new factory class, make it inherit our simple factory, and then override the createCoffee(CoffeeType type)
method. Let's write factories for creating Italian-style coffee and American-style coffee:
public class SimpleItalianCoffeeFactory extends SimpleCoffeeFactory {
@Override
public Coffee createCoffee(CoffeeType type) {
Coffee coffee = null;
switch (type) {
case AMERICANO:
coffee = new ItalianStyleAmericano();
break;
case ESPRESSO:
coffee = new ItalianStyleEspresso();
break;
case CAPPUCCINO:
coffee = new ItalianStyleCappuccino();
break;
case CAFFE_LATTE:
coffee = new ItalianStyleCaffeLatte();
break;
}
return coffee;
}
}
public class SimpleAmericanCoffeeFactory extends SimpleCoffeeFactory{
@Override
public Coffee createCoffee (CoffeeType type) {
Coffee coffee = null;
switch (type) {
case AMERICANO:
coffee = new AmericanStyleAmericano();
break;
case ESPRESSO:
coffee = new AmericanStyleEspresso();
break;
case CAPPUCCINO:
coffee = new AmericanStyleCappuccino();
break;
case CAFFE_LATTE:
coffee = new AmericanStyleCaffeLatte();
break;
}
return coffee;
}
}
Now we can pass the desired factory implementation to CoffeeShop. Let's see what the code for ordering coffee from different coffee shops would look like. For example, Italian-style and American-style cappuccino:
public class Main {
public static void main(String[] args) {
/*
Order an Italian-style cappuccino:
1. Create a factory for making Italian coffee
2. Create a new coffee shop, passing the Italian coffee factory to it through the constructor
3. Order our coffee
*/
SimpleItalianCoffeeFactory italianCoffeeFactory = new SimpleItalianCoffeeFactory();
CoffeeShop italianCoffeeShop = new CoffeeShop(italianCoffeeFactory);
italianCoffeeShop.orderCoffee(CoffeeType.CAPPUCCINO);
/*
Order an American-style cappuccino
1. Create a factory for making American coffee
2. Create a new coffee shop, passing the American coffee factory to it through the constructor
3. Order our coffee
*/
SimpleAmericanCoffeeFactory americanCoffeeFactory = new SimpleAmericanCoffeeFactory();
CoffeeShop americanCoffeeShop = new CoffeeShop(americanCoffeeFactory);
americanCoffeeShop.orderCoffee(CoffeeType.CAPPUCCINO);
}
}
We created two different coffee shops, passing the desired factory to each. On the one hand, we have accomplished our objective, but on the other hand... Somehow this doesn't sit well with the entrepreneurs... Let's figure out what is wrong.
First, the abundance of factories. What? Now for every new location, we're supposed to create its own factory and, in addition to that, make sure that the relevant factory is passed to the constructor when creating a coffee shop?
Second, it is still a simple factory. Just modernized slightly. But we're here to learn a new pattern.
Third, isn't a different approach possible? It would be great if we could put all issues related to coffee preparation into the CoffeeShop
class by linking the processes of creating coffee and servicing orders, while simultaneously maintaining sufficient flexibility to make various styles of coffee.
The answer is yes, we can. This is called the factory method design pattern.
From a simple factory to a factory method
To solve the task as efficiently as possible:- We return the
createCoffee(CoffeeType type)
method to theCoffeeShop
class. - We will make this method abstract.
- The
CoffeeShop
class itself will become abstract. - The
CoffeeShop
class will have child classes.
CoffeeShop
class, which implements the createCoffee(CoffeeType type)
method in accordance with the best traditions of Italian baristas.
Now, one step at a time.
Step 1. Make the Coffee
class abstract. We have two whole families of different products. Still, the Italian and American coffees have a common ancestor — the Coffee
class. It would be proper to make it abstract:
public abstract class Coffee {
public void makeCoffee(){
// Brew the coffee
}
public void pourIntoCup(){
// Pour into a cup
}
}
Step 2. Make CoffeeShop
abstract, with an abstract createCoffee(CoffeeType type)
method
public abstract class CoffeeShop {
public Coffee orderCoffee(CoffeeType type) {
Coffee coffee = createCoffee(type);
coffee.makeCoffee();
coffee.pourIntoCup();
System.out.println("Here's your coffee! Thanks! Come again!");
return coffee;
}
protected abstract Coffee createCoffee(CoffeeType type);
}
Step 3. Create an Italian coffee shop, which is a descendant of the abstract coffee shop. We implement the createCoffee(CoffeeType type)
method in it, taking into account the specifics of Italian recipies.
public class ItalianCoffeeShop extends CoffeeShop {
@Override
public Coffee createCoffee (CoffeeType type) {
Coffee coffee = null;
switch (type) {
case AMERICANO:
coffee = new ItalianStyleAmericano();
break;
case ESPRESSO:
coffee = new ItalianStyleEspresso();
break;
case CAPPUCCINO:
coffee = new ItalianStyleCappuccino();
break;
case CAFFE_LATTE:
coffee = new ItalianStyleCaffeLatte();
break;
}
return coffee;
}
}
Step 4. We do the same for the American-style coffee shop
public class AmericanCoffeeShop extends CoffeeShop {
@Override
public Coffee createCoffee(CoffeeType type) {
Coffee coffee = null;
switch (type) {
case AMERICANO:
coffee = new AmericanStyleAmericano();
break;
case ESPRESSO:
coffee = new AmericanStyleEspresso();
break;
case CAPPUCCINO:
coffee = new AmericanStyleCappuccino();
break;
case CAFFE_LATTE:
coffee = new AmericanStyleCaffeLatte();
break;
}
return coffee;
}
}
Step 5. Check out how American and Italian lattes will look:
public class Main {
public static void main(String[] args) {
CoffeeShop italianCoffeeShop = new ItalianCoffeeShop();
italianCoffeeShop.orderCoffee(CoffeeType.CAFFE_LATTE);
CoffeeShop americanCoffeeShop = new AmericanCoffeeShop();
americanCoffeeShop.orderCoffee(CoffeeType.CAFFE_LATTE);
}
}
Congratulations. We just implemented the factory method design pattern using our coffee shop as an example.
The principle behind factory methods
Now let's consider in greater detail what we got. The diagram below shows the resulting classes. The green blocks are creator classes, and the blue blocks are product classes. What conclusions can we make?- All products are implementations of the abstract
Coffee
class. - All creators are implementations of the abstract
CoffeeShop
class. - We see two parallel class hierarchies:
- Hierarchy of products. We see Italian descendants and American descendants
- Hierarchy of creators. We see Italian descendants and American descendants
- The
CoffeeShop
superclass has no information about which specific product (Coffee
) will be created. - The
CoffeeShop
superclass delegates the creation of a specific product to its descendants. - Each descendant of the
CoffeeShop
class implements acreateCoffee()
factory method in accordance with its own specifics features. In other words, the implementations of the producer classes prepare specific products based on the specifics of the producer class.
Structure of a factory method
The diagram above shows the general structure of the factory method pattern. What else is important here?- The Creator class implements all methods that interact with products, except the factory method.
- The abstract
factoryMethod()
method must be implemented by all descendants of theCreator
class. - The
ConcreteCreator
class implements thefactoryMethod()
method, which directly creates the product. - This class is responsible for creating specific products. This is the only class with information about creating these products.
- All products must implement a common interface, i.e. they must be descendants of a common product class. This is necessary so that classes that use products can operate on them as abstractions, rather than specific implementations.
GO TO FULL VERSION