Hi! Today we will continue to study design patterns and we'll discuss the abstract factory pattern.
Here's what we'll cover in the lesson:
- We'll discuss what an abstract factory is and what problem this pattern solves
- We'll create the skeleton of a cross-platform application for ordering coffee through a user interface
- We'll study instructions on how to use this pattern, including looking at a diagram and code
- And as a bonus, this lesson includes a hidden Easter egg that will help you learn how to use Java to determine the name of the operating system and, depending on the result, to perform one another action or another.
- inheritance in Java
- abstract classes and methods in Java
What problems does an abstract factory solve?
An abstract factory, like all factory patterns, helps us to ensure that new objects to be created correctly. We use it to manage the "production" of various families of interconnected objects. Various families of interconnected objects... What does that mean? Don't worry: in practice, everything is simpler than it might seem. To begin with, what could a family of interconnected objects be? Suppose we are developing a military strategy involving several types of units:- infantry
- cavalry
- archers
Let's continue automating our coffee shop
In the last lesson, we studied the factory method pattern. We used it to expand our coffee business and open several new locations. Today we'll continue to modernize our business. Using the abstract factory pattern, we will lay the foundation for a new desktop application for ordering coffee online. When writing a desktop application, we should always think about cross-platform support. Our application must work on both macOS and Windows (spoiler: Support for Linux is left for you to implement as homework). What will our application look like? Pretty simple: it will be a form consisting of a text field, a selection field, and a button. If you have experience using different operating systems, you have certainly noticed that buttons on Windows are rendered differently than on a Mac. As is everything else... Well, let's begin. As you probably already realized, the product families will consist of elements of the graphical interface:- buttons
- text fields
- selection fields
onClick
, onValueChanged
, or onInputChanged
. In other words, we could define methods that will allow us to handle various events (pressing a button, entering text, selecting a value in a selection box). All this is deliberately omitted here so as to not overload the example and to make it clearer as we study the factory pattern.
Let's define abstract interfaces for our products:
public interface Button {}
public interface Select {}
public interface TextField {}
For each operating system, we must create interface elements in the style the operating system. We'll are writing code for Windows and MacOS.
Let's create implementations for Windows:
public class WindowsButton implements Button {
}
public class WindowsSelect implements Select {
}
public class WindowsTextField implements TextField {
}
Now we do the same for MacOS:
public class MacButton implements Button {
}
public class MacSelect implements Select {
}
public class MacTextField implements TextField {
}
Excellent. Now we can proceed to our abstract factory, which will create all available abstract product types:
public interface GUIFactory {
Button createButton();
TextField createTextField();
Select createSelect();
}
Superb. As you can see, we haven't done anything complicated yet. Everything that follows is also simple. By analogy with the products, we create various factory implementations for each OS.
Let's start with Windows:
public class WindowsGUIFactory implements GUIFactory {
public WindowsGUIFactory() {
System.out.println("Creating GUIFactory for Windows OS");
}
public Button createButton() {
System.out.println("Creating Button for Windows OS");
return new WindowsButton();
}
public TextField createTextField() {
System.out.println("Creating TextField for Windows OS");
return new WindowsTextField();
}
public Select createSelect() {
System.out.println("Creating Select for Windows OS");
return new WindowsSelect();
}
}
We've added some console output inside the methods and constructor in order to further illustrate what is happening.
Now for macOS:
public class MacGUIFactory implements GUIFactory {
public MacGUIFactory() {
System.out.println("Creating GUIFactory for macOS");
}
@Override
public Button createButton() {
System.out.println("Creating Button for macOS");
return new MacButton();
}
@Override
public TextField createTextField() {
System.out.println("Creating TextField for macOS");
return new MacTextField();
}
@Override
public Select createSelect() {
System.out.println("Creating Select for macOS");
return new MacSelect();
}
}
Note that each method signature indicates that the method returns an abstract type. But inside the methods, we are creating specific implementations of the products. This is the only place we control the creation of specific instances.
Now it's time to write a class for the form. This is a Java class whose fields are interface elements:
public class CoffeeOrderForm {
private final TextField customerNameTextField;
private final Select coffeeTypeSelect;
private final Button orderButton;
public CoffeeOrderForm(GUIFactory factory) {
System.out.println("Creating coffee order form");
customerNameTextField = factory.createTextField();
coffeeTypeSelect = factory.createSelect();
orderButton = factory.createButton();
}
}
An abstract factory that creates interface elements is passed to the form's constructor. We will pass the necessary factory implementation to the constructor in order to create interface elements for a particular OS.
public class Application {
private CoffeeOrderForm coffeeOrderForm;
public void drawCoffeeOrderForm() {
// Determine the name of the operating system through System.getProperty()
String osName = System.getProperty("os.name").toLowerCase();
GUIFactory guiFactory;
if (osName.startsWith("win")) { // For Windows
guiFactory = new WindowsGUIFactory();
} else if (osName.startsWith("mac")) { // For Mac
guiFactory = new MacGUIFactory();
} else {
System.out.println("Unknown OS. Unable to draw form :(");
return;
}
coffeeOrderForm = new CoffeeOrderForm(guiFactory);
}
public static void main(String[] args) {
Application application = new Application();
application.drawCoffeeOrderForm();
}
}
If we run the application on Windows, we get the following output:
Creating GUIFactory for Windows OS
Creating coffee order form
Creating TextField for Windows OS
Creating Select for Windows OS
Creating Button for Windows OS
On a Mac, the output will be as follows:
Creating GUIFactory for macOS
Creating coffee order form
Creating TextField for macOS
Creating Select for macOS
Creating Button for macOS
On Linux:
Unknown OS. Unable to draw form :(
And now we summarize. We wrote the skeleton of a GUI-based application in which the interface elements are created specifically for the relevant OS. We'll concisely repeat what we have created:
- A product family consisting of an input field, a selection field, and a button.
- Different implementations of the product family for Windows and macOS.
- An abstract factory that defines an interface for creating our products.
- Two implementations of our factory, each responsible for creating a specific family of products.
- A form (a Java class) whose fields are abstract interface elements that are initialized with the necessary values in the constructor using an abstract factory.
- Application class Inside this class, we create a form, passing the desired factory implementation to its constructor.
Abstract factory: how to use
An abstract factory is a design pattern for managing the creation of various product families without being tied to concrete product classes. When using this pattern, you must:- Define product families. Suppose we have two of them:
SpecificProductA1
,SpecificProductB1
SpecificProductA2
,SpecificProductB2
- For each product within the family, define an abstract class (interface). In our case, we have:
ProductA
ProductB
- Within each product family, each product must implement the interface defined in step 2.
- Create an abstract factory, with methods for creating each product defined in step 2. In our case, these methods will be:
ProductA createProductA();
ProductB createProductB();
- Create abstract factory implementations so that each implementation controls the creation of products of a single family. To do this, inside each implementation of the abstract factory, you need to implement all creations methods so that they create and return specific product implementations.
// Define common product interfaces
public interface ProductA {}
public interface ProductB {}
// Create various implementations (families) of our products
public class SpecificProductA1 implements ProductA {}
public class SpecificProductB1 implements ProductB {}
public class SpecificProductA2 implements ProductA {}
public class SpecificProductB2 implements ProductB {}
// Create an abstract factory
public interface AbstractFactory {
ProductA createProductA();
ProductB createProductB();
}
// Implement the abstract factory in order to create products in family 1
public class SpecificFactory1 implements AbstractFactory {
@Override
public ProductA createProductA() {
return new SpecificProductA1();
}
@Override
public ProductB createProductB() {
return new SpecificProductB1();
}
}
// Implement the abstract factory in order to create products in family 2
public class SpecificFactory2 implements AbstractFactory {
@Override
public ProductA createProductA() {
return new SpecificProductA2();
}
@Override
public ProductB createProductB() {
return new SpecificProductB2();
}
}
Homework
To reinforce the material, you can do 2 things:- Refine the coffee-ordering application so that it also works on Linux.
- Create your own abstract factory for producing units involved in any military strategy. This can be either a historical military strategy involving with real armies, or a fantasy one with orcs, gnomes, and elves. The important thing is to choose something that interests you. Be creative, print messages on the console, and enjoy learning about patterns!
GO TO FULL VERSION