Menu
Coddy logo textTech
flag Ar iconالعربيةdown icon

نمط Factory

جزء من قسم البرمجة كائنية التوجه في رحلة Kotlin على Coddy. الدرس 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، تكون factory غالبًا دالةً في الكائن المرافق للواجهة أو للفئة الأساسية، لذا يكتب المستدعون 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اختبر نفسك

يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.

جميع دروس البرمجة كائنية التوجه

تدرّب بنفسك: Playground لـ Kotlin