Hi! Today we will continue to study design patterns and we'll discuss the abstract factory pattern. Design patterns: Abstract factory - 1Here'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.
To fully understand this pattern, you need to be well versed in the following topics:
  • 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
These types of units are interconnected, because they serve in the same army. We could say that the categories listed above are a family of interconnected objects. We understand this. But the abstract factory pattern is used to arrange the creation of various families of interconnected objects. There's nothing complicated here either. Let's continue with the military strategy example. Generally speaking, military units belong to several different warring parties. Depending on whose side they are on, military units can vary significantly in appearance. The foot soldiers, horsemen and archers of the Roman army are not the same as Viking foot soldiers, horsemen and archers. In the military strategy, soldiers of different armies are different families of interconnected objects. It would be funny if a programmer's mistake caused a soldier in a Napoleon-era French uniform, musket at the ready, to be found walking among the ranks of the Roman infantry. The abstract factory design pattern is needed precisely for solving this problem. No, not the problem of the embarrassment that can come from time travel, but the problem of creating various groups of interconnected objects. An abstract factory provides an interface for creating all available products (a family of objects). An abstract factory typically has multiple implementations. Each of them is responsible for creating products of one of the families. Our military strategy would include an abstract factory that creates abstract foot soldiers, archers, and cavalrymen, as well as implementations of this factory. For example, a factory that creates Roman legionnaires and a factory that creates Carthaginian soldiers. Abstraction is this pattern's most important guiding principle. The factory's clients work with the factory and its products only through abstract interfaces. As a result, you don't have to think about which soldiers are currently being created. Instead, you pass this responsibility to some concrete implementation of the abstract factory.

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
Disclaimer: In each interface, we could define methods like 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.
The upshot is that we implemented the abstract factory pattern. Design patterns: Abstract factory - 2

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:
  1. Define product families. Suppose we have two of them:
    • SpecificProductA1, SpecificProductB1
    • SpecificProductA2, SpecificProductB2
  2. For each product within the family, define an abstract class (interface). In our case, we have:
    • ProductA
    • ProductB
  3. Within each product family, each product must implement the interface defined in step 2.
  4. 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();
  5. 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.
The following UML diagram illustrates the instructions outlined above: Design patterns: Abstract factory - 3Now we will write code according to these instructions:
// 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 creatProductA() {
        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:
  1. Refine the coffee-ordering application so that it also works on Linux.
  2. 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!