Menu
Coddy logo textTech

Factoryパターン

CoddyのKotlinジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 43/57。

ファクトリパターンは、どのクラスを作成するかという判断を1か所に集約します。呼び出し側は必要なものを記述し、具象クラスの名前を指定せずにインターフェース型を受け取ります。

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プレイグラウンド