الفرق بين المراجعتين لصفحة: «Design Patterns/factory method»
أسامه-دمراني (نقاش | مساهمات) إدخال 4.0 إضافة صور. |
أسامه-دمراني (نقاش | مساهمات) إدخال 4.1 تنسيق للصور |
||
سطر 3: | سطر 3: | ||
== المشكلة == | == المشكلة == | ||
[[ملف:factory1.png|مركز]] | |||
تخيل أنك تنشئ تطبيقًا لشحن البضائع، ولا تتعامل أول نسخة من ذلك التطبيق إلا مع الشحن البري بالشاحنات (Trucks)، فمن المنطقي حينها أن يكون أغلب شيفرتك البرمجية تحت فئة <code>Trucks</code>، لكن لنفرض أن التطبيق اشتهر بعد مدة وتوسعت الطلبات وبدأت تتلقى طلبات شحن بحري، فإنك ستحتاج عندئذ إلى إضافة فئة جديدة ليكن اسمها <code>Ships</code> مثلًا. | تخيل أنك تنشئ تطبيقًا لشحن البضائع، ولا تتعامل أول نسخة من ذلك التطبيق إلا مع الشحن البري بالشاحنات (Trucks)، فمن المنطقي حينها أن يكون أغلب شيفرتك البرمجية تحت فئة <code>Trucks</code>، لكن لنفرض أن التطبيق اشتهر بعد مدة وتوسعت الطلبات وبدأت تتلقى طلبات شحن بحري، فإنك ستحتاج عندئذ إلى إضافة فئة جديدة ليكن اسمها <code>Ships</code> مثلًا. | ||
لكن هذا الأسلوب سيحدِث تغييرات في كل الشيفرة البرمجية لديك، وإن قررت بعد فترة أخرى إضافة نوع آخر من الشحن إلى التطبيق -الجوي مثلًا- فستخوض هذه العملية كلها مرة ثالثة، وستكون النتيجة شيفرة مزدحمة فوضوية تملؤها العبارات الشرطية التي ستغير سلوك التطبيق بناءً على فئة كائنات الشحن. | لكن هذا الأسلوب سيحدِث تغييرات في كل الشيفرة البرمجية لديك، وإن قررت بعد فترة أخرى إضافة نوع آخر من الشحن إلى التطبيق -الجوي مثلًا- فستخوض هذه العملية كلها مرة ثالثة، وستكون النتيجة شيفرة مزدحمة فوضوية تملؤها العبارات الشرطية التي ستغير سلوك التطبيق بناءً على فئة كائنات الشحن. | ||
== الحل == | == الحل == | ||
[[ملف:solution1.png|مركز]] | |||
يقترح أسلوب المصنع هنا أن تستبدل الاستدعاءات المباشرة لإنشاء الكائنات -من خلال معامِل <code>new</code>- باستدعاءات لأسلوب factory الخاص، ما سيحدث هو أن الكائنات سيتم إنشاؤها بمعامل <code>new</code> كما تقدم لكنها ستُستَدعى من داخل أسلوب المصنع. ويشار إلى تلك الكائنات التي تُعاد (returned) بأسلوب المصنع باسم المنتجات (products). | يقترح أسلوب المصنع هنا أن تستبدل الاستدعاءات المباشرة لإنشاء الكائنات -من خلال معامِل <code>new</code>- باستدعاءات لأسلوب factory الخاص، ما سيحدث هو أن الكائنات سيتم إنشاؤها بمعامل <code>new</code> كما تقدم لكنها ستُستَدعى من داخل أسلوب المصنع. ويشار إلى تلك الكائنات التي تُعاد (returned) بأسلوب المصنع باسم المنتجات (products). | ||
قد يبدو هذا التغيير لا معنى له في البداية، فما زدنا على نقل استدعاء الإنشاء (construction call) من مكان إلى آخر داخل البرنامج، لكن لاحظ الآن أنك تستطيع تخطي أسلوب المصنع داخل فئة فرعية (subclass) وتغير فئة المنتجات التي أنشئت بواسطته. | قد يبدو هذا التغيير لا معنى له في البداية، فما زدنا على نقل استدعاء الإنشاء (construction call) من مكان إلى آخر داخل البرنامج، لكن لاحظ الآن أنك تستطيع تخطي أسلوب المصنع داخل فئة فرعية (subclass) وتغير فئة المنتجات التي أنشئت بواسطته. | ||
غير أن الأمر مقيَّد نوعًا ما، فالفئات الفرعية لن تعيد أنواعًا مختلفة من المنتجات إلا إن كانت تلك المنتجات لديها فئة أو واجهة أساسية مشتركة، كذلك فإن يجب أن يكون نوع الإعادة لأسلوب المصنع في الفئة الأساسية مصرحًا به كالواجهة المشتركة التي ذكرناها. | غير أن الأمر مقيَّد نوعًا ما، فالفئات الفرعية لن تعيد أنواعًا مختلفة من المنتجات إلا إن كانت تلك المنتجات لديها فئة أو واجهة أساسية مشتركة، كذلك فإن يجب أن يكون نوع الإعادة لأسلوب المصنع في الفئة الأساسية مصرحًا به كالواجهة المشتركة التي ذكرناها. | ||
[[ملف:solution2-en.png|مركز]] | |||
[[ملف:solution2-en.png]] | |||
فمثلًا، يجب أن تستخدم الفئتان <code>Truck</code> و <code>Ship</code> واجهة <code>Transport</code> التي تصرح عن أسلوب اسمه <code>deliver</code>، وتنفذ كل فئة هذا الأسلوب بشكل مختلف، فالشاحنات تسلم حمولتها على الأرض، والسفن تسلمها عبر البحار، ويعيد أسلوب المصنع كائنات الشاحنات في فئة <code>RoadLogistics</code>، بينما يعيد سفنًا في فئة <code>SeaLogistics</code>. | فمثلًا، يجب أن تستخدم الفئتان <code>Truck</code> و <code>Ship</code> واجهة <code>Transport</code> التي تصرح عن أسلوب اسمه <code>deliver</code>، وتنفذ كل فئة هذا الأسلوب بشكل مختلف، فالشاحنات تسلم حمولتها على الأرض، والسفن تسلمها عبر البحار، ويعيد أسلوب المصنع كائنات الشاحنات في فئة <code>RoadLogistics</code>، بينما يعيد سفنًا في فئة <code>SeaLogistics</code>. | ||
[[ملف:solution3.png]] | [[ملف:solution3.png]] | ||
ولا ترى الشيفرة البرمجية التي تستخدم أسلوب المصنع -قد يطلق عليها عادة شيفرة العميل (client code)-، لا ترى أي فرق بين المنتجات الحقيقية التي أعيدت من خلال الفئات الفرعية، ويعامل العميل جميع المنتجات كواجهة <code>Transport</code> افتراضية. ويعرف العميل أن كل كائنات النقل يجب أن يكون لديها أسلوب <code>deliver</code>، لكن لا يهم عنده كيفية عملها بالضبط. | ولا ترى الشيفرة البرمجية التي تستخدم أسلوب المصنع -قد يطلق عليها عادة شيفرة العميل (client code)-، لا ترى أي فرق بين المنتجات الحقيقية التي أعيدت من خلال الفئات الفرعية، ويعامل العميل جميع المنتجات كواجهة <code>Transport</code> افتراضية. ويعرف العميل أن كل كائنات النقل يجب أن يكون لديها أسلوب <code>deliver</code>، لكن لا يهم عنده كيفية عملها بالضبط. | ||
مراجعة 11:58، 17 ديسمبر 2018
أسلوب المصنع هو نمط تصميم إنشائي (creational) يوفر واجهة لإنشاء الكائنات (objects) داخل فئات رئيسية (superclasses) لكنها تسمح في نفس الوقت للفئات الثانوية (subclasses) بتغيير نوع تلك الكائنات التي سيتم إنشاؤها.
المشكلة
تخيل أنك تنشئ تطبيقًا لشحن البضائع، ولا تتعامل أول نسخة من ذلك التطبيق إلا مع الشحن البري بالشاحنات (Trucks)، فمن المنطقي حينها أن يكون أغلب شيفرتك البرمجية تحت فئة Trucks
، لكن لنفرض أن التطبيق اشتهر بعد مدة وتوسعت الطلبات وبدأت تتلقى طلبات شحن بحري، فإنك ستحتاج عندئذ إلى إضافة فئة جديدة ليكن اسمها Ships
مثلًا.
لكن هذا الأسلوب سيحدِث تغييرات في كل الشيفرة البرمجية لديك، وإن قررت بعد فترة أخرى إضافة نوع آخر من الشحن إلى التطبيق -الجوي مثلًا- فستخوض هذه العملية كلها مرة ثالثة، وستكون النتيجة شيفرة مزدحمة فوضوية تملؤها العبارات الشرطية التي ستغير سلوك التطبيق بناءً على فئة كائنات الشحن.
الحل
يقترح أسلوب المصنع هنا أن تستبدل الاستدعاءات المباشرة لإنشاء الكائنات -من خلال معامِل new
- باستدعاءات لأسلوب factory الخاص، ما سيحدث هو أن الكائنات سيتم إنشاؤها بمعامل new
كما تقدم لكنها ستُستَدعى من داخل أسلوب المصنع. ويشار إلى تلك الكائنات التي تُعاد (returned) بأسلوب المصنع باسم المنتجات (products).
قد يبدو هذا التغيير لا معنى له في البداية، فما زدنا على نقل استدعاء الإنشاء (construction call) من مكان إلى آخر داخل البرنامج، لكن لاحظ الآن أنك تستطيع تخطي أسلوب المصنع داخل فئة فرعية (subclass) وتغير فئة المنتجات التي أنشئت بواسطته.
غير أن الأمر مقيَّد نوعًا ما، فالفئات الفرعية لن تعيد أنواعًا مختلفة من المنتجات إلا إن كانت تلك المنتجات لديها فئة أو واجهة أساسية مشتركة، كذلك فإن يجب أن يكون نوع الإعادة لأسلوب المصنع في الفئة الأساسية مصرحًا به كالواجهة المشتركة التي ذكرناها.
فمثلًا، يجب أن تستخدم الفئتان Truck
و Ship
واجهة Transport
التي تصرح عن أسلوب اسمه deliver
، وتنفذ كل فئة هذا الأسلوب بشكل مختلف، فالشاحنات تسلم حمولتها على الأرض، والسفن تسلمها عبر البحار، ويعيد أسلوب المصنع كائنات الشاحنات في فئة RoadLogistics
، بينما يعيد سفنًا في فئة SeaLogistics
.
ولا ترى الشيفرة البرمجية التي تستخدم أسلوب المصنع -قد يطلق عليها عادة شيفرة العميل (client code)-، لا ترى أي فرق بين المنتجات الحقيقية التي أعيدت من خلال الفئات الفرعية، ويعامل العميل جميع المنتجات كواجهة Transport
افتراضية. ويعرف العميل أن كل كائنات النقل يجب أن يكون لديها أسلوب deliver
، لكن لا يهم عنده كيفية عملها بالضبط.
البُنية
- يصرّح المنتج بالواجهة التي ستكون مشتركة لكل الكائنات التي يمكن إنتاجها بواسطة المنشئ (creator) وفئاته الفرعية.
- المنتجات الحقيقية (concrete products) هي الاستخدامات المختلفة لواجهة المنتج.
- تصرّح فئة المنشئ عن أسلوب المصنع الذي يعيد كائنات المنتج الجديد، من المهم أن نوع الإعادة من هذا الأسلوب يطابق واجهة المنتج. تستطيع تصريح أسلوب المصنع كأسلوب افتراضي لإجبار كل الفئات الفرعية على استخدام نسخها الخاص من الأسلوب، وكطريقة بديلة فإن أسلوب المصنع الأساسي يمكنه إعادة بعض الأنواع الافتراضية للمنتجات. لاحظ أن الوظيفة الأساسية للمنشئ (creator) ليست إنشاء المنتجات رغم التسمية التي توحي بهذا، فإن فئة المنشئ عادة يكون لها منطق تجاري متعلق بالمنتجات، ويساعد أسلوب المصنع في فصل هذا المنطق عن فئات المنتجات الحقيقية. إليك مثالًا يوضح الأمر، تستطيع شركات البرمجيات الكبيرة توفير أقسام لتدريب المبرمجين، لكن ذلك لا يعني أن الوظيفة الأساسية للشركة ككل هي إنتاج مبرمجين، بل كتابة البرامج هي وظيفتها.
- تتخطى المنشئات الحقيقية (Concrete Creators) أسلوب المصنع الأساسي لذا تعيد نوعًا مختلفًا من المنتجات. لاحظ أن أسلوب المصنع قد لا ينشئ حالات جديدة دومًا، فقد يعيد أحيانًا كائنات موجودة من مصادر مختلفة كالذاكرة المؤقتة (Cache memory) أو حوض الكائنات (object pool)، أو غيرها.
مثال توضيحي
يوضح المثال كيف يمكن استخدام أسلوب المصنع لإنشاء عناصر واجهة لأكثر من منصة دون الحاجة إلى جمع شيفرة العميل (client code) مع فئات واجهات المنتجات الحقيقية.
تستخدم فئة الصندوق الحواري الأساسية عناصر واجهة مختلفة لإخراج نافذتها، وقد تبدو تلك العناصر بشكل مختلف لكل نظام تشغيل، لكنها ستتصرف بشكل ثابت فيهم جميعًا، فالزر الذي يظهر لك في ويندوز هو نفس الزر الذي سيظهر لك في لينكس.
وحين يأتي دور أسلوب المصنع فلن تضطر إلى إعادة كتابة محتوى الصندوق الحواري لكل نظام تشغيل، فإن صرحنا أسلوبَ مصنع ينتج أزرارًا داخل فئة الصندوق الحواري الأساسية، فيمكننا لاحقًا إنشاء فئة صندوق حواري فرعية تعيد أزارًا بأسلوب ويندوز من أسلوب المصنعز وتكتسب الفئة الفرعية عندئد أغلب شيفرة الصندوق الحواري من الفئة الأساسية لكنها ستُخرج أزرارًا بأسلوب ويندوز على الشاشة بسبب أسلوب المصنع.
ولكي يعمل هذا النمط، يجب أن تعمل فئة الصندوق الحواري الأساسية مع أزرار افتراضية: كفئة أساسية أو واجهة تتبعها كل أزرار المنتجات الحقيقية، وبهذه الطريقة تظل شيفرة الصندوق الحواري عاملة بغض النظر عن نوع الأزرار التي تعمل معها.
بالطبع يمكنك تطبيق هذا المنظور على عناصر الواجهة الأخرى كذلك، لكن مع كل أسلوب مصنع تضيفه إلى الصندوق الحواري فإنك تقترب أكثر من نمط المصنع النظري.
// تصرح فئة المنشئ عن أسلوب المصنع الذي يجب أن يعيد أحد الكائنات من فئة المنتج، وتوضح فئات المنشئ الفرعية
// عادة استخدامات لهذا الأسلوب.
class Dialog is
// قد يوفر المنشئ أيضًا بعض الاستخدامات الافتراضية لأسلوب المصنع.
abstract method createButton()
// لاحظ أنه برغم التسمية فإن وظيفة المنشئ الأساسية ليست إنشاء منتجات، لكن له بعض المنطق التجاري
// الذي يعتمد على كائنات المنتجات المعادة بأسلوب المصنع.
// وتستطيع الفئات الفرعية تغيير هذا المنطق التجاري بشكل غير مباشر عن طرق تخطي أسلوب المصنع
// وإعادة نوع مختلف من المنتجات منه.
method renderWindow() is
// استدع أسلوب المصنع لإنشاء كائن منتج.
Button okButton = createButton()
// والآن، استخدم المنتج.
okButton.onClick(closeDialog)
okButton.render()
// المنشئات الحقيقية تتخطى أسلوب المصنع لتغير نوع المنتج.
class WindowsDialog extends Dialog is
method createButton() is
return new WindowsButton()
class WebDialog extends Dialog is
method createButton() is
return new HTMLButton()
// تصرح واجهة المنتج عن العمليات التي يجب أن تستخدمها كل المنتجات الحقيقية.
interface Button is
method render()
method onClick(f)
// توفر المنتجات الحقيقية استخدامات مختلفة لواجهة المنتج.
class WindowsButton implements Button is
method render(a, b) is
// أخرج الزر بأسلوب ويندوز.
method onClick(f) is
// اربط حدث نقرة محلية في نظام التشغيل.
class HTMLButton implements Button is
method render(a, b) is
// Return an HTML representation of a button.
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
dialog.initialize()
dialog.render()
قابلية التطبيق
- استخدم أسلوب المصنع عند عدم معرفة أنواع الكائنات التي ستتعامل شيفرتك معها، وكذلك عند عدم معرفة اعتمادياتها.
يفصل أسلوب المصنع شيفرة إنشاء المنتج عن الشيفرة التي تستخدم المنتج، ومن ثم يكون من السهل زيادة شيفرة إنشاء المنتج بشكل منفصل عن بقية الشيفرة. فمثلًان لإضافة نوع جديد من المنتجات إلى التطبيق، لن تحتاج إلا إلى إنشاء فئة creator فرعية جديدة لتتخطى بها أسلوب المصنع داخلها.
- استخدم أسلوب المصنع حين تريد تزويد مستخدمي مكتبتك أو إطار عملك بطريقة لزيادة عناصره الداخلية.
أسهل طريقة لتوسيع السلوك الافتراضي لمكتبة أو إطار عمل هو الاكتساب أو الوراثة (inheritance)، لكن أنّى لإطار العمل معرفة أن فئتك الفرعية يجب أن تُستخدم بدلًا من العنصر القياسي؟
الحل هو تقليل الشيفرة التي ستنشئ العناصر في إطار العمل إلى أسلوب مصنع وحيد، وتسمح لأي كان أن يتخطى تلك الطريقة إضافة إلى توسعة العنصر ذاته.
لنرى كيفية عمل ذلك، تخيل أنك تكتب تطبيقًا باستخدام إطار عمل مفتوح المصدر لواجهة المستخدم، يجب أن تكون الأزرار في تطبيقك دائرية الحواف، لكن إطار العمل لا يوفر سوى أزرار مربعة. الحل هنا أن توسع فئة Button
بفئة فرعية لتكن RoundButton
، لكنك الآن ستضطر إلى إخبار فئة UIFramework
الرئيسية أن تستخدم فئة الأزرار الفرعية الجديدة بدلًا من الافتراضية.
ولفعل ذلك تنشئ فئة فرعية لتكن UIWithRoundButtons
من فئة إطار العمل الأساسية وتتخطى أسلوب createButton
الخاص بها، ويمكنك أن تجعل فئتك الفرعية تعيد كائنات RoundButton
رغم أن هذا الأسلوب يعيد كائنات Button
في فئته الأساسية. والآن لم يتبق سوى أن تستخدم فئة UIWithRoundButtons
بدلًا من UIFramework
.
- استخدم أسلوب المصنع عند توفير موارد النظام بإعادة استخدام الكائنات الموجودة بدلًا من إعادة إنشائهم في كل مرة.
قد تحدث تلك الحالة حين تتعامل مع كائنات كبيرة وتستهلك الموارد مثل اتصالات قواعد البيانات أو أنظمة الملفات أو موارد الشبكات. إليك ما يجب فعله لإعادة استخدام كائن موجود فعلًا:
- أولًا عليك توفير بعض المساحة لمتابعة كل الكائنات المنشأة.
- عندما يطلب شخص أحد الكائنات فإن البرنامج يبحث عن كائن متاح في حوض الكائنات (object pool)، ثم يعيده إلى شيفرة العميل.
- إن لم تكن هناك كائنات متاحة فيجب أن ينشئ البرنامج واحدًا ويضيفه إلى الحوض..
هذه شيفرة طويلة، ويجب أن تجمعها كلها في مكان واحد لكي لا تلوث البرنامج بشيفرة مكررة.
لعل أنسب مكان يمكن أن توضع فيه تلك الشيفرة هو منشئ (constructor) الفئة الذي نحاول أن نعيد استخدام كائناته، لكن يجب أن يعيد المنشئ كائنات جديدة افتراضيًا، فلا يعيد حالات موجودة فعلًا، لذا تحتاج أن يكون لديك أسلوب عادي قادر على إنشاء كائنات جديدة إضافة إلى إعادة استخدام الكائنات الموجودة، وهذا يشبه كثيرًا أسلوب المصنع.
كيفية الاستخدام
- تأكد أن تتبع كل المنتجات نفس الواجهة، ويجب أن تصرح هذا الواجهة عن الأساليب التي تناسب كل منتج.
- أضف أسلوب مصنع فارغ داخل فئة المنشئ (creator)، يجب أن يطابق نوع الإعادة واجهة المنتج المشتركة.
- ابحث عن كل المراجع إلى منشئات المنتج (product constructors) داخل شيفرة المنشئ (creator)، واستبدل كل واحدة فيهم باستدعاءات إلى أسلوب المصنع في نفس الوقت الذي تستخرج فيه شيفرة إنشاء المنتج (creation code) إلى أسلوب المصنع، قد تحتاج إلى إضافة معامل مؤقت لأسلوب المصنع من أجل التحكم في نوع المنتج المُعاد. وقد تبدو شيفرة أسلوب المصنع في هذه المرحلة قبيحة، وقد يكون فيها معامل
switch
كبير يلتقط أي فئة منتج ليمثّلها، لكننا سنحل هذا قريبًا. - أنشئ سلسلة من فئات creator الفرعية لكل نوع من المنتجات الموجودة في أسلوب المصنع، وتخطى أسلوب المصنع في الفئات الفرعية واستخرج الأجزاء المناسبة من شيفرة البناء (construction code) من الأسلوب الأساسي.
- إن كانت أنواع المنتجات كثيرة وليس من المنطقي إنشاء فئات فرعية لهم جميعًا فيمكنك إعادة استخدام معامل التحكم (control parameter) من الفة الأساسية داخل الفئات الفرعية. فمثلًا، تخيل أن لديك الهرمية التالية من الفئات: فئة
Mail
الأساسية مع بضعة فئات فرعية:AirMail
وGroundMail
، فتكون فئات واجهةTransport
هيPlane
وTruck
وTrain
. وبينما تستخدمAirMail
كائنات منPlane
، فإنGroundMail
ستعمل مع كائناتTruck
وTrain
كليهما. يمكنك إنشاء فئات فرعية جديدة (TrainMail
مثلًا) لمعالجة كلا الحالتين، لكن هناك خيار آخر، يمكن لشيفرة العميل أن تمرر وسيطًا (argument) لأسلوب المصنع الخاص بفئةGroundMail
لاختيار أي المنتجات التي يريد استقبالها. - إن أصبح أسلوب المصنع الأساسي فارغًا بعد كل الاستخراجات فيمكنك تحويله ليكون نظريًا (abstract)، أما إن تبقى شيء ما فيمكنك جعله السلوك الافتراضي للأسلوب.
المزايا والعيوب
المزايا
- تتجنب الربط الوثيق بين منتجات المنشئ (creator products) والمنتجات الحقيقية (Concrete Products).
- مبدأ المسؤولية الواحدة. يمكنك نقل شيفرة إنشاء المنتج إلى مكان واحد في البرنامج، لتسهيل عملية دعم الشيفرة.
- مبدأ المفتوح/المغلق. يمكنك إدخال أنواع جديدة من المنتجات داخل البرنامج دون تعطيل شيفرة العميل الحالية.
العيوب
قد تصبح الشيفرة أكثر تعقيدًا بما أنك تحتاج إلى إضافة فئات فرعية كثيرة لتُستخدم داخل النمط، وأفضل حالة ستكون لديك هي عندما تضيف النمط إلى هيكل قائم بالفعل من فئات المنشئ (creator classes).
العلاقات مع الأنماط الأخرى
- تستخدم تصميمات كثيرة أسلوب المصنع بما أنه أقل تعقيدًا وأكثر قابلية للتخصيص باستخدام الفئات الفرعية، ثم تنتقل إلى أسلوب المصنع النظري أو النموذج الأولي أو الباني (Builder)، حيث أنهم أكثر مرونة لكن أكثر تعقيدًا في المقابل.
- تُبنى فئات المصنع النظري عادة على سلسلة من أساليب المصنع، لكنك تستطيع استخدام النموذج الأولي لتشكيل الأساليب فوق تلك الفئات.
- يمكنك استخدام أسلوب المصنع إلى جانب المكرِّر لتسمح لمجموعات من الفئات الفرعية بإعادة أنواع مختلفة من المكرٍّرات تتوافق مع المجموعات.
- لا يُبنى النموذج الأولي على الاكتساب (inheritance) لذلك ليس لديه عيوبه، لكن من الناحية الأخرى فإنه يتطلب عملية بدء معقدة من الكائن المستنسخ، أما أسلوب المصنع فمبني على الاكتساب لكنه لا يتطلب خطوة بدء.
- أسلوب المصنع هو استثناء من أسلوب القالب (Template Method)، لكنه قد يخدم كمرحلة داخل أسلوب كبير من نوع القالب.
الاستخدام في لغة جافا
المستوى: مبتدئ
الانتشار: واسع الانتشار
أمثلة الاستخدام: يستخدم نمط أسلوب المصنع بشكل واسع في شيفرة جافا، فهو مفيد حين تريد أن تكون شيفرتك على درجة عالية من المرونة. ويوجد النمط في مكتبات جافا التالية:
- ()java.util.Calendar#getInstance
- ()java.util.ResourceBundle#getBundle
- ()java.text.NumberFormat#getInstance
- ()java.nio.charset.Charset#forName
- (java.net.URLStreamHandlerFactory#createURLStreamHandler(String تُعيد كائنات مفردة (Singleton) مختلفة بناءً على البروتوكول.
- ()java.util.EnumSet#of
- ()javax.xml.bind.JAXBContext#createMarshaller وأساليب أخرى مشابهة.
يمكن ملاحظة أساليب المصنع من الأساليب الإنشائية التي تنشئ كائنات من فئات حقيقية (concrete classes) لكنها تعيدهم ككائنات من نوع نظري (abstract type) أو واجهة نظرية (abstract interface).
مثال: إنتاج عناصر واجهة رسومية للمنصات المختلفة
تلعب الأزرار دور المنتجات والصناديق الحوارية تمثل دور المنشئات (creators)، تتطلب الأنواع المختلفة من الصناديق الحوارية أنواعها الخاصة من العناصر، هذا هو السبب الذي ننشئ فئة فرعية لكل نوع من أنواع الصناديق الحوارية ونتخطى أساليب المصنع الخاصة بها.
والآن، سيمثّل كل نوع من أنواع الصناديق الحوارية فئة زر مناسبة، ويعمل الصندوق الأساسي مع المنتجات مستخدمًا واجهتهم المشتركة، لهذا تبقى شيفرتها عاملة بعد كل تلك التغييرات.
الأزرار
buttons/Button.java: واجهة مشتركة للمنتج
package refactoring_guru.factory_method.example.buttons;
/**
* واجهة مشتركة لكل الأزرار.
*/
public interface Button {
void render();
void onClick();
}
buttons/HtmlButton.java: منتج حقيقي
package refactoring_guru.factory_method.example.buttons;
/**
* HTML استخدام الزر في.
*/
public class HtmlButton implements Button {
public void render() {
System.out.println("<button>Test Button</button>");
onClick();
}
public void onClick() {
System.out.println("Click! Button says - 'Hello World!'");
}
}
buttons/WindowsButton.java: منتج حقيقي آخر
package refactoring_guru.factory_method.example.buttons;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
/**
* استخدام للزر في ويندوز.
*/
public class WindowsButton implements Button {
JPanel panel = new JPanel();
JFrame frame = new JFrame();
JButton button;
public void render() {
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JLabel label = new JLabel("Hello World!");
label.setOpaque(true);
label.setBackground(new Color(235, 233, 126));
label.setFont(new Font("Dialog", Font.BOLD, 44));
label.setHorizontalAlignment(SwingConstants.CENTER);
panel.setLayout(new FlowLayout(FlowLayout.CENTER));
frame.getContentPane().add(panel);
panel.add(label);
onClick();
panel.add(button);
frame.setSize(320, 200);
frame.setVisible(true);
onClick();
}
public void onClick() {
button = new JButton("Exit");
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
frame.setVisible(false);
System.exit(0);
}
});
}
}
المصنع
factory/Dialog.java: منشئ الأساس (Base Creator)
package refactoring_guru.factory_method.example.factory;
import refactoring_guru.factory_method.example.buttons.Button;
/**
* فئة المصنع الأساسية، لاحظ أن المصنع هنا مجرد دور للفئة، ينبغي أن يكون له منطق تجاري يحتاج منتجات مختلفة
* لكي يُنشأ.
*/
public abstract class Dialog {
public void renderWindow() {
// ... other code ...
Button okButton = createButton();
okButton.render();
}
/**
* ستتخطى الفئات الفرعية هذا الأسلوب من أجل إنشاء كائنات أزرار محددة
*/
public abstract Button createButton();
}
factory/HtmlDialog.java: منشئ حقيقي
package refactoring_guru.factory_method.example.factory;
import refactoring_guru.factory_method.example.buttons.Button;
import refactoring_guru.factory_method.example.buttons.HtmlButton;
/**
* HTML الحواري أزرار HTML سينتِج صندوق.
*/
public class HtmlDialog extends Dialog {
@Override
public Button createButton() {
return new HtmlButton();
}
}
factory/WindowsDialog.java: منشئ حقيقي آخر
package refactoring_guru.factory_method.example.factory;
import refactoring_guru.factory_method.example.buttons.Button;
import refactoring_guru.factory_method.example.buttons.WindowsButton;
/**
* صندوق ويندوز الحواري سيُنتِج أزرار ويندوز.
*/
public class WindowsDialog extends Dialog {
@Override
public Button createButton() {
return new WindowsButton();
}
}
Demo.java: شيفرة العميل
package refactoring_guru.factory_method.example;
import refactoring_guru.factory_method.example.factory.Dialog;
import refactoring_guru.factory_method.example.factory.HtmlDialog;
import refactoring_guru.factory_method.example.factory.WindowsDialog;
/**
* فئة عينة العرض، كل شيء يُجمَّع هنا.
*/
public class Demo {
private static Dialog dialog;
public static void main(String[] args) {
configure();
runBusinessLogic();
}
/**
* يُختار المصنع الحقيقي عادة بناءً على الإعدادات أو خيارات البيئة.
*/
static void configure() {
if (System.getProperty("os.name").equals("Windows 10")) {
dialog = new WindowsDialog();
} else {
dialog = new HtmlDialog();
}
}
/**
* يجب أن تعمل شيفرة العميل مع المصانع والمنتجات عبر واجهات نظرية،
* فبهذه الطريقة لا يهم أي مصنع تعمل معه ولا نوع المنتج الذي تعيده.
*/
static void runBusinessLogic() {
dialog.renderWindow();
}
}
OutputDemo.txt: نتائج التنفيذ (صندوق HTML)
<button>Test Button</button>
Click! Button says - 'Hello World!'
OutputDemo.png: نتائج التنفيذ (صندوق حواري في ويندوز)
الاستخدام في لغة #C
المستوى: مبتدئ
الانتشار: واسع الانتشار
أمثلة الاستخدام: يستخدم أسلوب المصنع بكثرة في شيفرة #C، وهو مفيد في كتابة شيفرة عالية المرونة.
كما تقدم في شرح الاستخدام في لغة جافا، يمكن ملاحظة أساليب المصنع من الأساليب الإنشائية التي تنشئ كائنات من فئات حقيقية (concrete classes) لكنها تعيدهم ككائنات من نوع نظري (abstract type) أو واجهة نظرية (abstract interface).
مثال: بُنية النمط
يوضح هذاالمثال بنية نمط أسلوب المصنع، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
Program.cs: مثال على البنية
using System;
namespace RefactoringGuru.DesignPatterns.FactoryMethod.Structural
{
abstract class Creator
{
public abstract IProduct FactoryMethod();
public string SomeOperation()
{
var product = FactoryMethod();
var result = "Creator: The same creator's code has just worked with " + product.Operation();
return result;
}
}
class ConcreteCreator1 : Creator
{
public override IProduct FactoryMethod()
{
return new ConcreteProduct1();
}
}
class ConcreteCreator2 : Creator
{
public override IProduct FactoryMethod()
{
return new ConcreteProduct2();
}
}
interface IProduct
{
string Operation();
}
class ConcreteProduct1 : IProduct
{
public string Operation()
{
return "{Result of ConcreteProduct1}";
}
}
class ConcreteProduct2 : IProduct
{
public string Operation()
{
return "{Result of ConcreteProduct2}";
}
}
class Client
{
public void Main()
{
Console.WriteLine("App: Launched with the ConcreteCreator1.");
ClientMethod(new ConcreteCreator1());
Console.WriteLine("");
Console.WriteLine("App: Launched with the ConcreteCreator2.");
ClientMethod(new ConcreteCreator2());
}
public void ClientMethod(Creator creator)
{
// ...
Console.WriteLine("Client: I'm not aware of the creator's class, but it still works.\n"
+ creator.SomeOperation());
// ...
}
}
class Program
{
static void Main(string[] args)
{
new Client().Main();
}
}
}
Output.txt: المخرجات
App: Launched with the ConcreteCreator1.
Client: I'm not aware of the creator's class, but it still works.
Creator: The same creator's code has just worked with {Result of ConcreteProduct1}
App: Launched with the ConcreteCreator2.
Client: I'm not aware of the creator's class, but it still works.
Creator: The same creator's code has just worked with {Result of ConcreteProduct2}
الاستخدام في لغة PHP
المستوى: مبتدئ
الانتشار: واسع الانتشار
أمثلة الاستخدام: يستخدم نمط أسلوب المصنع بكثرة في شيفرة PHP، وهو مفيد في كتابة شيفرة عالية المرونة.
مثال: بُنية النمط
يوضح هذاالمثال بنية نمط أسلوب المصنع، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
بعد تعلم بنية النمط سيكون من السهل استيعاب المثال التالي بناءً على مثال لحالة حقيقية في PHP.
FactoryMethodStructural.php: مثال على البنية
<?php
namespace RefactoringGuru\FactoryMethod\Structural;
/**
* تصرح فئة المنشئ عن أسلوب المصنع الذي يجب أن يعيد كائنًا من فئة أحد المنتجات
* وتوضح الفئة الفرعية للمنشئ في الغالب استخدامات هذا الأسلوب.
*/
abstract class Creator
{
/**
* لاحظ أن المنشئ قد يوضح بعض الاستخدامات الافتراضية لأسلوب المصنع.
*/
public abstract function factoryMethod(): Product;
/**
* لاحظ كذلك أن وظيفة المنشئ الأساسية ليست إنشاء المنتجات، رغم مايوحي به الاسم،
* لكن له عادة بعض المنطق التجاري الذي يعتمد على كائنات المنتج المعادة بواسطة أسلوب المصنع.
* وتستطيع الفئات الفرعية تغيير هذا المنطق التجاري بشكل غير مباشر عبر تخطي أسلوب
* المصنع وإعادة نوع مختلف من المنتجات منه.
*/
public function someOperation(): string
{
// Call the factory method to create a Product object.
$product = $this->factoryMethod();
// Now, use the product.
$result = "Creator: The same creator's code has just worked with ".
$product->operation();
return $result;
}
}
/**
* تتخطى المنشئات الحقيقية أسلوب المصنع من أجل تغيير نوع المنتج.
*/
class ConcreteCreator1 extends Creator
{
/**
* لاحظ أن توقيع الأسلوب لا يزال يستخدم نوع المنتج النظري برغم أن المنتج الحقيقي مُعاد فعلًا من الأسلوب
* بهذه الطريقة يظل المنشئ مستقلًا من فئات المنتج الحقيقي.
*/
public function factoryMethod(): Product
{
return new ConcreteProduct1();
}
}
class ConcreteCreator2 extends Creator
{
public function factoryMethod(): Product
{
return new ConcreteProduct2();
}
}
/**
* تصرح واجهة المنتج عن العمليات التي يجب أن تستخدمها المنتجات الحقيقية.
*/
interface Product
{
public function operation(): string;
}
/**
* توضح المنتجات الحقيقية الاستخدامات المختلفة لواجهة المنتج.
*/
class ConcreteProduct1 implements Product
{
public function operation(): string
{
return "{Result of the ConcreteProduct1}";
}
}
class ConcreteProduct2 implements Product
{
public function operation(): string
{
return "{Result of the ConcreteProduct2}";
}
}
/**
* تعمل شيفرة العميل مع نسخة من منشئ حقيقي ولو من خلال واجهته الأساسية، فطالما أن العميل يظل
* عاملًا مع المنشئ من خلال واجهة أساسية فيمكنك تمرير أي فئة منشئ فرعية إليه
*/
function clientCode(Creator $creator)
{
// ...
print("Client: I'm not aware of the creator's class, but it still works.\n"
.$creator->someOperation());
// ...
}
/**
* يختار التطبيق نوع منشئ بناءً على الإعدادات أو البيئة.
*/
print("App: Launched with the ConcreteCreator1.\n");
clientCode(new ConcreteCreator1());
print("\n\n");
print("App: Launched with the ConcreteCreator2.\n");
clientCode(new ConcreteCreator2());
Output.txt: المخرجات
App: Launched with the ConcreteCreator1.
Client: I'm not aware of the creator's class, but it still works.
Creator: The same creator's code has just worked with {Result of the ConcreteProduct1}
App: Launched with the ConcreteCreator2.
Client: I'm not aware of the creator's class, but it still works.
Creator: The same creator's code has just worked with {Result of the ConcreteProduct2}
مثال: حالة حقيقية
في هذا المثال، يوضح نمط أسلوب المصنع واجهة لإنشاء موصلات شبكة اجتماعية يمكن استخدامها لتشجيل الدخول إلى الشبكة وإنشاء منشورات ومن ثم إمكانية تنفيذ نشاطات أخرى، وكل ذلك دون دمج شيفرة العميل مع فئات محددة للشبكة الاجتماعية.
FactoryMethodRealWorld.php: مثال لحالة حقيقية
<?php
namespace RefactoringGuru\FactoryMethod\RealWorld;
/**
* نمط التصميم: أسلوب المصنع
*
* الهدف: تحديد واجهة لإنشاء الكائنات، مع السماح للفئات الفرعية باختيار الفئة التي تمثلها.
* يسمح أسلوب المصنع كذلك للفئة أن ترجئ التمثيل إلى فئات فرعية.
*
* مثال: يوفر نمط أسلوب المصنع هنا واجهة لإنشاء موصلات شبكة اجتماعية يمكن استخدامها لتسجيل الدخول إلى
* الشبكة وإنشاء منشورات ومن ثَمَّ تنفيذ نشاطات أخرى، دون ربط شيفرة العميل بأي فئات محددة للشبكة.
*/
/**
* (constructor) يصرِّح المنشئ عن أسلوب المصنع الذي يمكن استخدامه كبديل لاستدعاءات المنشئ
* المباشرة للمنتجات.
*
* - قبل: $p = new FacebookConnector()
* - بعد: $p = $this->getSocialNetwork()
*
* الفرعية SocialNetworkPoster يسمح هذا بتغيير نوع المنتج المُنشأ بفئات
*/
abstract class SocialNetworkPoster
{
/**
* أسلوب المصنع الفعلي، لاحظ أنه يعيدالموصل النظري.
* يسمح هذا للفئات الفرعية أن تعيد أي موصلات حقيقية دون خرق ميثاق الفئة الرئيسية.
*/
public abstract function getSocialNetwork(): SocialNetworkConnector;
/**
* حين يُستخدم أسلوب المصنع داخل منطق تجاري لمنشئ، فإن الفئات الفرعية قد تُغيِّر المنطق بشكل غير مباشر
* من خلال إعادة أنواع موصلات مختلفة من أسلوب المصنع.
*/
public function post($content)
{
// Call the factory method to create a Product object...
$network = $this->getSocialNetwork();
// ...then use it as you will.
//
// ...ثم استخدمها كما تشاء.
$network->logIn();
$network->createPost($content);
$network->logout();
}
}
/**
* 'post' هذا المنتج الحقيقي يدعم فيس بوك، تذكر أن هذه الفئة تكتسب أسلوب
* من الفئة الأم. المنشئات الحقيقية هي الفئات التي يستخدمها العميل فعليًا.
*/
class FacebookPoster extends SocialNetworkPoster
{
private $login, $password;
public function __construct($login, $password)
{
$this->login = $login;
$this->password = $password;
}
public function getSocialNetwork(): SocialNetworkConnector
{
return new FacebookConnector($this->login, $this->password);
}
}
/**
* هذا المنتج الحقيقي يدعم شبكة لينكدإن.
*/
class LinkedInPoster extends SocialNetworkPoster
{
private $email, $password;
public function __construct($email, $password)
{
$this->email = $email;
$this->password = $password;
}
public function getSocialNetwork(): SocialNetworkConnector
{
return new LinkedInConnector($this->email, $this->password);
}
}
/**
* تصرح واجهة المنتج عن سلوك الأنواع المختلفة من المنتجات.
*/
interface SocialNetworkConnector
{
public function logIn();
public function logOut();
public function createPost($content);
}
/**
* فيس بوك API هذا المنتج الحقيقي يستخدم.
*/
class FacebookConnector implements SocialNetworkConnector
{
private $login, $password;
public function __construct($login, $password)
{
$this->login = $login;
$this->password = $password;
}
public function logIn()
{
print("Send HTTP API request to log in user $this->login with " .
"password $this->password\n");
}
public function logOut()
{
print("Send HTTP API request to log out user $this->login\n");
}
public function createPost($content)
{
print("Send HTTP API requests to create a post in Facebook timeline.\n");
}
}
/**
* لينكدإن API هذا المنتج الحقيقي يستخدم.
*/
class LinkedInConnector implements SocialNetworkConnector
{
private $email, $password;
public function __construct($email, $password)
{
$this->email = $email;
$this->password = $password;
}
public function logIn()
{
print("Send HTTP API request to log in user $this->email with " .
"password $this->password\n");
}
public function logOut()
{
print("Send HTTP API request to log out user $this->email\n");
}
public function createPost($content)
{
print("Send HTTP API requests to create a post in LinkedIn timeline.\n");
}
}
/**
* SocialNetworkPoster تستطيع شيفرة العميل أن تعمل مع أي فئة فرعية لـ
* بما أنها لا تعتمد على فئات حقيقية.
*/
function clientCode(SocialNetworkPoster $creator)
{
// ...
$creator->post("Hello world!");
$creator->post("I had a large hamburger this morning!");
// ...
}
/**
* أثناء مرحلة البدء، يمكن للتطبيق اختيار الشبكة التي يريد العمل معها، وينشئ كائنًا من الفئة الفرعية
* المناسبة، ويمرره إلى شيفرة العميل.
*/
print("Testing ConcreteCreator1:\n");
clientCode(new FacebookPoster("john_smith", "******"));
print("\n\n");
print("Testing ConcreteCreator2:\n");
clientCode(new LinkedInPoster("john_smith@example.com", "******"));
Output.txt: المخرجات
Testing ConcreteCreator1:
Send HTTP API request to log in user john_smith with password ******
Send HTTP API requests to create a post in Facebook timeline.
Send HTTP API request to log out user john_smith
Send HTTP API request to log in user john_smith with password ******
Send HTTP API requests to create a post in Facebook timeline.
Send HTTP API request to log out user john_smith
Testing ConcreteCreator2:
Send HTTP API request to log in user john_smith@example.com with password ******
Send HTTP API requests to create a post in LinkedIn timeline.
Send HTTP API request to log out user john_smith@example.com
Send HTTP API request to log in user john_smith@example.com with password ******
Send HTTP API requests to create a post in LinkedIn timeline.
Send HTTP API request to log out user john_smith@example.com