نمط النموذج الأولي

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث

نمط النموذج الأولي (prototype) هو نمط تصميم إنشائي يسمح لك بنسخ الكائنات الموجودة حاليًا دون جعل شيفرتك تعتمد على فئات تلك الكائنات.

المشكلة

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

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

الحل

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

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

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

مثال حي

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

البُنية

ش.1

التطبيق الأساسي

  1. تصرح واجهة النموذج الأولي (prototype) عن أساليب الاستنساخ، وفي أغلب الحالات يكون أسلوب clone وحيد.
  2. تستخدم فئة النموذج الأولي الحقيقي (Concrete prototype) أسلوب الاستنساخ، وإضافة إلى نسخ بيانات الكائن الأصلي إلى المستنسَخ، فقد يتولى هذا الأسلوب أيضًا بعض الحالات الشاذة لعملية الاستنساخ المتعلقة باستنساخ الكائنات المرتبطة ببعضها أو حل الاعتماديات التكرارية (recursive dependencies)، إلخ
  3. يمكن للعميل (Client) أن ينتج نسخة من أي كائن يتبع واجهة النموذج الأولي.
ش.2

تطبيق سجل النموذج الأولي

  1. يوفر سجل النموذج الأولي (Prototype Registry) طريقة سهلة للوصول إلى النماذج الأولية المستخدمة بكثرة، فهو يخزن مجموعة من الكائنات المبنية مسبقًا والجاهزة للنسخ. وأسهل تسجيل للنموذ الأولي هو خريطة مزيج name → prototype ، لكن بأي حال، إن كنت تريد معايير بحث أفضل من مجرد اسم بسيط فيمكنك بناء نسخة من السجل أفضل من ذلك وأثبَتْ.

مثال توضيحي

ش.3 استنساخ مجموعة كائنات تنتمي إلى هرمية فئة ما.

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

 // النموذج الأولي الأساسي.
abstract class Shape is
    field X: int
    field Y: int
    field color: string

    // مؤسس عادي.
    constructor Shape() is
        // ...

    // مؤسس النموذج الأولي. يُبدأ كائن جديد بقيم من الكائن الموجود فعلًا.
   constructor Shape(source: Shape) is
        this()
        this.X = source.X
        this.Y = source.Y
        this.color = source.color

    // الفرعية shape تعيد عملية الاستنساخ إحدى فئات.
    abstract method clone():Shape


// النموذج الأولي الحقيقي. تنشئ عملية الاستنساخ كائنًا جديدًا وتمرره إلى 
// المؤسس. ويظل للمؤسس مرجعٌ إلى نسخة حديثة حتي ينتهي، لذا لا يكون لأي أحد
// وصول إلى نسخة غير مكتملة البناء من أجل الحفاظ على ثبات النتيجة.
class Rectangle extends Shape is
    field width: int
    field height: int

    constructor Rectangle(source: Rectangle) is
        // استدعاء المؤسس الأساسي مطلوب لنسخ الحقول الخاصة المعرَّفة في 
        // الفئة الأم.
        super(source)
        this.width = source.width
        this.height = source.height

    method clone():Shape is
        return new Rectangle(this)


class Circle extends Shape is
    field radius: int

    constructor Circle(source: Circle) is
        super(source)
        this.radius = source.radius

    method clone():Shape is
        return new Circle(this)


// في مكان ما داخل شيفرة العميل
class Application is
    field shapes: array of Shape

    constructor Application() is
        Circle circle = new Circle()
        circle.width = 10
        circle.height = 10
        circle.radius = 20
        shapes.add(circle)

        Circle anotherCircle = circle.clone()
        shapes.add(anotherCircle)
        // على نسخة طبق الأصل من 'anotherCircle' يحتوي متغير
        // 'circle' كائن.

        Rectangle rectangle = new Rectangle()
        rectangle.width = 10
        rectangle.height = 20
        shapes.add(rectangle)

    method businessLogic() is
        // تظهر أفضلية النموذج الأولي هنا إذ يسمح لك بإنتاج نسخة من الكائن
        // دون معرفة أي شيء عن نوعه.
        Array shapesCopy = new Array of Shapes.

        // فمثلًا، لا نعرف عناصر مصفوفة الأشكال بالضبط، فكل ما نعرفه أنها أشكال
        // 'clone' كلها. لكن بفضل تعدد الأشكال فإننا حين نستدعي أسلوب
        // على شكل ما، فإن البرنامج يفحص فئته الحقيقية وينفِّذ أسلوب الاستنساخ
        // المناسب والمعرَّف في تلك الفئة. هذا هو سبب حصولنا على نسخ صحيحة
        // ومناسبة بدلًا من مجموعة كائنات أشكال بسيطة.
        foreach (s in shapes) do
            shapesCopy.add(s.clone())

        // على نسخ طبق الأصل من  `shapesCopy` تحتوي مصفوفة
        // `shape` عناصر مصفوفة.

قابلية التطبيق

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

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

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

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

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

  1. أنشئ واجهة النموذج الأولي وصرّح عن أسلوب clone فيها، أو أضف الأسلوب إلى كل الفئات في الهرمية الحالية للفئات إن كانت لديك.
  2. يجب أن تعرِّف فئة النموذج الأولي المنشئ البديل الذي يقبل كائنًا من تلك الفئة كوسيط، ويجب أن ينسخ المنشئ فيم كل الحقول المعرَّفة في تلك الفئة من الكائن المُمرَّر إلى النسخة المنشأة حديثًا. وإن كنت تغير فئة فرعية فيجب أن تستدعي المنشئ الأساسي (parent constructor) ليسمح للفئة الأساسية (superclass) بتولي عملية استنساخ حقولها الخاصة. أما إن كانت اللغة البرمجية التي تستخدمها لا تدعم التحميل الزائد (overloading)، فيمكنك أن تعرِّف أسلوبًا خاصًا لنسخ بيانات الكائن، ويمكنك استخدام المنشئ (constructor) كمكان مناسب لفعل ذلك بسبب أنه يسلم الكائن الناتج مباشرة بعد استدعاء معامِل new.
  3. يتكون أسلوب الاستنساخ عادة من سطر واحد: تشغيل معامِل new مع النسخة النموذجية من المنشئ، لاحظ أنه يجب على كل فئة أن تتخطى أسلوب الاستنساخ وتستخدم اسم فئتها مع معامل new، وإلا فإن أسلوب الاستنساخ قد ينتج كائنًا من الفئة الأم.
  4. أنشئ سجل نموذج أولي مركزي لتخزين فهرس بالنماذج الأولية التي تستخدم بكثرة.

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

وأخيرًا، استبدل الاستدعاءات المباشرة إلى منشئات الفئات الفرعيةباستدعاءات إلى أسلوب المصنع لسجل النموذج الأولي.

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

المزايا

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

العيوب

  • قد يكون من الصعب استنساخ الكائنات المعقدة التي لديها مراجع حِلَقية (circular references).

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

  • تبدأ العديد من التصميمات مستخدمة أسلوب المصنع بما أنه أقل تعقيدًا وأكثر قابلية للتخصيص عبر الفئات الفرعية، ثم تتطور إلى أنماط المصنع المجرد والنموذج الأولي والباني، إذ أنها أكثر مرونة لكنها معقدة أكثر في المقابل.
  • تُبنى فئات المصنع المجرد عادة على مجموعة من أساليب المصنع، لكنك تستطيع استخدام نمط النموذج الأولي لتركيب الأساليب على تلك الفئات.
  • التصاميم التي تستخدم نمط المركَّب والمزخرِف قد تستفيد من استخدام نمط النموذج الأولي، إذ يسمح باستخدام بُنى معقدة بدلًا من إعادة إنشائها من الصفر.
  • لا يبنى نمط النموذج الأولي على الاكتساب (inheritance) لذا ليس له مساوئه، لكن من الناحية الأخرى فإن النموذج الأولي يتطلب عملية بدء (initialization) معقدة للكائن المستنسخ. في المقابل، أسلوب المصنع مبني على الاكتساب لكنه لا يتطلب خطوة بدء.
  • أحيانًا قد يكون نمط النموذج الأول بديلًا بسيطًا لنمط التذكرة (Memento)، حين يكون الكائن، الحالة التي تريد تخزينها في السجل، بسيطة للغاية وليس لديها روابط إلى مصادر خارجية أو أن الروابط يسهل إعادة بناؤها.
  • المصانع المجردة والبانيات والنماذج الأولية يمكن استخدامها جميعها كمفردات.

الاستخدام في لغة جافا

المستوى: ★ ☆ ☆

الانتشار: ★ ★ ☆

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

java.lang.Object#clone(): يجب أن تستخدم الفئة واجهة java.lang.Cloneable.

يمكن ملاحظة النموذج الأولي من خلال أساليب مثل ()clone أو ()copy.

مثال: نسخ الأشكال المرئية

يوضح المثال التالي كيفية استخدام نمط النموذج الأولي بدون واجهة Cloneable القياسية.

الأشكال: قائمة الأشكال

shapes/Shape.java: واجهة الشكل الشائع
package refactoring_guru.prototype.example.shapes;

import java.util.Objects;

public abstract class Shape {
    public int x;
    public int y;
    public String color;

    public Shape() {
    }

    public Shape(Shape target) {
        if (target != null) {
            this.x = target.x;
            this.y = target.y;
            this.color = target.color;
        }
    }

    public abstract Shape clone();

    @Override
    public boolean equals(Object object2) {
        if (!(object2 instanceof Shape)) return false;
        Shape shape2 = (Shape) object2;
        return shape2.x == x && shape2.y == y && Objects.equals(shape2.color, color);
    }
}
shapes/Circle.java: شكل بسيط
package refactoring_guru.prototype.example.shapes;

public class Circle extends Shape {
    public int radius;

    public Circle() {
    }

    public Circle(Circle target) {
        super(target);
        if (target != null) {
            this.radius = target.radius;
        }
    }

    @Override
    public Shape clone() {
        return new Circle(this);
    }

    @Override
    public boolean equals(Object object2) {
        if (!(object2 instanceof Circle) || !super.equals(object2)) return false;
        Circle shape2 = (Circle) object2;
        return shape2.radius == radius;
    }
}
shapes/Rectangle.java: شكل آخر
package refactoring_guru.prototype.example.shapes;

public class Rectangle extends Shape {
    public int width;
    public int height;

    public Rectangle() {
    }

    public Rectangle(Rectangle target) {
        super(target);
        if (target != null) {
            this.width = target.width;
            this.height = target.height;
        }
    }

    @Override
    public Shape clone() {
        return new Rectangle(this);
    }

    @Override
    public boolean equals(Object object2) {
        if (!(object2 instanceof Rectangle) || !super.equals(object2)) return false;
        Rectangle shape2 = (Rectangle) object2;
        return shape2.width == width && shape2.height == height;
    }
}
Demo.java: مثال للاستنساخ
package refactoring_guru.prototype.example;

import refactoring_guru.prototype.example.shapes.Circle;
import refactoring_guru.prototype.example.shapes.Rectangle;
import refactoring_guru.prototype.example.shapes.Shape;

import java.util.ArrayList;
import java.util.List;

public class Demo {
    public static void main(String[] args) {
        List<Shape> shapes = new ArrayList<>();
        List<Shape> shapesCopy = new ArrayList<>();

        Circle circle = new Circle();
        circle.x = 10;
        circle.y = 20;
        circle.radius = 15;
        shapes.add(circle);

        Circle anotherCircle = (Circle) circle.clone();
        shapes.add(anotherCircle);

        Rectangle rectangle = new Rectangle();
        rectangle.width = 10;
        rectangle.height = 20;
        shapes.add(rectangle);

        cloneAndCompare(shapes, shapesCopy);
    }

    private static void cloneAndCompare(List<Shape> shapes, List<Shape> shapesCopy) {
        for (Shape shape : shapes) {
            shapesCopy.add(shape.clone());
        }

        for (int i = 0; i < shapes.size(); i++) {
            if (shapes.get(i) != shapesCopy.get(i)) {
                System.out.println(i + ": Shapes are different objects (yay!)");
                if (shapes.get(i).equals(shapesCopy.get(i))) {
                    System.out.println(i + ": And they are identical (yay!)");
                } else {
                    System.out.println(i + ": But they are not identical (booo!)");
                }
            } else {
                System.out.println(i + ": Shape objects are the same (booo!)");
            }
        }
    }
}
OutputDemo.txt: نتائج التنفيذ
0: Shapes are different objects (yay!)
0: And they are identical (yay!)
1: Shapes are different objects (yay!)
1: And they are identical (yay!)
2: Shapes are different objects (yay!)
2: And they are identical (yay!)
سجل النموذج الأولي

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

الذاكرة المؤقتة Cache

cache/BundledShapeCache.java: مصنع النموذج الأولي
package refactoring_guru.prototype.caching.cache;

import refactoring_guru.prototype.example.shapes.Circle;
import refactoring_guru.prototype.example.shapes.Rectangle;
import refactoring_guru.prototype.example.shapes.Shape;

import java.util.HashMap;
import java.util.Map;

public class BundledShapeCache {
    private Map<String, Shape> cache = new HashMap<>();

    public BundledShapeCache() {
        Circle circle = new Circle();
        circle.x = 5;
        circle.y = 7;
        circle.radius = 45;
        circle.color = "Green";

        Rectangle rectangle = new Rectangle();
        rectangle.x = 6;
        rectangle.y = 9;
        rectangle.width = 8;
        rectangle.height = 10;
        rectangle.color = "Blue";

        cache.put("Big green circle", circle);
        cache.put("Medium blue rectangle", rectangle);
    }

    public Shape put(String key, Shape shape) {
        cache.put(key, shape);
        return shape;
    }

    public Shape get(String key) {
        return cache.get(key).clone();
    }
}
Demo.java: مثال للاستنساخ
package refactoring_guru.prototype.caching;

import refactoring_guru.prototype.caching.cache.BundledShapeCache;
import refactoring_guru.prototype.example.shapes.Shape;

public class Demo {
    public static void main(String[] args) {
        BundledShapeCache cache = new BundledShapeCache();

        Shape shape1 = cache.get("Big green circle");
        Shape shape2 = cache.get("Medium blue rectangle");
        Shape shape3 = cache.get("Medium blue rectangle");

        if (shape1 != shape2 && !shape1.equals(shape2)) {
            System.out.println("Big green circle != Medium blue rectangle (yay!)");
        } else {
            System.out.println("Big green circle == Medium blue rectangle (booo!)");
        }

        if (shape2 != shape3) {
            System.out.println("Medium blue rectangles are two different objects (yay!)");
            if (shape2.equals(shape3)) {
                System.out.println("And they are identical (yay!)");
            } else {
                System.out.println("But they are not identical (booo!)");
            }
        } else {
            System.out.println("Rectangle objects are the same (booo!)");
        }
    }
}
 OutputDemo.txt: نتائج التنفيذ
Big green circle != Medium blue rectangle (yay!)
Medium blue rectangles are two different objects (yay!)
And they are identical (yay!)

الاستخدام في #C

المستوى: ★ ☆ ☆

الانتشار: ★ ★ ☆

أمثلة الاستخدام: نمط النموذج الأولي متاح افتراضيًا في لغة #C مع واجهة Cloneable.

يمكن ملاحظة النموذج الأولي من خلال أساليب مثل ()clone أو ()copy.

مثال: بُنية النمط

يوضح هذا المثال بنية نمط النموذج الأولي، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

program.cs: مثال هيكلي

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RefactoringGuru.DesignPatterns.Prototype.Structural
{
    class Program
    {
        static void Main(string[] args)
        {
            Client client = new Client();
            client.ClientCode();
        }
    }

    class Client
    {
        public void ClientCode()
        {
            Prototype prototype = new Prototype();
            prototype.Primitive = 245;
            prototype.Component = new DateTime();
            prototype.CircularReference = new ComponentWithBackReference(prototype);

            Prototype clone = prototype.Clone();

            if(prototype.Primitive == clone.Primitive)
            {
                Console.Write("Primitive field values have been carried over to a clone. Yay!\n");
            }
            else
            {
                Console.Write("Primitive field values have not been copied. Booo!\n");
            }

            if (prototype.Component != clone.Component)
            {
                Console.Write("Simple component has been cloned. Yay!\n");
            }
            else
            {
                Console.Write("Simple component has not been cloned. Booo!\n");
            }

            if (prototype.CircularReference != clone.CircularReference)
            {
                Console.Write("Component with back reference has been cloned. Yay!\n");
            }
            else
            {
                Console.Write("Component with back reference has not been cloned. Booo!\n");
            }

            if (clone.CircularReference.Prototype != prototype)
            {
                Console.Write("Component with back reference is not linked to original object. Yay!\n");
            }
            else
            {
                Console.Write("Component with back reference is linked to original object. Booo!\n");
            }
        }
    }

    public class Prototype
    {
        public int Primitive { get; set; }

        public DateTime Component { get; set; }

        public ComponentWithBackReference CircularReference { get; set; }

        public Prototype()
        { }
        
        public Prototype Clone()
        {
            Prototype clone = this.MemberwiseClone() as Prototype;

            clone.Component = this.Component.MemberwiseClone() as DateTime;

            clone.CircularReference = this.CircularReference.MemberwiseClone() as ComponentWithBackReference;
            clone.CircularReference.Prototype = this;

            return clone;
        }
    }

    public class ComponentWithBackReference
    {
        public Prototype Prototype { get; set; }

        public ComponentWithBackReference(Prototype p)
        {
            Prototype = p;
        }
    }
}

Output.txt: المخرجات

Primitive field values have been carried over to a clone. Yay!
Simple component has been cloned. Yay!
Component with back reference has been cloned. Yay!
Component with back reference is linked to the clone. Yay!

الاستخدام في لغة PHP

المستوى: ★ ☆ ☆

الانتشار: ★ ★ ☆

أمثلة الاستخدام: نمط النموذج الأولي متاح في لغة PHP مباشرة، يمكنك استخدام كلمة clone المفتاحية لإنشاء نسخة طبق الأصل من كائن ما، أما لإضافة دعم الاستنساخ إلى فئة ما فستحتاج إلى استخدام أسلوب clone__.

مثال: بنية النمط

يوضح هذا المثال بنية نمط النموذج الأولي، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

PrototypeStructural.php: مثال هيكلي

<?php

namespace RefactoringGuru\Prototype\Structural;

/**
 * فئة المثال التي لديها إمكانية الاستنساخ.
 */
class Prototype
{
    public $primitive;
    public $component;
    public $circularReference;

    /**
     * لديها دعم مدمج فيها للاستنساخ، تستطيع استنساخ كائن ما دون تحديد PHP
     * أساليب خاصة طالما أن لديه حقولًا من الأنواع الأساسية.
     * تحتفظ الحقول الحاوية للكائنات بمراجعها في الكائن المستنسخ، لهذا 
     * قد ترغب باستنساخ تلك الكائنات المرجعية أيضًا، تستطيع فعل ذلك 
     * '__clone()' في الأسلوب الخاص
     */
    public function __clone()
    {
        $this->component = clone $this->component;

        // "backreference" تحتاج إلى معاملة خاصة في حالة استنساخ كائن له مرجعية 
        // مع كائن آخر، فبعد تمام الاستنساخ يجب أن يشير (nested) خلفية ومتداخل 
        // الكائن المتداخل إلى الكائن المستنسَخ بدلًا من الكائن الأصلي.
        $this->circularReference = clone $this->circularReference;
        $this->circularReference->prototype = $this;
    }
}

class ComponentWithBackReference
{
    public $prototype;

    /**
     * لاحظ ان المنشئ لن ينفَّذ خلال الاستنساخ، فإن كان لديك منطق معقد داخل المنشئ
     * أيضًا `__clone` فقد تضطر إلى تنفيذه داخل أسلوب
     */
    public function __construct(Prototype $prototype)
    {
        $this->prototype = $prototype;
    }
}

/**
 * شيفرة العميل.
 */
function clientCode()
{
    $p1 = new Prototype;
    $p1->primitive = 245;
    $p1->component = new \DateTime;
    $p1->circularReference = new ComponentWithBackReference($p1);

    $p2 = clone $p1;
    if ($p1->primitive === $p2->primitive) {
        echo "Primitive field values have been carried over to a clone. Yay!\n";
    } else {
        echo "Primitive field values have not been copied. Booo!\n";
    }
    if ($p1->component === $p2->component) {
        echo "Simple component has not been cloned. Booo!\n";
    } else {
        echo "Simple component has been cloned. Yay!\n";
    }

    if ($p1->circularReference === $p2->circularReference) {
        echo "Component with back reference has not been cloned. Booo!\n";
    } else {
        echo "Component with back reference has been cloned. Yay!\n";
    }

    if ($p1->circularReference->prototype === $p2->circularReference->prototype) {
        echo "Component with back reference is linked to original object. Booo!\n";
    } else {
        echo "Component with back reference is linked to the clone. Yay!\n";
    }
}

clientCode();

Output.txt: المخرجات

Primitive field values have been carried over to a clone. Yay!
Simple component has been cloned. Yay!
Component with back reference has been cloned. Yay!
Component with back reference is linked to the clone. Yay!

مثال: حالة حقيقية

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

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

 PrototypeRealWorld.php: حالة حقيقية

<?php

namespace RefactoringGuru\Prototype\RealWorld;

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

/**
 * النموذج الأولي.
 */
class Page
{
    private $title;

    private $body;

    /**
     * @var Author
     */
    private $author;

    private $comments = [];

    /**
     * @var \DateTime
     */
    private $date;

    // حقول خاصة +100.

    public function __construct(string $title, string $body, Author $author)
    {
        $this->title = $title;
        $this->body = $body;
        $this->author = $author;
        $this->author->addToPage($this);
        $this->date = new \DateTime;
    }

    public function addComment(string $comment): void
    {
        $this->comments[] = $comment;
    }

    /**
     * تستطيع التحكم في البيانات التي تريد نقلها إلى الكائن المستنسَخ.
     *
     * فمثلًا، عند استنساخ صفحة:
     * - "Copy of ..."فإنها تحصل على عنوان.
     * - يظل مؤلف الصفحة كما هو، لهذا نترك المرجع إلى الكائن الحالي أثناء إضافة
     * الصفحة المستنسخة إلى قائمة صفحات المؤلف.
     * - لا ننقل التعليقات من الصفحة القديمة.
     * - نلحق كائن تاريخ جديد إلى الصفحة.
     */
    public function __clone()
    {
        $this->title = "Copy of " . $this->title;
        $this->author->addToPage($this);
        $this->comments = [];
        $this->date = new \DateTime;
    }
}

class Author
{
    private $name;

    /**
     * @var Page[]
     */
    private $pages = [];

    public function __construct(string $name)
    {
        $this->name = $name;
    }

    public function addToPage(Page $page): void
    {
        $this->pages[] = $page;
    }
}

/**
 * شيفرة العميل.
 */
function clientCode()
{
    $author = new Author("John Smith");
    $page = new Page("Tip of the day", "Keep calm and carry on.", $author);

    // ...

    $page->addComment("Nice tip, thanks!");

    // ...

    $draft = clone $page;
    echo "Dump of the clone. Note that the author is now referencing two objects.\n\n";
    print_r($draft);
}

clientCode();

Output.txt: المخرجات

Dump of the clone. Note that the author is now referencing two objects.

RefactoringGuru\Prototype\RealWorld\Page Object
(
    [title:RefactoringGuru\Prototype\RealWorld\Page:private] => Copy of Tip of the day
    [body:RefactoringGuru\Prototype\RealWorld\Page:private] => Keep calm and carry on.
    [author:RefactoringGuru\Prototype\RealWorld\Page:private] => RefactoringGuru\Prototype\RealWorld\Author Object
        (
            [name:RefactoringGuru\Prototype\RealWorld\Author:private] => John Smith
            [pages:RefactoringGuru\Prototype\RealWorld\Author:private] => Array
                (
                    [0] => RefactoringGuru\Prototype\RealWorld\Page Object
                        (
                            [title:RefactoringGuru\Prototype\RealWorld\Page:private] => Tip of the day
                            [body:RefactoringGuru\Prototype\RealWorld\Page:private] => Keep calm and carry on.
                            [author:RefactoringGuru\Prototype\RealWorld\Page:private] => RefactoringGuru\Prototype\RealWorld\Author Object
 *RECURSION*
                            [comments:RefactoringGuru\Prototype\RealWorld\Page:private] => Array
                                (
                                    [0] => Nice tip, thanks!
                                )

                            [date:RefactoringGuru\Prototype\RealWorld\Page:private] => DateTime Object
                                (
                                    [date] => 2018-06-04 14:50:39.306237
                                    [timezone_type] => 3
                                    [timezone] => UTC
                                )

                        )

                    [1] => RefactoringGuru\Prototype\RealWorld\Page Object
 *RECURSION*
                )

        )

    [comments:RefactoringGuru\Prototype\RealWorld\Page:private] => Array
        (
        )

    [date:RefactoringGuru\Prototype\RealWorld\Page:private] => DateTime Object
        (
            [date] => 2018-06-04 14:50:39.306272
            [timezone_type] => 3
            [timezone] => UTC
        )

)