Factory Method pattern

Factory Method pattern

Summary

The Factory Method Pattern defines an interface for creating an object, but lets subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.

Problem

(The problem describes here is similar to the pizza example from the book Head First Design Patterns)

Imagine you are the owner of the lovely milk tea store in the town. Your store offers vary of milk tea, such as brown sugar milk tea, matcha latte, mango milk tea… To manage the store, you have some code like this:

class MilkTeaStore {

   fun orderMilkTea(type: String) {
       val milkTea = createMilkTea(type)

       milkTea.prepareIngredients()
       milkTea.make()
       milkTea.pack()
       milkTea.deliver()
   }

   private fun createMilkTea(type: String): MilkTea {
       return when(type) {
           BROWN_SUGAR_TYPE -> BrownSugarMilkTea()
           MATCHA_LATTE -> MatchaLatte()
           else -> throw Exception("Unsupported type")
       }
   }
}

The createMilkTea(String) function above determines the appropriate type of milk tea to make. Every time a new kind of milk tea is added, you need to change the createMilkTea(String) function. Here we can have an improvement. Currently, MilkTeaStore has two responsibilities: support ordering milk tea and also create different milk tea. If you want to separate these concerns into smaller parts, you can move the createMilkTea(String) to a new Factory class like this:

class MilkTeaStore (private val factory: MilkTeaFactory) {

   fun orderMilkTea(type: String) {
       val milkTea = factory.createMilkTea(type)

       milkTea.prepareIngredients()
       milkTea.make()
       milkTea.pack()
       milkTea.deliver()
   }
}

class MilkTeaFactory {
   fun createMilkTea(type: String): MilkTea {
       return when(type) {
           BROWN_SUGAR_TYPE -> BrownSugarMilkTea()
           MATCHA_LATTE -> MatchaLatte()
           else -> throw Exception("Unsupported type")
       }
   }
}

At first, it seems like we are just moving the creation code to another object for no reason. But the point is, by encapsulating the creation code in one class, we can support other clients. Other parts of the program (apart from MilkTeaStore) might want to create milk tea for different purposes. Now have only one place to make modifications when the implementation changes.

So far so good, but here comes the problem. As your business keeps growing, now you are planning to open your store in a new country. People in different countries have different tastes, so you have to change your recipe a little bit. Now the BrownSugarMilkTea in Taiwan will be a little different from the BrownSugarMilkTea in Singapore. How can we support that?

Approach #1: Injected Factory (Composition Factory)

One way to solve this problem is to create multiple factories and make a MilkTeaStore with a correspondence factory.

injected_factory.png

fun main() {
   val singaporeFactory = SingaporeMilkTeaFactory()
   var store = MilkTeaStore(singaporeFactory)
   store.orderMilkTea(MATCHA_LATTE)

   val taiwanFactory = TaiwanMilkTeaFactory()
   store = MilkTeaStore(taiwanFactory)
   store.orderMilkTea(MATCHA_LATTE)
}

class MilkTeaStore(private val factory: MilkTeaFactory) {
   fun orderMilkTea(type: String) {
       val milkTea = factory.createMilkTea(type)

       milkTea.prepareIngredients()
       milkTea.make()
       milkTea.pack()
       milkTea.deliver()
   }
}

interface MilkTeaFactory {
   fun createMilkTea(type: String): MilkTea
}

class SingaporeMilkTeaFactory: MilkTeaFactory {
   override fun createMilkTea(type: String): MilkTea {
       return when(type) {
           BROWN_SUGAR_TYPE -> SingaporeBrownSugarMilkTea()
           MATCHA_LATTE -> SingaporeMatchaLatte()
           else -> throw Exception("Unsupported type")
       }
   }

}

class TaiwanMilkTeaFactory: MilkTeaFactory {
   override fun createMilkTea(type: String): MilkTea {
       return when(type) {
           BROWN_SUGAR_TYPE -> TaiwanBrownSugarMilkTea()
           MATCHA_LATTE -> TaiwanMatchaLatte()
           else -> throw Exception("Unsupported type")
       }
   }
}

In this approach, the client will create the Factory and inject it into the MilkTeaStore. That means the store delegates object creation to the factory, which is injected from outside by the client. The store itself has no say in how the milk teas are created, it just asks for them from the factory.

Approach #2: Factory method pattern

Another approach is to get rid of the Factory and create multiple MilkTeaStore classes. What we’re going to do is to put the createMilkTea(String) function in the MilkTeaStore, make it abstract, and let the subclasses decide which object to instantiate.

FactoryMethod.png

fun main() {
   var store: MilkTeaStore = SingaporeMilkTeaStore()
   store.orderMilkTea(MATCHA_LATTE)

   store = TaiwanMilkTeaStore()
   store.orderMilkTea(MATCHA_LATTE)
}

abstract class MilkTeaStore {
   fun orderMilkTea(type: String) {
       val milkTea = createMilkTea(type)

       milkTea.prepareIngredients()
       milkTea.make()
       milkTea.pack()
       milkTea.deliver()
   }

   abstract fun createMilkTea(type: String): MilkTea
}

class SingaporeMilkTeaStore: MilkTeaStore() {
   override fun createMilkTea(type: String): MilkTea {
       return when(type) {
           BROWN_SUGAR_TYPE -> SingaporeBrownSugarMilkTea()
           MATCHA_LATTE -> SingaporeMatchaLatte()
           else -> throw Exception("Unsupported type")
       }
   }
}

class TaiwanMilkTeaStore: MilkTeaStore() {
   override fun createMilkTea(type: String): MilkTea {
       return when(type) {
           BROWN_SUGAR_TYPE -> TaiwanBrownSugarMilkTea()
           MATCHA_LATTE -> TaiwanMatchaLatte()
           else -> throw Exception("Unsupported type")
       }
   }
}

In this case, the MilkTeaStore class and createMilkTea(String) function are both abstract. Here, the MilkTeaStore’s subclasses control how to create concrete objects. But the client is still the one who really decided which kind of objects are created by choosing which “store” they want.

We have introduced two ways to deal with this problem, so what is the difference between them and what approach should we choose?

The first thing to notice is the first approach uses composition, while the second use inheritance. Besides that, in the first approach, the Factory class takes responsibility for creating concrete objects. While in the second approach, the logic to create concrete objects is encapsulated in the MilkTeaStore’s subclasses. In this example, I think it is fine to use either way. I actually like the composition approach more in general, and it seems simpler. But there are some cases where using the factory method pattern makes more sense. If you want “hide” (encapsulate) the creation logic in a specific class instead of exposing it to the outside, then the Factory method is a nice way to go. Let’s say, for some reason, you don’t want a third-party component to create the MilkTea objects. With the injected factory approach, there is no way to restrict that, anyone can access the factory class and create the object as they want.

Let's take one more example to make that point clearer.

abstract class UIRenderer {

   private var someInternalState: Int = 0

   fun render() {
       val component = createUIComponent()
       component.initState(someInternalState)
       component.initListener()
       component.show()
   }

   abstract fun createUIComponent(): UIComponent
}

class AndroidUIRenderer: UIRenderer() {
   override fun createUIComponent() : UIComponent {
       return AndroidUIComponent()
   }
}

interface UIComponent {
   fun initState(internalState: Int)
   fun initListener()
   fun show()
}

class AndroidUIComponent: UIComponent {
    override fun initState(internalState: Int) {}
    override fun initListener() {}
    override fun show() {}
}

class IOSUIComponent: UIComponent {
    override fun initState(internalState: Int) {}
    override fun initListener() {}
    override fun show() {}
}

Suppose, you want to make sure every time a UIComponent is created, it has to initialize some state and listener. The Factory method pattern works well here, as no one can easily add their own changes to the procedure. No one can access the component that is not initialized correctly.

Therefore, use the Factory Method when you don’t know ahead of time the exact types and dependencies of the objects your code should work with. When you need to implement the concrete type, just subclass and implement the factory method to return the type you want.

UML Diagram

factorymethod.png

Considerations

The Factory Method Pattern gives us a way to encapsulate the instantiations of concrete types and avoid tight coupling between the creator and the concrete products. But like some other patterns, it may make the code become more complicated than it should be.

References:

Head First Design Patterns by Eric Freeman, Elisabeth Robson, Bert Bates, Kathy Sierra

Factory method - refactoring.guru

From No Factory to Factory Method