الفرق بين المراجعتين لصفحة: «Design Patterns/factory method»

من موسوعة حسوب
طلا ملخص تعديل
إدخال 2.2 محتوى الصفحة
سطر 3: سطر 3:


== المشكلة ==
== المشكلة ==
<!-- ضع الصورة -->
تخيل أنك تنشئ تطبيقًا لشحن البضائع، ولا تتعامل أول نسخة من ذلك التطبيق إلا مع الشحن البري بالشاحنات (Trucks)، فمن المنطقي حينها أن يكون أغلب شيفرتك البرمجية تحت فئة Trucks، لكن لنفرض أن التطبيق اشتهر بعد مدة وتوسعت الطلبات وبدأت تتلقى طلبات شحن بحري، فإنك ستحتاج عندئذ إلى إضافة فئة جديدة ليكن اسمها Ships مثلًا.
تخيل أنك تنشئ تطبيقًا لشحن البضائع، ولا تتعامل أول نسخة من ذلك التطبيق إلا مع الشحن البري بالشاحنات (Trucks)، فمن المنطقي حينها أن يكون أغلب شيفرتك البرمجية تحت فئة Trucks، لكن لنفرض أن التطبيق اشتهر بعد مدة وتوسعت الطلبات وبدأت تتلقى طلبات شحن بحري، فإنك ستحتاج عندئذ إلى إضافة فئة جديدة ليكن اسمها Ships مثلًا.


سطر 8: سطر 9:


== الحل ==
== الحل ==
يقترح أسلوب المصنع هنا أن تستبدل الاستدعاءات المباشرة لإنشاء الكائنات -من خلال معامِل new- باستدعاءات لأسلوب factory الخاص، ما سيحدث هو أن الكائنات سيتم إنشاؤها بمعامل new كما تقدم لكنها ستُستَدعى من داخل أسلوب المصنع. ويشار إلى تلك الكائنات التي تُعاد (returned) بأسلوب المصنع باسم المنتجات (products).
يقترح أسلوب المصنع هنا أن تستبدل الاستدعاءات المباشرة لإنشاء الكائنات -من خلال معامِل new- باستدعاءات لأسلوب factory الخاص، ما سيحدث هو أن الكائنات سيتم إنشاؤها بمعامل new كما تقدم لكنها ستُستَدعى من داخل أسلوب المصنع. ويشار إلى تلك الكائنات التي تُعاد (returned) بأسلوب المصنع باسم المنتجات (products).<!-- ضع الصورة
-->


قد يبدو هذا التغيير لا معنى له في البداية، فما زدنا على نقل استدعاء الإنشاء (construction call) من مكان إلى آخر داخل البرنامج، لكن لاحظ الآن أنك تستطيع تخطي أسلوب المصنع داخل فئة فرعية (subclass) وتغير فئة المنتجات التي أنشئت بواسطته.
قد يبدو هذا التغيير لا معنى له في البداية، فما زدنا على نقل استدعاء الإنشاء (construction call) من مكان إلى آخر داخل البرنامج، لكن لاحظ الآن أنك تستطيع تخطي أسلوب المصنع داخل فئة فرعية (subclass) وتغير فئة المنتجات التي أنشئت بواسطته.
سطر 14: سطر 16:
غير أن الأمر مقيَّد نوعًا ما، فالفئات الفرعية لن تعيد أنواعًا مختلفة من المنتجات إلا إن كانت تلك المنتجات لديها فئة أو واجهة أساسية مشتركة، كذلك فإن يجب أن يكون نوع الإعادة لأسلوب المصنع في الفئة الأساسية مصرحًا به كالواجهة المشتركة التي ذكرناها.
غير أن الأمر مقيَّد نوعًا ما، فالفئات الفرعية لن تعيد أنواعًا مختلفة من المنتجات إلا إن كانت تلك المنتجات لديها فئة أو واجهة أساسية مشتركة، كذلك فإن يجب أن يكون نوع الإعادة لأسلوب المصنع في الفئة الأساسية مصرحًا به كالواجهة المشتركة التي ذكرناها.


<!-- ضع الصورة -->
فمثلًا، يجب أن تستخدم الفئتان Truck و Ship واجهة Transport التي تصرح عن أسلوب اسمه deliver، وتنفذ كل فئة هذا الأسلوب بشكل مختلف، فالشاحنات تسلم حمولتها على الأرض، والسفن تسلمها عبر البحار، ويعيد أسلوب المصنع كائنات الشاحنات في فئة RoadLogistics، بينما يعيد سفنًا في فئة SeaLogistics.
فمثلًا، يجب أن تستخدم الفئتان Truck و Ship واجهة Transport التي تصرح عن أسلوب اسمه deliver، وتنفذ كل فئة هذا الأسلوب بشكل مختلف، فالشاحنات تسلم حمولتها على الأرض، والسفن تسلمها عبر البحار، ويعيد أسلوب المصنع كائنات الشاحنات في فئة RoadLogistics، بينما يعيد سفنًا في فئة SeaLogistics.


<!-- ضع الصورة -->
ولا ترى الشيفرة البرمجية التي تستخدم أسلوب المصنع -قد يطلق عليها عادة شيفرة العميل (client code)-، لا ترى أي فرق بين المنتجات الحقيقية التي أعيدت من خلال الفئات الفرعية، ويعامل العميل جميع المنتجات كواجهة Transport افتراضية. ويعرف العميل أن كل كائنات النقل يجب أن يكون لديها أسلوب deliver، لكن لا يهم عنده كيفية عملها بالضبط.
ولا ترى الشيفرة البرمجية التي تستخدم أسلوب المصنع -قد يطلق عليها عادة شيفرة العميل (client code)-، لا ترى أي فرق بين المنتجات الحقيقية التي أعيدت من خلال الفئات الفرعية، ويعامل العميل جميع المنتجات كواجهة Transport افتراضية. ويعرف العميل أن كل كائنات النقل يجب أن يكون لديها أسلوب deliver، لكن لا يهم عنده كيفية عملها بالضبط.


== البُنية ==
== البُنية ==
ضع الصورة
<!-- ضع الصورة -->
# يصرّح المنتج بالواجهة التي ستكون مشتركة لكل الكائنات التي يمكن إنتاجها بواسطة المنشئ (creator) وفئاته الفرعية.
# يصرّح المنتج بالواجهة التي ستكون مشتركة لكل الكائنات التي يمكن إنتاجها بواسطة المنشئ (creator) وفئاته الفرعية.
# المنتجات الحقيقية (concrete products) هي الاستخدامات المختلفة لواجهة المنتج.
# المنتجات الحقيقية (concrete products) هي الاستخدامات المختلفة لواجهة المنتج.
سطر 26: سطر 30:


== مثال توضيحي ==
== مثال توضيحي ==
يوضح المثال كيف يمكن استخدام أسلوب المصنع لإنشاء عناصر واجهة لأكثر من منصة دون الحاجة إلى جمع شيفرة العميل (client code) مع فئات واجهات المنتجات الحقيقية. تستخدم فئة الصندوق الحواري الأساسية عناصر واجهة مختلفة لإخراج نافذتها، وقد تبدو تلك العناصر بشكل مختلف لكل نظام تشغيل، لكنها ستتصرف بشكل ثابت فيهم جميعًا، فالزر الذي يظهر لك في ويندوز هو نفس الزر الذي سيظهر لك في لينكس.
يوضح المثال كيف يمكن استخدام أسلوب المصنع لإنشاء عناصر واجهة لأكثر من منصة دون الحاجة إلى جمع شيفرة العميل (client code) مع فئات واجهات المنتجات الحقيقية. <!-- ضع الصورة -->
 
تستخدم فئة الصندوق الحواري الأساسية عناصر واجهة مختلفة لإخراج نافذتها، وقد تبدو تلك العناصر بشكل مختلف لكل نظام تشغيل، لكنها ستتصرف بشكل ثابت فيهم جميعًا، فالزر الذي يظهر لك في ويندوز هو نفس الزر الذي سيظهر لك في لينكس.


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


بالطبع يمكنك تطبيق هذا المنظور على عناصر الواجهة الأخرى كذلك، لكن مع كل أسلوب مصنع تضيفه إلى الصندوق الحواري فإنك تقترب أكثر من نمط [[Design Patterns/abstract factory|المصنع الافتراضي]].<syntaxhighlight lang="java">
بالطبع يمكنك تطبيق هذا المنظور على عناصر الواجهة الأخرى كذلك، لكن مع كل أسلوب مصنع تضيفه إلى الصندوق الحواري فإنك تقترب أكثر من نمط [[Design Patterns/abstract factory|المصنع الافتراضي]].<syntaxhighlight lang="java">
// The creator class declares the factory method that must
// تصرح فئة المنشئ عن أسلوب المصنع الذي يجب أن يعيد أحد الكائنات من فئة المنتج، وتوضح فئات المنشئ الفرعية
// return an object of a product class. The creator's subclasses
// عادة استخدامات لهذا الأسلوب.
// usually provide the implementation of this method.
class Dialog is
class Dialog is
     // The creator may also provide some default implementation
     // قد يوفر المنشئ أيضًا بعض الاستخدامات الافتراضية لأسلوب المصنع.
    // of the factory method.
     abstract method createButton()
     abstract method createButton()


     // Note that, despite its name, the creator's primary
     // لاحظ أنه برغم التسمية فإن وظيفة المنشئ الأساسية ليست إنشاء منتجات، لكن له بعض المنطق التجاري
     // responsibility isn't creating products. It usually
     // الذي يعتمد على كائنات المنتجات المعادة بأسلوب المصنع.
     // contains some core business logic that relies on product
     // وتستطيع الفئات الفرعية تغيير هذا المنطق التجاري بشكل غير مباشر عن طرق تخطي أسلوب المصنع
     // objects returned by the factory method. Subclasses can
     // وإعادة نوع مختلف من المنتجات منه.
    // indirectly change that business logic by overriding the
    // factory method and returning a different type of product
    // from it.
     method renderWindow() is
     method renderWindow() is
         // Call the factory method to create a product object.
         // استدع أسلوب المصنع لإنشاء كائن منتج.
         Button okButton = createButton()
         Button okButton = createButton()
         // Now use the product.
         // والآن، استخدم المنتج.
         okButton.onClick(closeDialog)
         okButton.onClick(closeDialog)
         okButton.render()
         okButton.render()




// Concrete creators override the factory method to change the
// المنشئات الحقيقية تتخطى أسلوب المصنع لتغير نوع المنتج.
// resulting product's type.
class WindowsDialog extends Dialog is
class WindowsDialog extends Dialog is
     method createButton() is
     method createButton() is
سطر 67: سطر 67:




// The product interface declares the operations that all
// تصرح واجهة المنتج عن العمليات التي يجب أن تستخدمها كل المنتجات الحقيقية.
// concrete products must implement.
interface Button is
interface Button is
     method render()
     method render()
     method onClick(f)
     method onClick(f)


// Concrete products provide various implementations of the
// توفر المنتجات الحقيقية استخدامات مختلفة لواجهة المنتج.
// product interface.
class WindowsButton implements Button is
class WindowsButton implements Button is
     method render(a, b) is
     method render(a, b) is
         // Render a button in Windows style.
         // أخرج الزر بأسلوب ويندوز.
     method onClick(f) is
     method onClick(f) is
         // Bind a native OS click event.
         // اربط حدث نقرة محلية في نظام التشغيل.


class HTMLButton implements Button is
class HTMLButton implements Button is
سطر 85: سطر 83:
         // Return an HTML representation of a button.
         // Return an HTML representation of a button.
     method onClick(f) is
     method onClick(f) is
         // Bind a web browser click event.
         // اربط حدث نقرة في المتصفح.




سطر 91: سطر 89:
     field dialog: Dialog
     field dialog: Dialog


     // The application picks a creator's type depending on the
     // يختار التطبيق نوع المنشئ بناءً على الإعدادات الحالية أو إعدادات البيئة.
    // current configuration or environment settings.
     method initialize() is
     method initialize() is
         config = readApplicationConfigFile()
         config = readApplicationConfigFile()
سطر 103: سطر 100:
             throw new Exception("Error! Unknown operating system.")
             throw new Exception("Error! Unknown operating system.")


     // The client code works with an instance of a concrete
     // تعمل الشيفرة الحالية للعميل مع نسخة من منشئ حقيقي، ولو من خلال واجهته الأساسية، فطالما أن العميل
     // creator, albeit through its base interface. As long as
     // يظل عاملًا مع المنشئ من خلال واجهة أساسيةن فيمكنك تمرير أي فئة منشئ فرعية إليه.
    // the client keeps working with the creator via the base
    // interface, you can pass it any creator's subclass.
     method main() is
     method main() is
         dialog.initialize()
         dialog.initialize()
سطر 170: سطر 165:
== الاستخدام في لغة جافا ==
== الاستخدام في لغة جافا ==
* التعقيد: مبتدئ
* التعقيد: مبتدئ
* الانتشار:
* الانتشار: واسع
<span> </span>
أمثلة الاستخدام: يستخدم نمط أسلوب المصنع بشكل واسع في شيفرة جافا، فهو مفيد حين تريد أن تكون شيفرتك على درجة عالية من المرونة. ويوجد النمط في مكتبات جافا التالية:
 
[http://docs.oracle.com/javase/8/docs/api/java/util/Calendar.html#getInstance-- ()java.util.Calendar#getInstance]
 
[http://docs.oracle.com/javase/8/docs/api/java/util/ResourceBundle.html#getBundle-java.lang.String- ()java.util.ResourceBundle#getBundle]
 
[http://docs.oracle.com/javase/8/docs/api/java/text/NumberFormat.html#getInstance-- ()java.text.NumberFormat#getInstance]
 
[http://docs.oracle.com/javase/8/docs/api/java/nio/charset/Charset.html#forName-java.lang.String- ()java.nio.charset.Charset#forName]
 
[http://docs.oracle.com/javase/8/docs/api/java/net/URLStreamHandlerFactory.html (java.net.URLStreamHandlerFactory#createURLStreamHandler(String] تُعيد كائنات مفردة (Singleton) مختلفة بناءً على البروتوكول.
 
[https://docs.oracle.com/javase/8/docs/api/java/util/EnumSet.html#of(E) ()java.util.EnumSet#of]
 
[https://docs.oracle.com/javase/8/docs/api/javax/xml/bind/JAXBContext.html#createMarshaller-- ()javax.xml.bind.JAXBContext#createMarshaller] وأساليب أخرى مشابهة.
 
يمكن ملاحظة أساليب المصنع من الأساليب الإنشائية التي تنشئ كائنات من فئات حقيقية (concrete classes) لكنها تعيدهم ككائنات من نوع نظري (abstract type) أو واجهة نظرية (abstract interface).
 
== مثال: إنتاج عناصر واجهة رسومية للمنصات المختلفة ==
تلعب الأزرار دور المنتجات والصناديق الحوارية تمثل دور المنشئات (creators)، تتطلب الأنواع المختلفة من الصناديق الحوارية أنواعها الخاصة من العناصر، هذا هو السبب الذي ننشئ فئة فرعية لكل نوع من أنواع الصناديق الحوارية ونتخطى أساليب المصنع الخاصة بها.
 
والآن، سيمثّل كل نوع من أنواع الصناديق الحوارية فئة زر مناسبة، ويعمل الصندوق الأساسي مع المنتجات مستخدمًا واجهتهم المشتركة، لهذا تبقى شيفرتها عاملة بعد كل تلك التغييرات.
 
=== الأزرار ===
 
==== buttons/Button.java: واجهة مشتركة للمنتج ====
<syntaxhighlight lang="java">
package refactoring_guru.factory_method.example.buttons;
 
/**
* واجهة مشتركة لكل الأزرار.
*/
public interface Button {
    void render();
    void onClick();
}
</syntaxhighlight>
 
==== buttons/HtmlButton.java: منتج حقيقي ====
<syntaxhighlight lang="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!'");
    }
}
</syntaxhighlight>
 
==== buttons/WindowsButton.java: منتج حقيقي آخر ====
<syntaxhighlight lang="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);
            }
        });
    }
}
</syntaxhighlight>
 
=== المصنع ===
 
==== factory/Dialog.java: منشئ الأساس (Base Creator) ====
<syntaxhighlight lang="java">
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();
}
</syntaxhighlight>
 
==== factory/HtmlDialog.java: منشئ حقيقي ====
<syntaxhighlight lang="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();
    }
}
</syntaxhighlight>
 
==== factory/WindowsDialog.java: منشئ حقيقي آخر ====
<syntaxhighlight lang="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();
    }
}
</syntaxhighlight>
 
==== Demo.java: شيفرة العميل ====
<syntaxhighlight lang="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();
    }
}
</syntaxhighlight>
 
==== OutputDemo.txt: نتائج التنفيذ (صندوق HTML) ====
<syntaxhighlight lang="html">
<button>Test Button</button>
Click! Button says - 'Hello World!'
</syntaxhighlight>
 
==== OutputDemo.png: نتائج التنفيذ (صندوق حواري في ويندوز) ====
'''ضع الصورة'''

مراجعة 12:23، 15 ديسمبر 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، لكن لا يهم عنده كيفية عملها بالضبط.

البُنية

  1. يصرّح المنتج بالواجهة التي ستكون مشتركة لكل الكائنات التي يمكن إنتاجها بواسطة المنشئ (creator) وفئاته الفرعية.
  2. المنتجات الحقيقية (concrete products) هي الاستخدامات المختلفة لواجهة المنتج.
  3. تصرّح فئة المنشئ عن أسلوب المصنع الذي يعيد كائنات المنتج الجديد، من المهم أن نوع الإعادة من هذا الأسلوب يطابق واجهة المنتج. تستطيع تصريح أسلوب المصنع كأسلوب افتراضي لإجبار كل الفئات الفرعية على استخدام نسخها الخاص من الأسلوب، وكطريقة بديلة فإن أسلوب المصنع الأساسي يمكنه إعادة بعض الأنواع الافتراضية للمنتجات. لاحظ أن الوظيفة الأساسية للمنشئ (creator) ليست إنشاء المنتجات رغم التسمية التي توحي بهذا، فإن فئة المنشئ عادة يكون لها منطق تجاري متعلق بالمنتجات، ويساعد أسلوب المصنع في فصل هذا المنطق عن فئات المنتجات الحقيقية. إليك مثالًا يوضح الأمر، تستطيع شركات البرمجيات الكبيرة توفير أقسام لتدريب المبرمجين، لكن ذلك لا يعني أن الوظيفة الأساسية للشركة ككل هي إنتاج مبرمجين، بل كتابة البرامج هي وظيفتها.
  4. تتخطى منشئات المنتجات الحقيقية (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.

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

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

  1. أولًا عليك توفير بعض المساحة لمتابعة كل الكائنات المنشأة.
  2. عندما يطلب شخص أحد الكائنات فإن البرنامج يبحث عن كائن متاح في حوض الكائنات (object pool)، ثم يعيده إلى شيفرة العميل.
  3. إن لم تكن هناك كائنات متاحة فيجب أن ينشئ البرنامج واحدًا ويضيفه إلى الحوض..

هذه شيفرة طويلة، ويجب أن تجمعها كلها في مكان واحد لكي لا تلوث البرنامج بشيفرة مكررة.

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

كيفية الاستخدام

  1. تأكد أن تتبع كل المنتجات نفس الواجهة، ويجب أن تصرح هذا الواجهة عن الأساليب التي تناسب كل منتج.
  2. أضف أسلوب مصنع فارغ داخل فئة المنشئ (creator)، يجب أن يطابق نوع الإعادة واجهة المنتج المشتركة.
  3. ابحث عن كل المراجع إلى منشئات المنتج (product constructors) داخل شيفرة المنشئ (creator)، واستبدل كل واحدة فيهم باستدعاءات إلى أسلوب المصنع في نفس الوقت الذي تستخرج فيه شيفرة إنشاء المنتج (creation code) إلى أسلوب المصنع، قد تحتاج إلى إضافة معامل مؤقت لأسلوب المصنع من أجل التحكم في نوع المنتج المُعاد. وقد تبدو شيفرة أسلوب المصنع في هذه المرحلة قبيحة، وقد يكون فيها معامل switch كبير يلتقط أي فئة منتج ليمثّلها، لكننا سنحل هذا قريبًا.
  4. أنشئ سلسلة من فئات creator الفرعية لكل نوع من المنتجات الموجودة في أسلوب المصنع، وتخطى أسلوب المصنع في الفئات الفرعية واستخرج الأجزاء المناسبة من شيفرة البناء (construction code) من الأسلوب الأساسي.
  5. إن كانت أنواع المنتجات كثيرة وليس من المنطقي إنشاء فئات فرعية لهم جميعًا فيمكنك إعادة استخدام معامل التحكم (control parameter) من الفة الأساسية داخل الفئات الفرعية. فمثلًا، تخيل أن لديك الهرمية التالية من الفئات: فئة Mail الأساسية مع بضعة فئات فرعية: AirMail و GroundMail، فتكون فئات واجهة Transport هي Plane و Truck و Train. وبينما تستخدم AirMail كائنات من Plane، فإن GroundMail ستعمل مع كائنات Truck و Train كليهما. يمكنك إنشاء فئات فرعية جديدة (TrainMail مثلًا) لمعالجة كلا الحالتين، لكن هناك خيار آخر، يمكن لشيفرة العميل أن تمرر وسيطًا (argument) لأسلوب المصنع الخاص بفئة GroundMail لاختيار أي المنتجات التي يريد استقبالها.
  6. إن أصبح أسلوب المصنع الأساسي فارغًا بعد كل الاستخراجات فيمكنك تحويله ليكون نظريًا (abstract)، أما إن تبقى شيء ما فيمكنك جعله السلوك الافتراضي للأسلوب.

المزايا والعيوب

المزايا

تتجنب الربط الوثيق بين منتجات المنشئ (creator products) والمنتجات الحقيقية (Concrete Products).

مبدأ المسؤولية الواحدة. يمكنك نقل شيفرة إنشاء المنتج إلى مكان واحد في البرنامج، لتسهيل عملية دعم الشيفرة.

مبدأ المفتوح/المغلق. يمكنك إدخال أنواع جديدة من المنتجات داخل البرنامج دون تعطيل شيفرة العميل الحالية.

العيوب

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

العلاقات مع الأنماط الأخرى

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

تُبنى فئات المصنع النظري عادة على سلسلة من أساليب المصنع، لكنك تستطيع استخدام النموذج الأولي لتشكيل الأساليب فوق تلك الفئات.

يمكنك استخدام أسلوب المصنع إلى جانب المكرِّر لتسمح لمجموعات من الفئات الفرعية بإعادة أنواع مختلفة من المكرٍّرات تتوافق مع المجموعات.

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

أسلوب المصنع هو استثناء من أسلوب القالب (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: نتائج التنفيذ (صندوق حواري في ويندوز)

ضع الصورة