Abstract Factory Pattern in practice
In this post we will learn how to implement an abstract factory pattern. For that purpose we will pretend to be a sandwich bar.
When making a sandwich, bread is only our first and most basic ingredient; we obviously need some kind of filling. An abstract factory is simply a factory that creates other factories. The added layer of abstraction that this requires is amply paid off when we consider how little the top-level control code in our main activity needs to be altered. Being able to modify low-level structures without affecting those preceding constitutes one of the major reasons for applying design patterns, and when applied to complex architectures, this flexibility can save many weeks off development time.
We begin, by creating an interface, one for the bread and one for the filling. They should look like this:
interface Bread {
fun name(): String
fun calories(): String
}
interface Filling {
fun name(): String
fun calories(): String
}
Now lets create concrete examples of these interfaces.
class Baguette: Bread {
override fun name() = "Baguette"
override fun calories() = "80 kcal"
}
class Brioche: Bread {
override fun name() = "Brioche"
override fun calories() = "90 kcal"
}
class Cheese: Filling {
override fun name() = "Cheese"
override fun calories() = "110 kcal"
}
class Tomato: Filling {
override fun name() = "tomato"
override fun calories() = "50 kcal"
}
Next step is to create a class that can call on each type of our future factories.
Recommended by LinkedIn
abstract class AbstractFactory {
abstract fun getBread(bread: String?): Bread?
abstract fun getFilling(filling: String?): Filling?
}
Now create the factories themselves.
class BreadFactory: AbstractFactory() {
override fun getBread(bread: String?): Bread? {
return bread?.let { type ->
if (type == "BAG") return Baguette() else Brioche()
} ?: kotlin.run { null }
}
override fun getFilling(filling: String?): Filling? = null
}
class FillingFactory: AbstractFactory() {
override fun getBread(bread: String?): Bread? = null
override fun getFilling(filling: String?): Filling? {
return filling?.let { type ->
if (type == "CHE") return Cheese() else Tomato()
} ?: kotlin.run { null }
}
}
Finally we create our factory generator in charge of creating our concrete factory.
class FactoryGenerator {
companion object {
fun getFactory(factory: String) : AbstractFactory {
return if (factory == "BRE") BreadFactory()
else FillingFactory()
}
}
}
In our main application we could have the following client code.
fun main() {
val filling = FactoryGenerator.getFactory("FIL").getFilling("CHE")
val bread = FactoryGenerator.getFactory("BRE").getBread("BAG")
println("Here you have your sandwich with
${bread?.name()} / ${bread?.calories()} bread
and ${filling?.name()} / ${filling?.calories()}.")
}
----> Here you have your sandwich with
Baguette / 80 kcal and Cheese / 110 kcal
If now our bar will do sandwiches with many different types of bread and fillings, our application will be able to scale in a simple and easy way by adding new product implementations.