Menu
CoddyTech

工厂模式

本课属于 Coddy Kotlin 学习旅程的“面向对象编程”单元。第 43 课,共 57 课。

工厂模式将创建哪个类的决定集中在一个地方。调用方描述自己的需求,并接收接口类型,而无需指定具体类:

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
    }
}

在 main 中:

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

输出:

sms to ada
null

在 Kotlin 中,工厂通常是接口或基类的伴生对象中的函数,因此调用方可以写成 Shape.from("circle 2")。它可以先解析并检查输入,再创建对象:

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
}

在 main 中:

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

输出:

9.0
null

调用方只会看到接口,因此工厂可以更改返回的类,而不影响调用方。这里,小型数据保留在内存中,大型数据则写入磁盘:

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")

在 main 中:

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

输出:

memory
disk at /tmp/cache

工厂也可以是一个值:类型为 (String) -> Animal 的函数可以创建对象,而调用方无需知道它们的类。从名称到此类函数的映射就是注册表,像 ::Dog 这样的构造函数引用则是一个现成的工厂函数:

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"
}

在 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()}")

输出:

Tom says Meow
challenge icon

挑战

简单

多个导出器实现了提供的接口 Exporter(extension 和 export(title, lines))。编写 MarkdownExporter(扩展名为 md:一行 # Title,然后每行对应一行 - item)、CsvExporter(扩展名为 csv:先输出 title,Title,然后每行对应一行 line,item)和 PlainExporter(扩展名为 txt:标题转换为大写字母,然后原样输出各行)。接着编写工厂对象 ExporterFactory:create(format) 会忽略大小写,并为 markdown、csv 或 plain 返回对应的导出器,否则返回 null;supported() 会返回格式名称。

提供的代码会读取格式、标题和各行内容,并输出 report.md 和导出的文本,或者输出 unsupported format pdf; try markdown, csv, plain。

你的代码应写在 MarkdownExporter.kt、CsvExporter.kt、PlainExporter.kt、ExporterFactory.kt 和 Exporter.kt 中。Main.kt 包含提供的输入/输出代码,不能编辑。

亲手试试

fun main() {
    // 提供的输入输出代码:保持原样。
    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 icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Kotlin 在线编辑器