This is an extension of my previous post regarding the Factory Pattern in Java. The Abstract Factory
pattern is generally used to create a family of dependent or related products while the Factory Pattern
is used to create one type of Object. Both patterns consecrate object instantiation to subclasses. As I did in the previous post I’m using Sandwiche
s for this example. The ingredients of the sandwiches are limited to one to simplify the example (they use a single object for each abstract method). In reality sandwiches could contain more than one condiment.
Example Problem
This extends the example problem described in the previous post The Factory Pattern. Here we will extend the Sandwiche
s being created by applying the Abstract Factory Method.
Abstract Sandwich Factory
Let’s start with an abstract
sandwich factory. This factory object defines the methods responsible for creating the basic elements of a sandwich. Each of the elements (Bread
, Condiment
, Topping
and Meat
are interfaces (with limited functionality for this example). Each method returns a different type defined via interface (see below).
public abstract class AbstractSandwichFactory { public abstract Bread createBread(); public abstract Condiment createCondiments(); public abstract Topping createToppings(); public abstract Meat createMeat(); } |
Sandwich Factory Implementations
Sandwich factory implementations define the ingredients used to create a sandwich.
public class ItalianSandwichFactory extends AbstractSandwichFactory { @Override public Bread createBread() { return new Roll(); } @Override public Condiment createCondiments() { return new Mustard(); } @Override public Topping createToppings() { return new Lettuce(); } @Override public Meat createMeat() { return new Salami(); } } public class MontrealSandwichFactory extends AbstractSandwichFactory { @Override public Bread createBread() { return new Rye(); } @Override public Condiment createCondiments() { return new Mustard(); } @Override public Topping createToppings() { return null; // just meat, no toppings! } @Override public Meat createMeat() { return new Smoked(); } } |
Abstract Sandwich Object
This is type of object the sandwich stores will return.
public abstract class Sandwich { Bread bread; Condiment condiment; Topping topping; Meat meat; abstract void prepare(); void pack() { System.out.println("Packing sandwich to go!"); } } |
Sandwich Implementations
These are the instances that the subclasses (sandwich stores) will create when a sandwich is requested.
public class MontrealSandwich extends Sandwich { private AbstractSandwichFactory sandwichFactory; public MontrealSandwich(AbstractSandwichFactory sandwichFactory) { this.sandwichFactory = sandwichFactory; } @Override void prepare() { bread = sandwichFactory.createBread(); condiment = sandwichFactory.createCondiments(); meat = sandwichFactory.createMeat(); } } public class ItalianSandwich extends Sandwich { AbstractSandwichFactory sandwichFactory; public ItalianSandwich(AbstractSandwichFactory sandwichFactory) { this.sandwichFactory = sandwichFactory; } @Override void prepare() { bread = sandwichFactory.createBread(); condiment = sandwichFactory.createCondiments(); topping = sandwichFactory.createToppings(); meat = sandwichFactory.createMeat(); } } |
Sandwich Stores
The client will write implementations of sandwich stores which are responsible for the instantiation of the sandwich objects (as we’ve already done in the previous Factory Pattern post). The sandwich stores own and instantiate the appropriate AbstractSandwichFactory. If an implementation of a sandwich doesn’t exist, the stores are able to provide their own implementation of AbstractSandwichFactory
giving them complete control over how the system works within the constraints of the design.
public abstract class SandwichStore { public abstract Sandwich createSandwich(String type); } public class ItalianSandwichStore extends SandwichStore { private Sandwich sandwich = null; private AbstractSandwichFactory sandwichFactory = new ItalianSandwichFactory(); @Override public Sandwich createSandwich(String type) { if (type.equals("italian")) { sandwich = new ItalianSandwich(sandwichFactory); } return sandwich; } } public class MontrealSandwichStore extends SandwichStore { private Sandwich sandwich = null; private final AbstractSandwichFactory sandwichFactory = new MontrealSandwichFactory(); @Override public Sandwich createSandwich(String type) { if (type.equals("smoked")) { sandwich = new MontrealSandwich(sandwichFactory); } return sandwich; } } |
Using the Sandwich Factory as a client
The client is only responsible for creating the desired SandwichStore
s. The client can they request a sandwich using the createSanwich()
method.
Summary
When new sandwiches are added or changes to existing sandwiches are made the logic pertaining to those sandwiches is encapsulated within their own classes. We’re programming against abstractions and interfaces instead of implementations. Maintenance and new features should be seamless following this design.