ConstructiCat Logo
CodeBust.
Browse section ▾

طريقة المصنع.

طريقة المصنع هي نمط تصميم إنشائي يوفّر واجهة لإنشاء الكائنات في الصنف الأعلى (superclass)، لكنه يسمح للأصناف الفرعية بتغيير نوع الكائنات التي سيتم إنشاؤها.

##Intent

طريقة المصنع هي نمط تصميم إنشائي يوفّر واجهة لإنشاء الكائنات في الصنف الأعلى (superclass)، لكنه يسمح للأصناف الفرعية بتغيير نوع الكائنات التي سيتم إنشاؤها.

نمط طريقة المصنع

##Problem

تخيّل أنك تبني تطبيقًا لإدارة الخدمات اللوجستية. لا تستطيع النسخة الأولى من تطبيقك التعامل إلا مع النقل بالشاحنات، لذا فإن الجزء الأكبر من كودك يقع داخل الصنف Truck.

بعد فترة، يصبح تطبيقك مشهورًا للغاية. وتتلقى كل يوم عشرات الطلبات من شركات النقل البحري لدمج الخدمات اللوجستية البحرية في التطبيق.

إضافة صنف نقل جديد إلى البرنامج تسبب مشكلة

إضافة صنف جديد إلى البرنامج ليست بهذه البساطة إذا كانت بقية الكود مرتبطة بالفعل بأصناف موجودة.

أخبار رائعة، أليس كذلك؟ لكن ماذا عن الكود؟ في الوقت الحالي، يرتبط معظم كودك بالصنف Truck. وإضافة Ships إلى التطبيق ستتطلب إجراء تغييرات على قاعدة الكود بأكملها. علاوة على ذلك، إذا قررت لاحقًا إضافة نوع آخر من النقل إلى التطبيق، فستحتاج على الأرجح إلى إجراء كل هذه التغييرات مرة أخرى.

ونتيجة لذلك، سينتهي بك الأمر إلى كود سيئ للغاية، مليء بالشروط التي تبدّل سلوك التطبيق بناءً على صنف كائنات النقل.

##Solution

يقترح نمط طريقة المصنع أن تستبدل استدعاءات إنشاء الكائنات المباشرة (باستخدام العامل new) باستدعاءات لطريقة مصنع خاصة. لا تقلق: لا تزال الكائنات تُنشأ عبر العامل new، لكنه يُستدعى من داخل طريقة المصنع. وكثيرًا ما يُشار إلى الكائنات التي تُرجعها طريقة المصنع باسم المنتجات.

بنية أصناف المُنشئ

يمكن للأصناف الفرعية تغيير صنف الكائنات التي تُرجعها طريقة المصنع.

للوهلة الأولى، قد يبدو هذا التغيير بلا جدوى: لقد نقلنا فقط استدعاء المُنشئ من جزء من البرنامج إلى آخر. غير أنه فكّر في هذا: يمكنك الآن تجاوز طريقة المصنع في صنف فرعي وتغيير صنف المنتجات التي تُنشئها الطريقة.

هناك قيد بسيط رغم ذلك: لا يمكن للأصناف الفرعية إرجاع أنواع مختلفة من المنتجات إلا إذا كانت هذه المنتجات تشترك في صنف أساسي أو واجهة مشتركة. كذلك ينبغي أن يكون نوع الإرجاع لطريقة المصنع في الصنف الأساسي معلَنًا على أنه هذه الواجهة.

بنية التسلسل الهرمي للمنتجات

يجب أن تتبع جميع المنتجات الواجهة نفسها.

على سبيل المثال، ينبغي أن ينفّذ كلا الصنفين Truck وShip الواجهة Transport، التي تُعلن عن طريقة تُدعى deliver. وينفّذ كل صنف هذه الطريقة بشكل مختلف: فالشاحنات توصّل البضائع برًا، والسفن توصّل البضائع بحرًا. وتُرجع طريقة المصنع في الصنف RoadLogistics كائنات شاحنات، بينما تُرجع طريقة المصنع في الصنف SeaLogistics سفنًا.

بنية الكود بعد تطبيق نمط طريقة المصنع

طالما أن جميع أصناف المنتجات تنفّذ واجهة مشتركة، يمكنك تمرير كائناتها إلى كود العميل دون كسره.

الكود الذي يستخدم طريقة المصنع (الذي يُسمى غالبًا كود العميل) لا يرى فرقًا بين المنتجات الفعلية التي تُرجعها مختلف الأصناف الفرعية. فالعميل يتعامل مع جميع المنتجات على أنها Transport مجردة. ويعلم العميل أنه يُفترض في جميع كائنات النقل أن تمتلك طريقة deliver، لكن كيفية عملها بالضبط ليست مهمة بالنسبة إلى العميل.

##Structure

«abstract» Creator ...+someOperation()+createProduct() : Product «interface» Product +doStuff() ConcreteCreatorA ...+createProduct() : Product ConcreteCreatorB ...+createProduct() : Product ConcreteProductA ConcreteProductB Product p = createProduct()p.doStuff() return new ConcreteProductA()
«abstract» Creator ...+someOperation()+createProduct() : Product «interface» Product +doStuff() ConcreteCreatorA ...+createProduct() : Product ConcreteCreatorB ...+createProduct() : Product ConcreteProductA ConcreteProductB Product p = createProduct()p.doStuff() return new ConcreteProductA() 1 2 3 4
  1. يُعلن المنتج عن الواجهة المشتركة بين جميع الكائنات التي يمكن أن ينتجها المُنشئ وأصنافه الفرعية.

  2. المنتجات الملموسة هي تنفيذات مختلفة لواجهة المنتج.

  3. يُعلن صنف المُنشئ عن طريقة المصنع التي تُرجع كائنات منتج جديدة. ومن المهم أن يتطابق نوع الإرجاع لهذه الطريقة مع واجهة المنتج.

    يمكنك إعلان طريقة المصنع على أنها abstract لإجبار جميع الأصناف الفرعية على تنفيذ نسخها الخاصة من الطريقة. وكبديل، يمكن لطريقة المصنع الأساسية أن تُرجع نوع منتج افتراضيًا.

    لاحظ أنه على الرغم من اسمه، فإن إنشاء المنتجات ليس المسؤولية الأساسية للمُنشئ. فعادةً ما يمتلك صنف المُنشئ بالفعل بعض منطق العمل الأساسي المتعلق بالمنتجات. وتساعد طريقة المصنع على فصل هذا المنطق عن أصناف المنتجات الملموسة. وإليك تشبيهًا: قد يكون لدى شركة كبيرة لتطوير البرمجيات قسم تدريب للمبرمجين. ومع ذلك، تظل الوظيفة الأساسية للشركة ككل هي كتابة الكود، لا إنتاج المبرمجين.

  4. تتجاوز المُنشئات الملموسة طريقة المصنع الأساسية بحيث تُرجع نوعًا مختلفًا من المنتج.

    لاحظ أن طريقة المصنع ليست مضطرة إلى إنشاء نسخ جديدة في كل مرة. يمكنها أيضًا إرجاع كائنات موجودة من ذاكرة تخزين مؤقت (cache) أو مجمّع كائنات (object pool) أو مصدر آخر.

##Pseudocode

يوضّح هذا المثال كيف يمكن استخدام طريقة المصنع لإنشاء عناصر واجهة مستخدم متعددة المنصّات دون اقتران كود العميل بأصناف واجهة المستخدم الملموسة.

بنية مثال نمط طريقة المصنع

مثال مربع الحوار متعدد المنصّات.

يستخدم الصنف الأساسي Dialog عناصر واجهة مستخدم مختلفة لعرض نافذته. وقد تبدو هذه العناصر مختلفة قليلًا تحت أنظمة تشغيل مختلفة، لكن ينبغي أن تتصرف بشكل متّسق. فالزر في Windows لا يزال زرًا في Linux.

عندما تدخل طريقة المصنع إلى المشهد، لن تحتاج إلى إعادة كتابة منطق الصنف Dialog لكل نظام تشغيل. فإذا أعلنّا عن طريقة مصنع تُنتج الأزرار داخل الصنف الأساسي Dialog، يمكننا لاحقًا إنشاء صنف فرعي يُرجع أزرارًا بنمط Windows من طريقة المصنع. عندئذٍ يرث الصنف الفرعي معظم الكود من الصنف الأساسي، لكنه بفضل طريقة المصنع يستطيع عرض أزرار بمظهر Windows على الشاشة.

لكي يعمل هذا النمط، يجب أن يتعامل الصنف الأساسي Dialog مع أزرار مجردة: صنف أساسي أو واجهة تتبعها جميع الأزرار الملموسة. بهذه الطريقة يظل الكود داخل Dialog فعّالًا أيًّا كان نوع الأزرار التي يتعامل معها.

بالطبع، يمكنك تطبيق هذا النهج على عناصر واجهة المستخدم الأخرى أيضًا. غير أنك مع كل طريقة مصنع جديدة تضيفها إلى Dialog، تقترب أكثر من نمط المصنع المجرد. لا تقلق، سنتحدث عن هذا النمط لاحقًا.

// يُعلن صنف المُنشئ عن طريقة المصنع التي يجب أن
// تُرجع كائنًا من صنف منتج. عادةً ما توفّر الأصناف
// الفرعية للمُنشئ تنفيذ هذه الطريقة.
class Dialog is
    // قد يوفّر المُنشئ أيضًا تنفيذًا افتراضيًا
    // لطريقة المصنع.
    abstract method createButton():Button

    // لاحظ أنه على الرغم من اسمه، فإن المسؤولية
    // الأساسية للمُنشئ ليست إنشاء المنتجات. فهو عادةً
    // يحتوي على بعض منطق العمل الأساسي الذي يعتمد على
    // كائنات المنتج التي تُرجعها طريقة المصنع. ويمكن
    // للأصناف الفرعية أن تغيّر منطق العمل ذاك بشكل غير
    // مباشر عبر تجاوز طريقة المصنع وإرجاع نوع مختلف من
    // المنتج منها.
    method render() is
        // استدعِ طريقة المصنع لإنشاء كائن منتج.
        Button okButton = createButton()
        // الآن استخدم المنتج.
        okButton.onClick(closeDialog)
        okButton.render()


// تتجاوز المُنشئات الملموسة طريقة المصنع لتغيير
// نوع المنتج الناتج.
class WindowsDialog extends Dialog is
    method createButton():Button is
        return new WindowsButton()

class WebDialog extends Dialog is
    method createButton():Button is
        return new HTMLButton()


// تُعلن واجهة المنتج عن العمليات التي يجب أن
// تنفّذها جميع المنتجات الملموسة.
interface Button is
    method render()
    method onClick(f)

// توفّر المنتجات الملموسة تنفيذات متنوعة
// لواجهة المنتج.
class WindowsButton implements Button is
    method render(a, b) is
        // عرض زر بنمط Windows.
    method onClick(f) is
        // ربط حدث نقر أصلي لنظام التشغيل.

class HTMLButton implements Button is
    method render(a, b) is
        // إرجاع تمثيل HTML للزر.
    method onClick(f) is
        // ربط حدث نقر لمتصفح الويب.


class Application is
    field dialog: Dialog

    // يختار التطبيق نوع المُنشئ بناءً على الإعدادات
    // أو إعدادات البيئة الحالية.
    method initialize() is
        config = readApplicationConfigFile()

        if (config.OS == "Windows") then
            dialog = new WindowsDialog()
        else if (config.OS == "Web") then
            dialog = new WebDialog()
        else
            throw new Exception("Error! Unknown operating system.")

    // يعمل كود العميل مع نسخة من مُنشئ ملموس، وإن
    // كان ذلك عبر واجهته الأساسية. وطالما ظل العميل
    // يتعامل مع المُنشئ عبر الواجهة الأساسية، يمكنك
    // تمرير أي صنف فرعي من المُنشئ إليه.
    method main() is
        this.initialize()
        dialog.render()

##Applicability

استخدم طريقة المصنع عندما لا تعرف مسبقًا الأنواع والاعتماديات الدقيقة للكائنات التي ينبغي أن يتعامل معها الكود الخاص بك.

تفصل طريقة المصنع كود بناء المنتج عن الكود الذي يستخدم المنتج فعليًا. لذلك يصبح من الأسهل توسيع كود بناء المنتج بشكل مستقل عن بقية الكود.

على سبيل المثال، لإضافة نوع منتج جديد إلى التطبيق، ستحتاج فقط إلى إنشاء صنف فرعي جديد من المُنشئ (creator) وتجاوز (override) طريقة المصنع فيه.

استخدم طريقة المصنع عندما تريد أن توفر لمستخدمي مكتبتك أو إطار العمل الخاص بك وسيلة لتوسيع مكوناته الداخلية.

ربما تكون الوراثة هي أسهل طريقة لتوسيع السلوك الافتراضي لمكتبة أو إطار عمل. لكن كيف سيتعرّف إطار العمل على أن صنفك الفرعي هو الذي ينبغي استخدامه بدلًا من المكوّن القياسي؟

الحل هو اختزال الكود الذي يبني المكونات عبر إطار العمل في طريقة مصنع واحدة، والسماح لأي شخص بتجاوز هذه الطريقة بالإضافة إلى توسيع المكوّن نفسه.

لنرَ كيف يعمل ذلك. تخيّل أنك تكتب تطبيقًا باستخدام إطار عمل واجهة مستخدم مفتوح المصدر. يجب أن يحتوي تطبيقك على أزرار دائرية، لكن إطار العمل لا يوفّر سوى أزرار مربعة. تقوم بتوسيع الصنف القياسي Button بصنف فرعي رائع هو RoundButton. لكنك الآن بحاجة إلى إخبار الصنف الرئيسي UIFramework باستخدام صنف الزر الفرعي الجديد بدلًا من الصنف الافتراضي.

لتحقيق ذلك، تنشئ صنفًا فرعيًا UIWithRoundButtons من صنف أساسي في إطار العمل وتتجاوز طريقته createButton. وبينما تُرجع هذه الطريقة كائنات Button في الصنف الأساسي، تجعل صنفك الفرعي يُرجع كائنات RoundButton. والآن استخدم الصنف UIWithRoundButtons بدلًا من UIFramework. وهذا كل ما في الأمر!

استخدم طريقة المصنع عندما تريد توفير موارد النظام عبر إعادة استخدام الكائنات الموجودة بدلًا من إعادة بنائها في كل مرة.

كثيرًا ما تواجه هذه الحاجة عند التعامل مع كائنات كبيرة وكثيفة الاستهلاك للموارد مثل اتصالات قواعد البيانات وأنظمة الملفات وموارد الشبكة.

لنفكّر فيما يجب فعله لإعادة استخدام كائن موجود:

  1. أولًا، تحتاج إلى إنشاء مساحة تخزين لتتبّع جميع الكائنات التي تم إنشاؤها.
  2. عندما يطلب شخص ما كائنًا، ينبغي أن يبحث البرنامج عن كائن متاح داخل ذلك المجمّع (pool).
  3. … ثم يُعيده إلى كود العميل.
  4. إذا لم تكن هناك كائنات متاحة، ينبغي أن ينشئ البرنامج كائنًا جديدًا (ويضيفه إلى المجمّع).

هذا قدر كبير من الكود! ويجب وضعه كله في مكان واحد حتى لا تلوّث البرنامج بكود مكرر.

ربما يكون المكان الأكثر وضوحًا وملاءمة لوضع هذا الكود هو مُنشئ (constructor) الصنف الذي نحاول إعادة استخدام كائناته. غير أن المُنشئ يجب أن يُرجع دائمًا كائنات جديدة بحكم تعريفه. ولا يمكنه إرجاع نسخ موجودة مسبقًا.

لذلك، تحتاج إلى امتلاك طريقة عادية قادرة على إنشاء كائنات جديدة وكذلك إعادة استخدام الموجودة منها. وهذا يبدو إلى حد بعيد كأنه طريقة مصنع.

##How to Implement

  1. اجعل جميع المنتجات تتبع الواجهة نفسها. ينبغي أن تُعلن هذه الواجهة عن طرق منطقية في كل منتج.

  2. أضف طريقة مصنع فارغة داخل صنف المُنشئ. ينبغي أن يتطابق نوع الإرجاع للطريقة مع واجهة المنتج المشتركة.

  3. في كود المُنشئ، ابحث عن جميع الإشارات إلى مُنشئات المنتجات. استبدلها واحدًا تلو الآخر باستدعاءات لطريقة المصنع، مع استخراج كود إنشاء المنتج إلى طريقة المصنع.

    قد تحتاج إلى إضافة معامل مؤقت إلى طريقة المصنع للتحكم في نوع المنتج المُرجَع.

    في هذه المرحلة، قد يبدو كود طريقة المصنع قبيحًا للغاية. فقد يحتوي على جملة switch كبيرة تختار صنف المنتج الذي سيُنشأ. لكن لا تقلق، سنُصلح ذلك قريبًا.

  4. الآن، أنشئ مجموعة من الأصناف الفرعية للمُنشئ لكل نوع من المنتجات المدرجة في طريقة المصنع. تجاوز طريقة المصنع في الأصناف الفرعية واستخرج الأجزاء المناسبة من كود البناء من الطريقة الأساسية.

  5. إذا كان هناك عدد كبير جدًا من أنواع المنتجات ولم يكن من المنطقي إنشاء أصناف فرعية لها جميعًا، يمكنك إعادة استخدام معامل التحكم من الصنف الأساسي في الأصناف الفرعية.

    على سبيل المثال، تخيّل أن لديك التسلسل الهرمي التالي من الأصناف: الصنف الأساسي Mail مع صنفين فرعيين: AirMail وGroundMail؛ وأصناف Transport هي Plane وTruck وTrain. وبينما يستخدم الصنف AirMail كائنات Plane فقط، قد يعمل GroundMail مع كائنات Truck وTrain معًا. يمكنك إنشاء صنف فرعي جديد (لِنَقُل TrainMail) للتعامل مع الحالتين، لكن هناك خيار آخر. يمكن لكود العميل تمرير وسيط إلى طريقة المصنع في الصنف GroundMail للتحكم في المنتج الذي يريد الحصول عليه.

  6. إذا أصبحت طريقة المصنع الأساسية فارغة بعد كل عمليات الاستخراج، يمكنك جعلها مجردة. وإذا بقي شيء فيها، يمكنك جعله سلوكًا افتراضيًا للطريقة.

##Pros & Cons

Pros
  • تتجنّب الاقتران الوثيق بين المُنشئ والمنتجات الملموسة.
  • مبدأ المسؤولية الواحدة. يمكنك نقل كود إنشاء المنتج إلى مكان واحد في البرنامج، مما يجعل الكود أسهل في الصيانة.
  • مبدأ الفتح/الإغلاق. يمكنك إدخال أنواع جديدة من المنتجات إلى البرنامج دون كسر كود العميل الموجود.
Cons
  • قد يصبح الكود أكثر تعقيدًا لأنك تحتاج إلى إدخال الكثير من الأصناف الفرعية الجديدة لتطبيق النمط. وأفضل سيناريو هو عندما تُدخِل النمط إلى تسلسل هرمي قائم بالفعل من أصناف المُنشئ (creator).

##Relations with Other Patterns

##Extra Content

  • اقرأ مقارنة المصانع إذا لم تستطع التمييز بين مختلف أنماط المصانع ومفاهيمها.