The Abstract Factory pattern builds on the Factory pattern by abstracting the concrete factory implementation behind an abstract class or an interface. It allows us to loosely couple our factories in situations where we are making a family classes. Rather than having one factory class for each concrete class, we can have specialized factories that build a specific set of classes.
The code remains loosely coupled from both the concrete class itself but also from the factory that creates the class. By using the factory itself as an abstraction point, we can add additional factories later on with their own sets of concrete classes. In the end, not only is client code disconnected from the actual object in use, but it’s also loosely coupled from the creation of the object.
This post shows a demonstration of the Abstract Factory Pattern. We have an interface, CookFactory that abstracts two concrete classes. One of the Factory classes is a BurgerCookFactory that produces Cooks who make burgers. The other Factory is a PizzaCookFactory that produces Cooks who make pizzas. The client code uses the Cook interface.
Let’s begin with the interfaces that serve as abstraction points for both Cooks and Factories.
/** * Our factories are going to produce objects of type Cook */ interface Cook { val name : String fun cook() } /** * BurgerCook is made by the BurgerCookFactory */ interface BurgerCook : Cook /** * PizzaCook is made by the PizzaCookFactory */ interface PizzaCook : Cook /** * The CookFactory is the base interface for our Factory */ interface CookFactory { fun getInstance(name : String) : Cook }
We have four interfaces here. Cook is the base interface of all Cook objects. From Cook, we have two subinterfaces called BurgerCook and PizzaCook. The final interface is the CookFactory, which produces all Cook objects. Of course, we now need some concrete classes to go with these interfaces, so let’s begin with BurgerCookFactory.
/** * BurgerCookFactory implements CookFactory and serves as a concrete class to * make Cooks that make burgers */ class BurgerCookFactory : CookFactory { override fun getInstance(name: String) : BurgerCook { return when (name){ "Bob" -> Bob() "Tina" -> Tina() else -> throw IllegalArgumentException("No class available for $name") } } /** * Bob is one concrete class */ private class Bob : BurgerCook { override val name: String get() = "Bob" override fun cook() { println("Bob cooked One Fish, Two Fish, Red Fish Hamburger") } } private class Tina : BurgerCook { override val name: String get() = "Tina" override fun cook() { println("Tina dropped the burger on the floor while cooking it") } } }
The above BurgerCookFactory is responsible for making all BurgerCooks. You will notice that it’s making use of covarient return types by returning BurgerCook rather than Cook. The intent behind BurgerCookFactory is to group all classes that implement BurgerCook into this factory class so that this factory is responsible for this family of classes. Inside of BurgerCookFactory, we have two BurgerCooks, Bob and Tina. We are free to add more BurgerCooks later on and need not worry about breaking existing client code.
After making our BurgerCooks, we decided that we need PizzaCooks also. The existing framework we have makes this fairly straightforward. We only need a PizzaCookFactory.
/** * This factory is for PizzaCooks */ class PizzaCookFactory : CookFactory { override fun getInstance(name: String) : PizzaCook { return when (name){ "Jimmy" -> Jimmy() "Jr" -> JimmyJr() else -> throw IllegalArgumentException("No class available for $name") } } private class Jimmy : PizzaCook { override val name: String get() = "Jimmy" override fun cook() { println("Jimmy is cooking a pizza") } } private class JimmyJr : PizzaCook { override val name: String get() = "Jimmy Junior" override fun cook() { println("Jimmy Junior started dancing rather than cooking a pizza") } } }
The PizzaCookFactory is another Factory class that implements CookFactory. It makes Cooks that implement the PizzaCook interface, which in turn extends the Cook interface. At this point, PizzaCookFactory is usable anywhere Cooks and CookFactory are needed. As our program grows and changes, we can continue to add more factories and cooks when needed.
The Client code for using the factories is really simple. Here is a simple function that accepts a factory and returns a cook.
fun makeCook(factory: CookFactory, name : String) : Cook { return factory.getInstance(name) }
Of course, this is a gross simplification but demonstrates the point. The makeCook function does not care if the factory is BurgerCookFactory or PizzaCookFactory. It only cares that is has a factory of some sort and returns a cook of some sort. Any CookFactory will do and so will all Cooks.
Putting it Together
Here is a complete working program that demonstrates the Abstract Factory Pattern followed by the output.
package ch4.abstractfactory /** * Our factories are going to produce objects of type Cook */ interface Cook { val name : String fun cook() } /** * BurgerCook is made by the BurgerCookFactory */ interface BurgerCook : Cook /** * PizzaCook is made by the PizzaCookFactory */ interface PizzaCook : Cook /** * The CookFactory is the base interface for our Factory */ interface CookFactory { fun getInstance(name : String) : Cook } /** * BurgerCookFactory implements CookFactory and serves as a concrete class to * make Cooks that make burgers */ class BurgerCookFactory : CookFactory { override fun getInstance(name: String) : BurgerCook { return when (name){ "Bob" -> Bob() "Tina" -> Tina() else -> throw IllegalArgumentException("No class available for $name") } } /** * Bob is one concrete class */ private class Bob : BurgerCook { override val name: String get() = "Bob" override fun cook() { println("Bob cooked One Fish, Two Fish, Red Fish Hamburger") } } private class Tina : BurgerCook { override val name: String get() = "Tina" override fun cook() { println("Tina dropped the burger on the floor while cooking it") } } } /** * This factory is for PizzaCooks */ class PizzaCookFactory : CookFactory { override fun getInstance(name: String) : PizzaCook { return when (name){ "Jimmy" -> Jimmy() "Jr" -> JimmyJr() else -> throw IllegalArgumentException("No class available for $name") } } private class Jimmy : PizzaCook { override val name: String get() = "Jimmy" override fun cook() { println("Jimmy is cooking a pizza") } } private class JimmyJr : PizzaCook { override val name: String get() = "Jimmy Junior" override fun cook() { println("Jimmy Junior started dancing rather than cooking a pizza") } } } fun makeCook(factory: CookFactory, name : String) : Cook { return factory.getInstance(name) } fun main(args : Array<String>){ val burgerFactory = BurgerCookFactory() val pizzaFactory = PizzaCookFactory() var cook = makeCook(burgerFactory, "Bob") cook.cook() println() cook = makeCook(burgerFactory, "Tina") cook.cook() println() cook = makeCook(pizzaFactory, "Jimmy") cook.cook() println() cook = makeCook(pizzaFactory, "Jr") cook.cook() }
Output
Bob cooked One Fish, Two Fish, Red Fish Hamburger Tina dropped the burger on the floor while cooking it Jimmy is cooking a pizza Jimmy Junior started dancing rather than cooking a pizza