Menu
Coddy logo textTech

Factory Pattern

Part of the Object Oriented Programming section of Coddy's Kotlin journey. Lesson 43 of 57.

The factory pattern puts the decision about which class to create in one place. The caller describes what it needs and receives the interface type, without naming a concrete class:

interface Notifier {
    fun send(to: String): String
}
class EmailNotifier : Notifier {
    override fun send(to: String) = "email to $to"
}
class SmsNotifier : Notifier {
    override fun send(to: String) = "sms to $to"
}

object NotifierFactory {
    fun create(channel: String): Notifier? = when (channel) {
        "email" -> EmailNotifier()
        "sms" -> SmsNotifier()
        else -> null
    }
}

Inside main:

println(NotifierFactory.create("sms")?.send("ada"))
println(NotifierFactory.create("fax")?.send("bo"))

Output:

sms to ada
null

In Kotlin a factory is often a function in the companion object of the interface or base class, so callers write Shape.from("circle 2"). It can parse and check the input before it creates anything:

interface Shape {
    fun area(): Double

    companion object {
        fun from(text: String): Shape? {
            val p = text.split(" ")
            val n = p.getOrNull(1)?.toDoubleOrNull() ?: return null
            return when (p[0]) {
                "circle" -> Circle(n)
                "square" -> Square(n)
                else -> null
            }
        }
    }
}
class Circle(val r: Double) : Shape {
    override fun area() = 3.14 * r * r
}
class Square(val s: Double) : Shape {
    override fun area() = s * s
}

Inside main:

println(Shape.from("square 3")?.area())
println(Shape.from("circle x")?.area())

Output:

9.0
null

Callers see only the interface, so the factory can change which class it returns without touching them. Here small data stays in memory and large data goes to disk:

interface Storage {
    fun describe(): String
}
class MemoryStorage : Storage {
    override fun describe() = "memory"
}
class DiskStorage(val path: String) : Storage {
    override fun describe() = "disk at $path"
}

fun storageFor(sizeMb: Int): Storage = if (sizeMb < 100) MemoryStorage() else DiskStorage("/tmp/cache")

Inside main:

println(storageFor(5).describe())
println(storageFor(500).describe())

Output:

memory
disk at /tmp/cache

A factory can also be a value: a function of type (String) -> Animal creates objects without the caller knowing their class. A map from names to such functions is a registry, and a constructor reference such as ::Dog is a ready-made factory function:

open class Animal(val name: String) {
    open fun sound() = "..."
}
class Dog(name: String) : Animal(name) {
    override fun sound() = "Woof"
}
class Cat(name: String) : Animal(name) {
    override fun sound() = "Meow"
}

Inside main:

val makers: Map<String, (String) -> Animal> = mapOf("dog" to ::Dog, "cat" to ::Cat)
val pet = makers.getValue("cat")("Tom")
println("${pet.name} says ${pet.sound()}")

Output:

Tom says Meow
challenge icon

Challenge

Easy

Several exporters implement the supplied interface Exporter (extension and export(title, lines)). Write MarkdownExporter (extension md: a # Title line and one - item line per line), CsvExporter (extension csv: title,Title and then line,item per line) and PlainExporter (extension txt: the title in capital letters, then the lines unchanged). Then write the factory object ExporterFactory: create(format) returns the exporter for markdown, csv or plain, ignoring case, or null, and supported() returns the format names.

The supplied code reads a format, a title and the lines, and prints report.md and the exported text, or unsupported format pdf; try markdown, csv, plain.

Your code goes in MarkdownExporter.kt, CsvExporter.kt, PlainExporter.kt, ExporterFactory.kt and Exporter.kt. Main.kt holds the supplied input/output code and cannot be edited.

Try it yourself

fun main() {
    // Supplied input/output code: keep it as it is
    val input = generateSequence(::readLine).toList()
    val exporter = ExporterFactory.create(input[0])
    if (exporter == null) {
        println("unsupported format ${input[0]}; try ${ExporterFactory.supported().joinToString(", ")}")
    } else {
        println("report.${exporter.extension}")
        println(exporter.export(input[1], input.drop(2)))
    }
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming

Practice on your own: Kotlin playground