Abstract Factory - Design Pattern Explanation
Design patterns like "Decorator", "Singleton" are common and be easily recognized. The "Builder" pattern is also recognizable whenever you use Java's StringBuilder class.
Some patterns which are commonly used by frameworks are not easily recognizable unless you are a framework author. Recently, I spent time trying to recognize and understand Abstract Factory design pattern.
In this post, we will look at "Abstract Factory" and explain it with an example. I consulted multiple resources, and ultimately to gain confidence, I had to rely upon The Gang of Four [1] design patterns book.
This post is intended as refresher for a reader who has read the Gang of Four chapter on Abstract Factory. This post presents an example which the reader can relate to in the modern world.
Abstract Factory is a Factory for Factories. To understand this, you will first have to understand the Factory Design pattern, which encapsulates creation of objects. Factory pattern is recognized when instead of using new SomeClass() we call SomeClass.createObject() static method. The advantage is SomeClass is independent of your code, it could be supplied as a dependency from someone else and you simply use the factory. The person controlling the factory can modify the object creation process.
For example, SomeClass.createObject() in version1, can be return new SomeClass(arg1) and in version2, it can change to return new SomeClass(arg1, arg2) with you as the caller, invoking the object creation entirely as SomeClass.createObject() unaffected by the change made by the creator of SomeClass.
Factory pattern is easy to understand. The next step comes in dealing with Abstract Factory.
Intent
Abstract Factory provides an interface for creating families of related or dependent objects without specifying the concrete classes.
Canonical Design
Factory is a class that defers the instantiation of the object to the subclass.Factory encapsulates the decision-making that figures out which specific subclass to instantiate. There are three different kinds of Factory patterns observable with respect to object instantiation.
- Simple Factory.
-
The client directly uses a static method of a subclass, to instantiate an object.
- Factory Pattern
-
The client uses a Factory class to create an object. A Factory, is a class that defers the instantiation of an object to the subclasses.Factory method creates only one product
- Abstract Factory
-
Abstract Factory is a constructional design pattern that is used to create a family of related products.
Abstract Factory is applicable when the
System should be configured with one of multiple families of Products.
The family of related product objects is designed to to used together and we need to enforce this constraint.
Design Problem
In this problem, we are trying to design a "Operating System Installer" for Unix family of Operating Systems. We know there are two popular variants of Unix, there popular operating system with Linux kernel and related application stack, and there is BSD systems.
Each Operating System will consists of components like
Bootloader
Kernel
Shell
DisplayManager
WindowManager
Applications
The installer will have to abstract those components and help the client create an Unix operating system choice.
Correspondence with Canonical Design
Let's look at each of these in some detail.
Product Interfaces
Starting with products, these are:
Bootloader
Kernel
Shell
DisplayManager
WindowManager
BaseApplications
We will have Interfaces for the products.
java/abstractfactory/IBootLoader.java (Source)
java/abstractfactory/IKernel.java (Source)
public interface IKernel { /** * Load the kernel on top of the system image. */ void loadKernel(); }
java/abstractfactory/IShell.java (Source)
java/abstractfactory/IDisplayManager.java (Source)
Concrete Products
Each of these can create many difference concrete products. For the different concrete products like
-
Bootloader
BSDBootLoader
LinuxBootLoader
-
Kernel
BSDKernel
Linux
-
Shell
BASH
CShell
-
DisplayManager
X11
WayLand
-
WindowManager
Gnome
KDE
-
BaseApplications
SystemVUnix
GNUApplications
ProprietaryApps
Let's denote these concrete products in code that can be instantiated.
java/abstractfactory/BSDBootLoader.java (Source)
public class BSDBootLoader implements IBootLoader{ /** * Boot up the System Image. */ @Override public void bootUp() { System.out.println("Booting: " + this.getClass().getSimpleName()); } }
java/abstractfactory/BSDKernel.java (Source)
public class BSDKernel implements IKernel { /** * Load the kernel on top of the system image. */ @Override public void loadKernel() { System.out.println("Loading: " + this.getClass().getSimpleName()); } }
java/abstractfactory/Bash.java (Source)
public class Bash implements IShell { @Override public void loadShell() { System.out.println("Loading: " + this.getClass().getSimpleName()); } }
java/abstractfactory/CShell.java (Source)
public class CShell implements IShell { @Override public void loadShell() { System.out.println("Loading: " + this.getClass().getSimpleName()); } }
java/abstractfactory/GNUApplications.java (Source)
public class GNUApplications implements IBaseApplications{ @Override public void installApplications() { System.out.println("Installing: " + this.getClass().getSimpleName()); } }
java/abstractfactory/Gnome.java (Source)
public class Gnome implements IWindowManager { @Override public void installWindowManager() { System.out.println("Installing: " + this.getClass().getSimpleName()); } }
java/abstractfactory/KDE.java (Source)
public class KDE implements IWindowManager { @Override public void installWindowManager() { System.out.println("Installing: " + this.getClass().getSimpleName()); } }
java/abstractfactory/Linux.java (Source)
public class Linux implements IKernel{ /** * Load the kernel on top of the system image. */ @Override public void loadKernel() { System.out.println("Loading: " + this.getClass().getSimpleName()); } }
java/abstractfactory/LinuxBootLoader.java (Source)
public class LinuxBootLoader implements IBootLoader { @Override public void bootUp() { System.out.println("Booting: " + this.getClass().getSimpleName()); } }
java/abstractfactory/ProprietaryApps.java (Source)
public class ProprietaryApps implements IBaseApplications{ @Override public void installApplications() { System.out.println("Installing: " + this.getClass().getSimpleName()); } }
java/abstractfactory/SystemVUnix.java (Source)
public class SystemVUnix implements IBaseApplications{ @Override public void installApplications() { System.out.println("Installing: " + this.getClass().getSimpleName()); } }
Factories
The products are created by Factories
BSDFactory
LinuxFactory
UbuntuFactory
java/abstractfactory/BSDFactory.java (Source)
public class BSDFactory implements IUnixFactory { @Override public IBootLoader installBootLoader() { return new BSDBootLoader(); } @Override public IKernel installKernel() { return new BSDKernel(); } @Override public IShell installShell() { return new CShell(); } @Override public IDisplayManager installDisplayManager() { return new X11(); } @Override public IWindowManager installWindowManager() { return new KDE(); } @Override public IBaseApplications installApps() { return new SystemVUnix(); } }
java/abstractfactory/LinuxFactory.java (Source)
public class LinuxFactory implements IUnixFactory { @Override public IBootLoader installBootLoader() { return new LinuxBootLoader(); } @Override public IKernel installKernel() { return new Linux(); } @Override public IShell installShell() { return new Bash(); } @Override public IDisplayManager installDisplayManager() { return new X11(); } @Override public IWindowManager installWindowManager() { return new Gnome(); } @Override public IBaseApplications installApps() { return new GNUApplications(); } }
Abstract Factory
The factories will implement an abstraction provided by the Abstract Factory
Client
The design is best understood from the view of the client which uses the Abstract Factory to the create the products.
java/abstractfactory/OperatingSystem.java (Source)
public class OperatingSystem { IUnixFactory unixFactory; public OperatingSystem(IUnixFactory unixFactory) { this.unixFactory = unixFactory; } /** * installerClient uses only the interfaces declared by AbstractFactory (IUnixFactory) and AbstractProduct * (IBootLoader, IKernel, IShell, IDisplayManager, IWindowManager, IBaseApplications) classes. */ public void installerClient() { IBootLoader bootLoader = unixFactory.installBootLoader(); IKernel kernel = unixFactory.installKernel(); IShell shell = unixFactory.installShell(); IDisplayManager displayManager = unixFactory.installDisplayManager(); IWindowManager windowManager = unixFactory.installWindowManager(); IBaseApplications applications = unixFactory.installApps(); bootLoader.bootUp(); kernel.loadKernel(); shell.loadShell(); displayManager.installDisplayManager(); windowManager.installWindowManager(); applications.installApplications(); } /** * client will not know the * products the type of bootloader, kernel, shell, display, window manager or applications. * That is encapsulated in factory used by the client. * */ private static void factoryClient(IUnixFactory factory) { OperatingSystem operatingSystem = new OperatingSystem(factory); operatingSystem.installerClient(); } public static void main(String[] args) { IUnixFactory factory; factory = new LinuxFactory(); factoryClient(factory); factory = new BSDFactory(); factoryClient(factory); factory = new UbuntuFactory(); factoryClient(factory); } }
The execution looks like this.
Booting: LinuxBootLoader Loading: Linux Loading: Bash Installing: X11 Installing: Gnome Installing: GNUApplications Booting: BSDBootLoader Loading: BSDKernel Loading: CShell Installing: X11 Installing: KDE Installing: SystemVUnix Booting: LinuxBootLoader Loading: Linux Loading: Bash Installing: X11 Installing: Gnome Installing: ProprietaryApps
Tabulated Correspondence
Mapping of the code with various elements in the design helps us to appreciate this pattern.
Hope this was useful. If you have any comments on this article, please add your thoughts in the comments section of this article.
Thank you for reading!