ConstructiCat Logo
CodeBust.
Browse section ▾

النموذج الأولي (Prototype).

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

##Intent

Prototype is a creational design pattern that lets you copy existing objects without making your code dependent on their classes.

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

##Problem

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

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

ما الذي يمكن أن يحدث خطأ عند نسخ الأشياء "من الخارج"؟

نسخ الكائن "من الخارج" ليس ممكنًا دائمًا.

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

##Solution

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

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

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

النماذج الأولية المبنية مسبقًا

يمكن أن تكون النماذج الأولية المبنية مسبقًا بديلاً للتصنيف الفرعي.

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

##Structure

التنفيذ الأساسي

Client «interface» Prototype +clone() : Prototype ConcretePrototype -field1+ConcretePrototype(prototype)+clone() : Prototype SubclassPrototype -field2+SubclassPrototype(prototype)+clone() : Prototype copy = existing.clone() this.field1 = prototype.field1return new ConcretePrototype(this) super(prototype)this.field2 = prototype.field2return new SubclassPrototype(this)
Client «interface» Prototype +clone() : Prototype ConcretePrototype -field1+ConcretePrototype(prototype)+clone() : Prototype SubclassPrototype -field2+SubclassPrototype(prototype)+clone() : Prototype copy = existing.clone() this.field1 = prototype.field1return new ConcretePrototype(this) super(prototype)this.field2 = prototype.field2return new SubclassPrototype(this) 1 2 3
  1. تُعلن واجهة Prototype عن طرق الاستنساخ. في معظم الحالات تكون طريقة clone واحدة فقط.

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

  3. يمكن لـ Client إنتاج نسخة من أي كائن يتبع واجهة النموذج الأولي.

تنفيذ سجل النماذج الأولية

Client PrototypeRegistry -items : Prototype[]+addItem(id: string, p: Prototype)+getById(id: string) : Prototype+getByColor(color: string) : Prototype «interface» Prototype +getColor() : string+clone() : Prototype Button -x, y, color+Button(x, y, color)+Button(prototype)+getColor() : string+clone() : Prototype button = new Button(10, 40, \"red\")registry.addItem(\"LandingButton\", button) button = registry.getByColor(\"red\") foreach (item in items) if (item.getColor() == color) return item.clone() return new Button(this)
Client PrototypeRegistry -items : Prototype[]+addItem(id: string, p: Prototype)+getById(id: string) : Prototype+getByColor(color: string) : Prototype «interface» Prototype +getColor() : string+clone() : Prototype Button -x, y, color+Button(x, y, color)+Button(prototype)+getColor() : string+clone() : Prototype button = new Button(10, 40, \"red\")registry.addItem(\"LandingButton\", button) button = registry.getByColor(\"red\") foreach (item in items) if (item.getColor() == color) return item.clone() return new Button(this) 1
  1. يوفر Prototype Registry طريقة سهلة للوصول إلى النماذج الأولية المستخدمة بشكل متكرر. يُخزِّن مجموعة من الكائنات المبنية مسبقًا الجاهزة للنسخ. أبسط سجل للنماذج الأولية هو خريطة تجزئة name → prototype. ومع ذلك، إذا كنت بحاجة إلى معايير بحث أفضل من مجرد اسم، يمكنك بناء نسخة أكثر قوة من السجل.

##Pseudocode

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

بنية مثال نمط النموذج الأولي

Cloning a set of objects that belong to a class hierarchy.

All shape classes follow the same interface, which provides a cloning method. A subclass may call the parent’s cloning method before copying its own field values to the resulting object.

// النموذج الأولي الأساسي.
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.X = 10
        circle.Y = 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` على شكل ما يتحقق البرنامج من فئته الحقيقية
        // ويشغّل طريقة الاستنساخ المناسبة المُعرَّفة
        // في تلك الفئة. لهذا السبب نحصل على نسخ صحيحة
        // بدلاً من مجموعة من كائنات Shape البسيطة.
        foreach (s in shapes) do
            shapesCopy.add(s.clone())

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

##Applicability

استخدم نمط النموذج الأولي عندما لا ينبغي لكودك الاعتماد على الفئات المحددة للكائنات التي تحتاج إلى نسخها.

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

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

استخدم النمط عندما تريد تقليل عدد الفئات الفرعية التي تختلف فقط في طريقة تهيئة كائناتها المعنية.

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

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

##How to Implement

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

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

    إذا كانت لغة البرمجة لا تدعم تحميل الدوال الزائد (method overloading)، لن تتمكن من إنشاء مُنشئ "نموذج أولي" منفصل. وبالتالي ستُنفَّذ عملية نسخ بيانات الكائن إلى النسخة المستنسخة المنشأة حديثًا داخل طريقة clone. ومع ذلك، وجود هذا الكود في مُنشئ عادي أكثر أمانًا لأن الكائن الناتج يُعاد مُهيَّأ بالكامل مباشرةً بعد استدعاء عامل new.

  3. تتكون طريقة الاستنساخ عادةً من سطر واحد فقط: تشغيل عامل new مع نسخة النموذج الأولي من المُنشئ. لاحظ أن كل فئة يجب أن تُجاوز طريقة الاستنساخ صراحةً وتستخدم اسم فئتها الخاص مع عامل new. وإلا قد تُنتج طريقة الاستنساخ كائنًا من الفئة الأصلية.

  4. اختياريًا، أنشئ سجل نماذج أولية مركزيًا لتخزين كتالوج من النماذج الأولية المستخدمة بشكل متكرر.

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

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

##Pros & Cons

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

##Relations with Other Patterns

  • تبدأ العديد من التصميمات باستخدام Factory Method (أقل تعقيداً وأكثر قابلية للتخصيص عبر الفئات الفرعية) وتتطور نحو Abstract Factory، أو Prototype، أو Builder (أكثر مرونة، ولكن أكثر تعقيداً).

  • غالباً ما تعتمد فئات Abstract Factory على مجموعة من Factory Methods، ولكن يمكنك أيضاً استخدام Prototype لتركيب الدوال على هذه الفئات.

  • يمكن أن يساعد Prototype عندما تحتاج إلى حفظ نسخ من Commands في السجل.

  • التصميمات التي تستخدم Composite وDecorator بشكل مكثف يمكنها في كثير من الأحيان الاستفادة من استخدام Prototype. يتيح لك تطبيق النمط استنساخ هياكل معقدة بدلاً من إعادة بنائها من الصفر.

  • لا يعتمد Prototype على الوراثة لذلك لا يعاني من عيوبها. من ناحية أخرى، يتطلب Prototype تهيئة معقدة للكائن المستنسخ. يعتمد Factory Method على الوراثة لكنه لا يتطلب خطوة تهيئة.

  • أحياناً يمكن أن يكون Prototype بديلاً أبسط لـ Memento. يعمل هذا إذا كان الكائن الذي تريد تخزين حالته في السجل بسيطًا نسبيًا ولا يحتوي على روابط لموارد خارجية، أو كانت الروابط سهلة الإعادة.

  • يمكن تنفيذ كل من Abstract Factories وBuilders وPrototypes كـ Singletons.