Abstract Factory pattern

Abstract Factory pattern

Summary

Abstract Factory provides an interface for creating families of related objects, without specifying concrete classes.

Problem

Let’s take an example to find out what problems this pattern is trying to solve.

abstract factory car factory-01.png

Bob is planning to open a car factory. His business is relatively small at the moment, his factory can only produce two types of cars: trucks and vans. Each type of car consists of several components: a car body, a wheel, and a brake. Let’s find out how Bob builds a program to manage his business.

Simple approach

The first step to solve this problem is of course creating all classes that represent the car type and its components.

class Car(val body: CarBody, val wheels: Wheels, val brakes: Brakes)

interface CarBody
class TruckBody: CarBody
class MinivanBody: CarBody

interface Wheels
class TruckWheels: Wheels
class MinivanWheels: Wheels

interface Brakes
class TruckBrakes: Brakes
class MinivanBrakes: Brakes

After that, Bob needs to make a function that creates a car object. This function must be able to produce two types of cars with all of their components. After some consideration, Bob comes up with this idea.

fun createCar(type: String): Car? {
   return when(type) {
       TRUCK_TYPE -> Car(TruckBody(), TruckWheels(), TruckBrakes())
       MINIVAN_TYPE -> Car(MinivanBody(), MinivanWheels(), MinivanBrakes())
       else -> null
   }
}

fun main(args: Array<String>) {
   val truck = createCar(TRUCK_TYPE)
   val minivan = createCar(MINIVAN_TYPE)
}

At first glance, this “factory” function seems pretty nice. It takes a type as a parameter and then produces a car based on that type. Every time Bob needs to build a car, he can call this function with the type and receive the correct car he expected.

So what is the problem here? First, as Bob’s business grows, he will need to create more types of cars, like SUV or Sedan. He will also need to add more components to make his car better, something like car body decorations or a super fantastic headlight. With every new type of car or new component introduced, Bob has to update this factory function. We can see that this function has kind of too much responsibility. Ideally, we want each function to have only one responsibility, and have only a single reason to change (Single Responsibility Principle).

Abstract Factory pattern approach

The purpose of the Abstract Factory is to provide an interface for creating families of related objects. The Abstract Factory is an interface with a list of creation methods for all products that are part of the product family.

fun main(args: Array<String>) {
   val truck = makeCar(TRUCK_TYPE)
   val minivan = makeCar(MINIVAN_TYPE)
}

fun makeCar(type: String): Car {
   val factory = getCarFactory(type)
   return Car(factory.createBody(), factory.createWheels(), factory.createBrakes())
}

fun getCarFactory (type: String): CarFactory {
   return when (type) {
       TRUCK_TYPE -> TruckFactory()
       MINIVAN_TYPE -> MinivanFactory()
       else -> throw Exception("Unsupported type")
   }
}

interface CarFactory {
   fun createBody(): CarBody
   fun createWheels(): Wheels
   fun createBrakes(): Brakes
}

class TruckFactory: CarFactory {
   override fun createBody(): CarBody = TruckBody()
   override fun createWheels(): Wheels = TruckWheels()
   override fun createBrakes(): Brakes = TruckBrakes()
}

class MinivanFactory: CarFactory {
   override fun createBody(): CarBody = MinivanBody()
   override fun createWheels(): Wheels = MinivanWheels()
   override fun createBrakes(): Brakes = MinivanBrakes()
}

What if a new type of car is introduced? Bob just has to create a new factory and update the logic of selecting a factory. All the logic in other factories is not changed.

fun getCarFactory (type: String): CarFactory {
   return when (type) {
       TRUCK_TYPE -> TruckFactory()
       MINIVAN_TYPE -> MinivanFactory()
       SUV_TYPE -> SUVFactory()
       else -> throw Exception("Unsupported type")
   }
}

class SUVBody: CarBody
class SUVWheels: Wheels
class SUVBrakes: Brakes

class SUVFactory: CarFactory {
   override fun createBody(): CarBody = SUVBody()
   override fun createWheels(): Wheels = SUVWheels()
   override fun createBrakes(): Brakes = SUVBrakes()
}

And what if a new car component is introduced? Bob only needs to update the code in the factories, while the code creating a factory is not affected. Every class now has only one responsibility.

interface Decors
class TruckDecors: Decors
class MinivanDecors: Decors

interface CarFactory {
   fun createBody(): CarBody
   fun createWheels(): Wheels
   fun createBrakes(): Brakes
   fun createDecors(): Decors
}

class TruckFactory: CarFactory {
   override fun createBody(): CarBody = TruckBody()
   override fun createWheels(): Wheels = TruckWheels()
   override fun createBrakes(): Brakes = TruckBrakes()
   override fun createDecors(): Decors = TruckDecors()

}

class MinivanFactory: CarFactory {
   override fun createBody(): CarBody = MinivanBody()
   override fun createWheels(): Wheels = MinivanWheels()
   override fun createBrakes(): Brakes = MinivanBrakes()
   override fun createDecors(): Decors = MinivanDecors()
}

UML Diagram

class-example-abstract-factory.png

Considerations

Abstract Factory helps us avoid tight coupling between concrete products and the client code (the code using these products). We can also ensure the products getting from a factory are compatible with each other. The trade-off with this approach is sometimes it will make the code become more complicated than it should be.

References

Abstract Factory - refactoring.guru

From No Factory to Factory Method