Gang of Four’s Creational Patterns describe the way of creating and initializing objects.
1. Abstract Factory
Let’s assume we need to build a car using different parts, such as Tire
and Engine
. The data types can be defined like below:
interface Tire {}
class TireA : Tire
class TireB : Tire
interface Engine {}
class EngineA : Engine
class EngineB : Engine
class Car(val tire: Tire, val engine: Engine)
However, a certain type of tire can only be used with a certain type of engine. How should we write a method to build a car, especially if new types of tires and engines will be added in the future?
The abstract factory pattern can be used to solve this problem. This pattern provides a factory interface to create a family of products, which are designed to be used together, without specifying the concrete classes.
We can create a group of factories like this:
interface CarPartsFactory {
fun produceTire(): Tire
fun produceEngine(): Engine
}
class CarPartsFactoryA : CarPartsFactory {
override fun produceTire(): Tire = TireA()
override fun produceEngine(): Engine = EngineA()
}
class CarPartsFactoryB : CarPartsFactory {
override fun produceTire(): Tire = TireB()
override fun produceEngine(): Engine = EngineB()
}
Then we can write the buildCar()
function like below:
fun buildCar(factory: CarPartsFactory): Car
= Car(factory.produceTire(), factory.produceEngine())
In the future, if we have new parts like TireC
and EngineC
, all we need is to create a CarPartsFactoryC
to utilize these new types, without changing anything inside the buildCar()
function.
2. Builder
Let’s continue with the previous example. What if we can build different types of cars using the same car parts? How are we going to extend the buildCar()
function to achieve this?
The builder pattern can be used to solve this problem. It separates the creation of an object from its representation.
We can create car builders like below:
interface CarBuilder {
fun withTire(tire: Tire): CarBuilder
fun withEngine(engine: Engine): CarBuilder
fun build(): Car
}
class CarBuilderA: CarBuilder {
private lateinit var tire: Tire
private lateinit var engine: Engine
override fun withTire(tire: Tire): CarBuilder {
this.tire = tire
return this
}
override fun withEngine(engine: Engine): CarBuilder {
this.engine = engine
return this
}
override fun build(): Car {
return CarA(tire, engine)
}
}
Now we can refactor the buildCar()
function to something like below:
fun buildCar(factory: CarPartsFactory, builder: CarBuilder): Car {
return builder.withTire(factory.produceTire())
.withEngine(factory.produceEngine())
.build()
}
In the future, if we introduce new car types, we just need to create a new CarBuilder
and pass it to the buildCar()
function.
3. Factory Method
Now assume we have a new CarManufacturer
class, and one of the operations needs to build a new car. However, the logic inside the buildCar()
function differs across the manufacturers.
The factory method pattern can be used here, which isolates the logic to create objects in subclasses.
We can define the manufacturers like this:
abstract class CarManufacturer {
abstract fun buildCar(): Car
}
class CarManufacturerA: CarManufacturer() {
override fun buildCar(): Car {
val carPartsFactory: CarPartsFactory = CarPartsFactoryA()
return CarBuilderA()
.withTire(carPartsFactory.produceTire())
.withEngine(carPartsFactory.produceEngine())
.build()
}
}
4. Prototype
Now assume we need to make a copy of a cerated Car
instance. However, the cost of invoking the buildCar()
function to create a new instance is high.
The prototype pattern is provided to create a copy of an instance.
Kotlin provides a copy()
function for data classes. Suppose Car
is a data class, then we can do something like below:
val anotherCar = car.copy()
val anotherCarWithCustomTire = car.copy(tire = TireA())
5. Singleton
Now assume that for a car manufacturer, it has at most one instance of any car parts factory class.
The singleton pattern is to handle such cases, where a class has only one instance.
Kotlin provides an object
keyword to define singletons. So we can change the CarPartsFactoryA
class to something like below:
object CarPartsFactoryA : CarPartsFactory {
override fun produceTire(): Tire = TireA()
override fun produceEngine(): Engine = EngineA()
}
Note that there is no change other than changing the keyword from class
to object
. In the client, we can directly use its name to reference it, e.g.:
val car = buildCar(CarPartsFactoryA, someCarBuilderInstance)