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

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

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

المشكلة

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

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

الحل

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

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

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

مثال حي

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

البُنية

ش.1

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

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



ش.2

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

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




مثال توضيحي

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

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





 // النموذج الأولي الأساسي.
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;

namespace RefactoringGuru.DesignPatterns.Prototype.Conceptual
{
    public class Person
    {
        public int Age;
        public DateTime BirthDate;
        public string Name;
        public IdInfo IdInfo;

        public Person ShallowCopy()
        {
            return (Person) this.MemberwiseClone();
        }

        public Person DeepCopy()
        {
            Person clone = (Person) this.MemberwiseClone();
            clone.IdInfo = new IdInfo(IdInfo.IdNumber);
            clone.Name = String.Copy(Name);
            return clone;
        }
    }

    public class IdInfo
    {
        public int IdNumber;

        public IdInfo(int idNumber)
        {
            this.IdNumber = idNumber;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Person p1 = new Person();
            p1.Age = 42;
            p1.BirthDate = Convert.ToDateTime("1977-01-01");
            p1.Name = "Jack Daniels";
            p1.IdInfo = new IdInfo(666);

            // P2 وأسندها إلى P1 خذ نسخة سطحية من .
            Person p2 = p1.ShallowCopy();
            // P3 وأسندها إلى P1 خذ نسخة عميقة من .
            Person p3 = p1.DeepCopy();

            // P3 و P2 و P1 اعرض قيم.
            Console.WriteLine("Original values of p1, p2, p3:");
            Console.WriteLine("   p1 instance values: ");
            DisplayValues(p1);
            Console.WriteLine("   p2 instance values:");
            DisplayValues(p2);
            Console.WriteLine("   p3 instance values:");
            DisplayValues(p3);

            // P3 و P2 و P1واعرض قيم P1 غيّر قيمة الخصائص لـ
            p1.Age = 32;
            p1.BirthDate = Convert.ToDateTime("1900-01-01");
            p1.Name = "Frank";
            p1.IdInfo.IdNumber = 7878;
            Console.WriteLine("\nValues of p1, p2 and p3 after changes to p1:");
            Console.WriteLine("   p1 instance values: ");
            DisplayValues(p1);
            Console.WriteLine("   p2 instance values (reference values have changed):");
            DisplayValues(p2);
            Console.WriteLine("   p3 instance values (everything was kept the same):");
            DisplayValues(p3);
        }

        public static void DisplayValues(Person p)
        {
            Console.WriteLine("      Name: {0:s}, Age: {1:d}, BirthDate: {2:MM/dd/yy}",
                p.Name, p.Age, p.BirthDate);
            Console.WriteLine("      ID#: {0:d}", p.IdInfo.IdNumber);
        }
    }
}

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

Original values of p1, p2, p3:
   p1 instance values: 
      Name: Jack Daniels, Age: 42, BirthDate: 01/01/77
      ID#: 666
   p2 instance values:
      Name: Jack Daniels, Age: 42, BirthDate: 01/01/77
      ID#: 666
   p3 instance values:
      Name: Jack Daniels, Age: 42, BirthDate: 01/01/77
      ID#: 666

Values of p1, p2 and p3 after changes to p1:
   p1 instance values: 
      Name: Frank, Age: 32, BirthDate: 01/01/00
      ID#: 7878
   p2 instance values (reference values have changed):
      Name: Jack Daniels, Age: 42, BirthDate: 01/01/77
      ID#: 7878
   p3 instance values (everything was kept the same):
      Name: Jack Daniels, Age: 42, BirthDate: 01/01/77
      ID#: 666

الاستخدام في لغة 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
        )

)

الاستخدام في لغة بايثون

المستوى: ★ ☆ ☆

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

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

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

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

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

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

 main.py: مثال تصوري

from __future__ import annotations
from datetime import datetime
from copy import deepcopy
from typing import Any


class Prototype:
    """
    فئة المثال هذه لديها القدرة على الاستنساخ، سنرى كيف تُنسخ قيم الحقول مختلفة الأنواع.
    """

    def __init__(self) -> None:
        self._primitive = None
        self._component = None
        self._circular_reference = None

    @property
    def primitive(self) -> Any:
        return self._primitive

    @primitive.setter
    def primitive(self, value: Any) -> None:
        self._primitive = value

    @property
    def component(self) -> object:
        return self._component

    @component.setter
    def component(self, value: object) -> None:
        self._component = value

    @property
    def circular_reference(self) -> ComponentWithBackReference:
        return self._circular_reference

    @circular_reference.setter
    def circular_reference(self, value: ComponentWithBackReference) -> None:
        self._circular_reference = value

    def clone(self) -> Prototype:
        self.component = deepcopy(self.component)

        # "backreference" تحتاج إلى معاملة خاصة في حالة استنساخ كائن له مرجعية 
        #  مع كائن آخر، فبعد تمام الاستنساخ يجب أن يشير (nested) خلفية ومتداخل 
        # الكائن المتداخل إلى الكائن المستنسَخ بدلًا من الكائن الأصلي.
        self.circular_reference = deepcopy(self.circular_reference)
        self.circular_reference.prototype = self
        return deepcopy(self)


class ComponentWithBackReference:
    def __init__(self, prototype: Prototype):
        self._prototype = prototype

    @property
    def prototype(self) -> Prototype:
        return self._prototype

    @prototype.setter
    def prototype(self, value: Prototype) -> None:
        self._prototype = value


if __name__ == "__main__":
    # شيفرة العميل.
    p1 = Prototype()
    p1.primitive = 245
    p1.component = datetime.now()
    p1.circular_reference = ComponentWithBackReference(p1)

    p2 = p1.clone()

    if p1.primitive is p2.primitive:
        print("Primitive field values have been carried over to a clone. Yay!")
    else:
        print("Primitive field values have not been copied. Booo!")

    if p1.component is p2.component:
        print("Simple component has not been cloned. Booo!")
    else:
        print("Simple component has been cloned. Yay!")

    if p1.circular_reference is p2.circular_reference:
        print("Component with back reference has not been cloned. Booo!")
    else:
        print("Component with back reference has been cloned. Yay!")

    if p1.circular_reference.prototype is p2.circular_reference.prototype:
        print("Component with back reference is linked to original object. Booo!", end="")
    else:
        print("Component with back reference is linked to the clone. Yay!", end="")

 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!

الاستخدام في لغة روبي

المستوى: ★ ☆ ☆

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

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

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

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

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

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

 main.rb: مثال تصوري

# فئة المثال هذه لديها القدرة على الاستنساخ، سنرى كيف تُنسخ قيم الحقول مختلفة الأنواع.
class Prototype
  attr_accessor :primitive, :component, :circular_reference

  def initialize
    @primitive = nil
    @component = nil
    @circular_reference = nil
  end

  # @return [Prototype]
  def clone
    @component = deep_copy(@component)

    # "backreference" تحتاج إلى معاملة خاصة في حالة استنساخ كائن له مرجعية 
    # مع كائن آخر، فبعد تمام الاستنساخ يجب أن يشير (nested) خلفية ومتداخل 
    # الكائن المتداخل إلى الكائن المستنسَخ بدلًا من الكائن الأصلي.
    @circular_reference = deep_copy(@circular_reference)
    @circular_reference.prototype = self
    deep_copy(self)
  end

  # هو الأسلوب المعتاد لصنع نسخة عميقة، لكنه بطيء وغير كفء deep_copy
  # خاصة عند العمل مع التطبيقات الحقيقية gem لهذا، استخدم.
  private def deep_copy(object)
    Marshal.load(Marshal.dump(object))
  end
end

class ComponentWithBackReference
  attr_accessor :prototype

  # @param [Prototype] prototype
  def initialize(prototype)
    @prototype = prototype
  end
end

# شيفرة العميل.
p1 = Prototype.new
p1.primitive = 245
p1.component = Time.now
p1.circular_reference = ComponentWithBackReference.new(p1)

p2 = p1.clone

if p1.primitive == p2.primitive
  puts 'Primitive field values have been carried over to a clone. Yay!'
else
  puts 'Primitive field values have not been copied. Booo!'
end

if p1.component.equal?(p2.component)
  puts 'Simple component has not been cloned. Booo!'
else
  puts 'Simple component has been cloned. Yay!'
end

if p1.circular_reference.equal?(p2.circular_reference)
  puts 'Component with back reference has not been cloned. Booo!'
else
  puts 'Component with back reference has been cloned. Yay!'
end

if p1.circular_reference.prototype.equal?(p2.circular_reference.prototype)
  print 'Component with back reference is linked to original object. Booo!'
else
  print 'Component with back reference is linked to the clone. Yay!'
end

 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!

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

المستوى: ★ ☆ ☆

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

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

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

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

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

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

بعد تعلم بنية النمط سيكون من السهل عليك استيعاب المثال التالي المبني على حالة واقعية في لغة Swift.

 Example.swift: مثال تصوري

import XCTest

/// تدعم الاستنساخ إذ هو مضمن فيها، ولكي تضيف ذلك الدعم إلى فئتك Swift لغة
/// copy في تلك الفئة وتوفير الاستخدام لأسلوب NSCopying فإنك تحتاج إلى استخدام بروتوكول.
class BaseClass: NSCopying, Equatable {

    private var intValue = 1
    private var stringValue = "Value"

    required init(intValue: Int = 1, stringValue: String = "Value") {

        self.intValue = intValue
        self.stringValue = stringValue
    }

    /// MARK: - NSCopying
    func copy(with zone: NSZone? = nil) -> Any {
        let prototype = type(of: self).init()
        prototype.intValue = intValue
        prototype.stringValue = stringValue
        print("Values defined in BaseClass have been cloned!")
        return prototype
    }

    /// MARK: - Equatable
    static func == (lhs: BaseClass, rhs: BaseClass) -> Bool {
        return lhs.intValue == rhs.intValue && lhs.stringValue == rhs.stringValue
    }
}

/// لتنسخ بياناتها داخل copy تستطيع الفئات الفرعية أن تتجاوز أسلوب
/// الكائن الناتج، لكن يجب أن تستدعي الأسلوب الأساسي أولًا.
class SubClass: BaseClass {

    private var boolValue = true

    func copy() -> Any {
        return copy(with: nil)
    }

    override func copy(with zone: NSZone?) -> Any {
        guard let prototype = super.copy(with: zone) as? SubClass else {
            return SubClass() // oops
        }
        prototype.boolValue = boolValue
        print("Values defined in SubClass have been cloned!")
        return prototype
    }
}

/// شيفرة العميل.
class Client {
    // ...
    static func someClientCode() {
        let original = SubClass(intValue: 2, stringValue: "Value2")

        guard let copy = original.copy() as? SubClass else {
            XCTAssert(false)
            return
        }

        /// للمزيد من التفاصيل Equatable انظر استخدام بروتوكول.
        XCTAssert(copy == original)

        print("The original object is equal to the copied object!")
    }
    // ...
}

/// لنرى الآن كيف سيعمل كل ذلك.
class PrototypeConceptual: XCTestCase {

    func testPrototype_NSCopying() {
        Client.someClientCode();
    }
}

 Output.txt: نتائج التنفيذ

Values defined in BaseClass have been cloned!
Values defined in SubClass have been cloned!
The original object is equal to the copied object!

مثال واقعي

 Example.swift: مثال واقعي

import XCTest

class PrototypeRealWorld: XCTestCase {

    func testPrototypeRealWorld() {

        let author = Author(id: 10, username: "Ivan_83")
        let page = Page(title: "My First Page", contents: "Hello world!", author: author)

        page.add(comment: Comment(message: "Keep it up!"))

        /// يعيد أي شيء NSCopying بما أن.
        guard let anotherPage = page.copy() as? Page else {
            XCTFail("Page was not copied")
            return
        }

        /// يجب أن تكون التعليقات فارغة بما أن الصفحة جديدة.
        XCTAssert(anotherPage.comments.isEmpty)

        /// لاحظ أن المؤلف يشير الآن إلى كائنين.
        XCTAssert(author.pagesCount == 2)

        print("Original title: " + page.title)
        print("Copied title: " + anotherPage.title)
        print("Count of pages: " + String(author.pagesCount))
    }
}

private class Author {

    private var id: Int
    private var username: String
    private var pages = [Page]()

    init(id: Int, username: String) {
        self.id = id
        self.username = username
    }

    func add(page: Page) {
        pages.append(page)
    }

    var pagesCount: Int {
        return pages.count
    }
}

private class Page: NSCopying {

    private(set) var title: String
    private(set) var contents: String
    private weak var author: Author?
    private(set) var comments = [Comment]()

    init(title: String, contents: String, author: Author?) {
        self.title = title
        self.contents = contents
        self.author = author
        author?.add(page: self)
    }

    func add(comment: Comment) {
        comments.append(comment)
    }

    /// MARK: - NSCopying

    func copy(with zone: NSZone? = nil) -> Any {
        return Page(title: "Copy of '" + title + "'", contents: contents, author: author)
    }
}

private struct Comment {

    let date = Date()
    let message: String
}

 Output.txt: نتائج التنفيذ

Original title: My First Page
Copied title: Copy of 'My First Page'
Count of pages: 2

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

المستوى: ★ ☆ ☆

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

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

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

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

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

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

 index.ts: مثال تصوري

/**
 * فئة المثال هذه لديها القدرة على الاستنساخ، سنرى كيف تُنسخ قيم الحقول مختلفة الأنواع.
 */
class Prototype {
    public primitive: any;
    public component: object;
    public circularReference: ComponentWithBackReference;

    public clone(): this {
        const clone = Object.create(this);

        clone.component = Object.create(this.component);

        // يحتاج استنساخ كائن به كائن متداخل مع مرجع خلفي إلى معاملة خاصة،
        // ذلك أنه بعد انتهاء الاستنساخ فإن الكائن المتداخل يجب أن يشير إلى
        // الكائن المستنسَخ، بدلًا من الكائن الأصلي.
        // مفيدًا ها هنا Spread قد يكون معامِل.
        clone.circularReference = {
            ...this.circularReference,
            prototype: { ...this },
        };

        return clone;
    }
}

class ComponentWithBackReference {
    public prototype;

    constructor(prototype: Prototype) {
        this.prototype = prototype;
    }
}

/**
 * شيفرة العميل.
 */
function clientCode() {
    const p1 = new Prototype();
    p1.primitive = 245;
    p1.component = new Date();
    p1.circularReference = new ComponentWithBackReference(p1);

    const p2 = p1.clone();
    if (p1.primitive === p2.primitive) {
        console.log('Primitive field values have been carried over to a clone. Yay!');
    } else {
        console.log('Primitive field values have not been copied. Booo!');
    }
    if (p1.component === p2.component) {
        console.log('Simple component has not been cloned. Booo!');
    } else {
        console.log('Simple component has been cloned. Yay!');
    }

    if (p1.circularReference === p2.circularReference) {
        console.log('Component with back reference has not been cloned. Booo!');
    } else {
        console.log('Component with back reference has been cloned. Yay!');
    }

    if (p1.circularReference.prototype === p2.circularReference.prototype) {
        console.log('Component with back reference is linked to original object. Booo!');
    } else {
        console.log('Component with back reference is linked to the clone. Yay!');
    }
}

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!

انظر أيضًا

مصادر