انتقال للمقال
وقت القراءة: ≈ 15 دقيقة

الـ Prototype Pattern استنساخ الـ Object

السلام عليكم ورحمة الله وبركاته

يمكنك متابعة السلسلة بالترتيب أو الانتقال مباشرة إلى أي مقال:


المقدمة

اليوم سنتعرف الـ Prototype Pattern وهو ينتمي إلى عائلة الـ Creational Design Patterns وهى المسؤولة عن عملية إنشاء الـ objects بطريقة مرنة ومنظمة
لتسهيل وتنظيم عملية بناء الـ objects المختلفة

والـ Prototype Pattern لا يركز على إنشاء object بحد ذاته
بل يفترض أن لديك بالفعل object جاهز بكل البيانات التي يحتاجها
وتريد فقط أن تحصل على نسخة جديدة مطابقة لهذا الـ object، دون أن تعيد بناءه من الصفر ؟

والـ Prototype Pattern بكل بساطة يسمح لك باستنساخ أي object موجود بالفعل كـ object جديد فيه كل البيانات
لكن قبل أن نبدأ في شرح الـ Prototype Pattern دعونا نتعرف أولاً على المشكلة التي يحلها

ما المشاكل التي يحلها الـ Prototype Pattern ؟

تخيل أنك تعمل على نظام لإدارة المنتجات
ولديك كلاس الـ Attributs الذي يمثل خصائص المنتج وكلاس الـ Product:

class Attributs {
  constructor(
    public material: string,
    public size: string,
    public color: string,
  ) {}
}

class Product {
  constructor(
    public sku: string,
    public name: string,
    public category: string,
    public price: number,
    public taxRate: number,
    public supplierId: string,
    public attributes: Attributs,
  ) {}
}

والآن لنقم بإنشاء منتج بسيط

const product_1 = new Product(
  "TSHIRT-WHT-S-0001",
  "Classic Cotton T-Shirt",
  "Apparel",
  19.99,
  0.14,
  "SUP-1042",
  new Attributs("cotton", "S", "white"),
);

الآن تخيل أننا نريد نسخة مطابقة من هذا الـ object
أول ما يخطر في بالك هو إنشاء object جديد وتكرار كل البيانات:

const product_2 = new Product(
  product_1.sku,
  product_1.name,
  product_1.category,
  product_1.price,
  product_1.taxRate,
  product_1.supplierId,
  product_1.attributes,
);

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

const product_3 = new Product(
  product_1.sku,
  product_1.name,
  product_1.category,
  product_1.price,
  product_1.taxRate,
  product_1.supplierId,
  product_1.attributes,
);

const product_4 = new Product(
  product_1.sku,
  product_1.name,
  product_1.category,
  product_1.price,
  product_1.taxRate,
  product_1.supplierId,
  product_1.attributes,
);

// ... etc

وأيضًا لدينا مشكلة أخرى وهى مشكلة الـ Shallow Copy بحيث أن product_1.attributes يمرر كـ reference وليس كنسخة مستقلة
وبما أن الـ attributes هو object بحد ذاته فهذا يعني أن product_1.attributes و product_2.attributes سيكونان نفس الـ reference وسيشيران إلى نفس الـ object
فلو قمنا بتعديل product_2.attributes ستتأثر product_1.attributes أيضاً دون أن تقصد ذلك:

product_2.attributes.color = "red";

console.log(product_1.attributes.color); // "red"
console.log(product_2.attributes.color); // "red"

لاحظ أنه برفممن أننا قمنا بتعديل فقط الـ attributes الخاص بـ product_2
إلا أن التعدي أثر على الـ attributes الخاص بـ product_1

قد تظن أن حلها بسيط بحيث أنك يمكنك استخدام طرق تحايلية لنسخ الخصائص مثل

product_2.attributes = new Attributs(
  product_1.attributes.material,
  product_1.attributes.size,
  product_1.attributes.color,
);

هذه الطريقة فعلًا قد تحل مشكلة الـ Shallow Copy وتحولها إلى Deep Copy
لكنها لا تحل مشكلة التكرار

بحيث أننا لو أردنا استنساخ product_1 إلى عدة نسخ هل نقوم بعمل الكود التالي كل مرة ؟

const new_project_1 = new Product(
  "TSHIRT-RED-M",
  product_1.name,
  product_1.category,
  product_1.price,
  product_1.taxRate,
  product_1.supplierId,
  new Attributs(
    product_1.attributes.material,
    product_1.attributes.size,
    product_1.attributes.color,
  ),
);

const new_project_2 = new Product(
  "TSHIRT-RED-M",
  product_1.name,
  product_1.category,
  product_1.price,
  product_1.taxRate,
  product_1.supplierId,
  new Attributs(
    product_1.attributes.material,
    product_1.attributes.size,
    product_1.attributes.color,
  ),
);

const new_project_3 = new Product(
  "TSHIRT-RED-M",
  product_1.name,
  product_1.category,
  product_1.price,
  product_1.taxRate,
  product_1.supplierId,
  new Attributs(
    product_1.attributes.material,
    product_1.attributes.size,
    product_1.attributes.color,
  ),
);

// ... etc

قد تفكر في حل مشكلة التكرار عن طريق كتابة دالة مساعدة للاستنساخ مثل duplicateProduct

function duplicateProduct(source: Product): Product {
  return new Product(
    source.sku,
    source.name,
    source.category,
    source.price,
    source.taxRate,
    source.supplierId,
    new Attributs(
      source.attributes.material,
      source.attributes.size,
      source.attributes.color,
    ),
  );
}

هذه الدالة قد تحل أو تخفف من مشكلة التكرار

const product_2 = duplicateProduct(product_1);
const product_3 = duplicateProduct(product_1);
const product_4 = duplicateProduct(product_1);

ويبدو أن هذا حلاً جيداً حتى الآن لكن ماذا سيحدث عندما يكون لدينا بيانات private داخل الكلاس ؟


لنفترض أن كلاس الـ Product بعض خصائصه أصبحت private على سبيل المثال

class Product {
  constructor(
    private sku: string,
    private name: string,
    private category: string,
    public price: number,
    public taxRate: number,
    public supplierId: string,
    public attributes: Attributs,
  ) {}
}

الآن دالة duplicateProduct لن تستطيع الوصول إلى أي خاصية من خصائص الـ private:

function duplicateProduct(source: Product): Product {
  return new Product(
    source.sku, // ❌ Property 'sku' is private and only accessible within class 'Product'
    source.name, // ❌ Property 'name' is private and only accessible within class 'Product'
    source.category, // ❌ Property 'category' is private and only accessible within class 'Product'
    // ...
  );
}

الكلاس يخفي بياناته الداخلية وهذا يعني أن أي دالة خارجية لن تستطيع نسخ الـ object بكل تفاصيله، بما فيها الخصائص الـ private


حتى لو تجاهلنا مشكلة الـ private، ماذا لو أن كلاس Product نفسه كان abstract class، وكان لدينا كلاسات مثل DigitalProduct و PhysicalProduct ترث منه ؟

abstract class Product {
  constructor(
    private sku: string,
    private name: string,
    private category: string,
    public price: number,
    public taxRate: number,
    public supplierId: string,
    public attributes: Attributs,
  ) {}
}

class DigitalProduct extends Product {
  constructor(
    sku: string,
    name: string,
    category: string,
    price: number,
    taxRate: number,
    supplierId: string,
    attributes: Attributs,
    public downloadLink: string,
    private fileSize: number,
  ) {
    super(sku, name, category, price, taxRate, supplierId, attributes);
  }
}

class PhysicalProduct extends Product {
  constructor(
    sku: string,
    name: string,
    category: string,
    price: number,
    taxRate: number,
    supplierId: string,
    attributes: Attributs,
    public weight: number,
    private shippingCost: number,
  ) {
    super(sku, name, category, price, taxRate, supplierId, attributes);
  }
}

هنا Product أصبح abstract class لا يمكن إنشاء object منه مباشرة
ولدينا DigitalProduct و PhysicalProduct وكلاهما يرثان منه

في هذه الحالة إذا كان لدينا object كمنتج رقمي و object آخر كمنتج مادي
وحاولنا استنساخهما باستخدام دالتنا البسيطة duplicateProduct فستواجهنا مشكلة كبيرة
وهى أن الدالة تعتمد على كلاس Product الأساسي وليس لديها أي علم بالـ DigitalProduct و PhysicalProduct
لذلك كل الخصائص الإضافية التي يضيفها الكلاس DigitalProduct أو الكلاس PhysicalProduct فلن نستطيع الوصول إليها

لذا قد تفكر في تعديل الدالة قليلًا واستخدام instanceof لتفريق عن طريق if-else

function duplicateProduct(source: Product): Product {
  if (source instanceof DigitalProduct) {
    return new DigitalProduct(
      source.sku,
      source.name,
      source.category,
      source.price,
      source.taxRate,
      source.supplierId,
      new Attributs(
        source.attributes.material,
        source.attributes.size,
        source.attributes.color,
      ),
      source.downloadLink,
      source.fileSize,
    );
  }
  if (source instanceof PhysicalProduct) {
    return new PhysicalProduct(
      source.sku,
      source.name,
      source.category,
      source.price,
      source.taxRate,
      source.supplierId,
      new Attributs(
        source.attributes.material,
        source.attributes.size,
        source.attributes.color,
      ),
      source.weight,
      source.shippingCost,
    );
  }
}

لكن لاحظ أن الدالة هكذا أصبحت فوضوية للغاية، وتواجه مشكلتين أساسيتين

أولاً الخصائص الـ private في الكلاسات بحيث أنه لا يمكن لأي دالة خارجية الوصول إليها، مما يجعل عملية النسخ مستحيلة تماماً من خارج الكلاس
ثانياً انتهاك لمبدأ الـ Open/Closed بحيث أنه في كل مرة نقرر فيها إضافة نوع منتج جديد مثل SubscriptionProduct أو BundleProduct سنضطر مجدداً لتعديل دالة duplicateProduct وإضافة شرط else if جديد
وهذا بإضافة إلى وجود تكرار للكود والفضوى التي في دالة duplicateProduct

ما الحل إذن؟

الحل هو أن نجعل كل كلاس مسؤولًا عن استنساخ نفسه
بمعى أن كلاس يجب أن يمتلك طريقة خاصة به لاستنساخ نفسه بسهولة

وهذا يقودنا مباشرة إلى الـ Prototype Pattern الذي يركز على إضافة دالة تدعى clone داخل الكلاس وجعل كل كلاس له مسؤولية الاستنساخ

ما هو الـ Prototype Pattern ؟

فكرة الـ Prototype Pattern بسيطة جدًا وهى أننا سنضيف دالة clone داخل الكلاس نفسه
أو بمعنى أصح أننا سنجبر الكلاسات على تنفيذ داله تدعى clone
هذه الدالة مسؤوليتها الوحيدة هي إنشاء نسخة مطابقة من الـ object الحالي وإعادتها

وبما أنها دالة موجودة داخل الكلاس، فالدالة تستطيع الوصول لكل شيء حتى الخصائص الـ private
وبما أن كل كلاس لديه دالة clone الخاصة به، فنحن هكذا نفوض مهمة الاستنساخ للكلاس فهو سيتحمل مسؤولية استنساخ نفسه بشكل كامل دون أن يحتاج لأي مساعدة خارجية

ولضمان إجبار كل كلاس بتنفيذ هذه الداله ننشئ interface يسمى Cloneable على سبيل المثال

interface Cloneable<T> {
  clone(): T;
}

بعض الأشخاص قد يسمي هذا الـ interface بـ Prototype
أي كلاس ينفذ هذا الـ interface سيكون مجبر على أن يوفر دالة clone تعيد نسخة من نفسه

والـ T هنا هو نوع الكلاس نفسه فالـ Cloneable<Attributs> يعني أن دالة clone ستقوم بإرجاع Attributs
و Cloneable<Product> يعني أن دالة clone ستقوم بإرجاع Product وهكذا
فالـ Cloneable<T> بشكل عام تعني أن دالة clone ستقوم بإرجاع object من نوع T
والـ <T> هذه تسمى Template أو Generic وهى نوع من أنواع الـ Compiler-Time Polymorphism


ولو كنت تتعامل مع لغة لا تدعم الـ Template أو Generic ففي هذه الحالة ستقوم بعمل interface بحسب النوع

interface ProductCloneable {
  clone(): Product;
}

interface AttributsCloneable {
  clone(): Attributs;
}

التطبيق العملي للـ Prototype Pattern

لنبدأ التطبيق من أبسط كلاس لدينا ونتوسع تدريجيًا
وأبسط كلاس لدينا هو Attributs
والحل هو أن يتولى الكلاس نفسه مسؤولية استنساخ نفسه
لذا سنطبق عليه فكرة الـ Prototype Pattern ونجعله ينفذ الـ interface الجميل الذي أنشأناه

class Attributs implements Cloneable<Attributs> {
  constructor(
    public material: string,
    public size: string,
    public color: string,
  ) {}

  clone(): Attributs {
    return new Attributs(this.material, this.size, this.color);
  }
}

هكذا كلاس الـ Attributs سيقوم بتنفيذ دالة clone ويكتب تفاصيل استنساخ object منه
وغالبًا ما تكون وظيفة الدالة فقط هى إرجاع object جديد عن طريق استدعاء الـ constructor

الآن نجرب:

const attributs = new Attributs("cotton", "M", "white");
const newAttributs = attributs.clone();

newAttributs.color = "red";

console.log(attributs.color); // "white"
console.log(newAttributs.color); // "red"

أنظر إلى جمال الكود وتأمله
دالة واحدة بسطر واحد حلت لنا جميع مشاكلنا attributs.clone()
أصبح هناك طريق ووسيلة بسيطة داخل كل كلاس و object تسمح لنا بأستنساخ أي object بدالة واحدة بسيطة
ولاحظ أنه حتى لو كانت جميع البيانات الخاصة بـ private فذلك لن يؤثر على شيء
قد نضطر لعمل دوال setter و getter لكن دالة الـ clone لن تتأثر

لاحظ الفكرة الأساسية هى جعل الكلاس يقوم بإنشاء نسخة من نفسه
ولاحظ أننا نخفي تفاصيل استنساخ أي object داخل دالة بسيطة
فأنا كشخص يريد فقط استنساخ object فأنا سوف استدعي دالة clone دون أن اهتم بتفاصيل كيفية تم الاستنساخ
فتلك التفاصيل لا تهمني فأنا فقط أريد نسخة فقط من الـ object دون معرفة لكيف تمت العملية

التطبيق على مثال الـ Product

الآن بعد أن فهمنا الفكرة الأساسية وطبقنا عليها على مثال Attributs البسيط
حان الوقت لتطبيقها على مثالنا الكامل مع الكلاس Product والكلاسات التي ترثه

أولاً سنعدل كلاس Product ليقوم بتنفيذ الـ Cloneable
لكن بما أنه abstract class فلن نقوم بعمل implementation للدالة بل سنتركها كما هى في الكلاس كـ abstract
لنجبر كل كلاس يرث منه على تنفيذ clone الخاصة به

abstract class Product implements Cloneable<Product> {
  constructor(
    public sku: string,
    public name: string,
    public category: string,
    public price: number,
    public taxRate: number,
    public supplierId: string,
    public attributes: Attributs,
  ) {}

  abstract clone(): Product;
}

لاحظ أننا تركنا clone كـ abstract دون تنفيذ
وهذا يضمن أن أي Concrete Prototype جديد مجبر على تنفيذ clone الخاصة به

وعليك أن تعلم أن كل كلاس ينفذ هذه الدالة يسمى Concrete Prototype


الآن لنبدأ مع الـ DigitalProduct:

class DigitalProduct extends Product {
  constructor(
    sku: string,
    name: string,
    category: string,
    price: number,
    taxRate: number,
    supplierId: string,
    attributes: Attributs,
    public downloadLink: string,
    private fileSize: number,
  ) {
    super(sku, name, category, price, taxRate, supplierId, attributes);
  }

  clone(): DigitalProduct {
    return new DigitalProduct(
      this.sku,
      this.name,
      this.category,
      this.price,
      this.taxRate,
      this.supplierId,
      this.attributes.clone(),
      this.downloadLink,
      this.fileSize,
    );
  }
}

لاحظ شيئاً مهماً وهى أن this.fileSize هي private لكن دالة clone تستطيع الوصول إليها بلا مشكلة
لأن دالة clone موجودة داخل DigitalProduct نفسه وليست خارجه
وهذا بالضبط ما يميزها عن دالة duplicateProduct الخارجية التي كانت تفشل في هذه الحالة

ولاحظ أننا في دالة clone استخدمنا this.attributes.clone() وليس this.attributes
لضمان حصولنا على نسخة Deep Copy من الـ attributes وليس نسخة Shallow Copy
واستطعنا استخدام دالة clone لأن كلاس الـ Attributs نفسه ينفذ الـ Prototype Pattern ولديه clone خاصة به
فبإمكاننا استنساخه بنفس الأسلوب ونضمن الـ Deep Copy

لكن لو افترضنا أن كلاس الـ Attributs كان كلاس لا ينفذ Prototype Pattern ولا توجد دالة تقوم بعملية النسخ
ولا تستطيع التعديل على هذا الكلاس، في حالة أنه كلاس خارجي أو من مكتبة لا تملك القدرة على تعديلها

فيمكن ايجاد طريقة بديلة لتنسخ الـ object سواء عن طريق استدعاء الـ constructor أو أي بسيلة متاحة لك
لأنه في النهاية التفاصيل النسخ ستكون مخبئة داخل دالة الـ clone فأيًا ما كانت الوسيلة التي استخدمتها للنسخ
طالما أنها صحيح فهذا لن يؤثر على استخدام الدالة
لأن تفاصيل الدالة الداخلية لا أحد سيهتم لها، المهم فقط هى النتيجة النهائية للدالة
وهذا هو أساس مبدأ الـ Abstraction


الآن لننفذ PhysicalProduct بنفس الأسلوب:

class PhysicalProduct extends Product {
  constructor(
    sku: string,
    name: string,
    category: string,
    price: number,
    taxRate: number,
    supplierId: string,
    attributes: Attributs,
    public weight: number,
    private shippingCost: number,
  ) {
    super(sku, name, category, price, taxRate, supplierId, attributes);
  }

  clone(): PhysicalProduct {
    return new PhysicalProduct(
      this.sku,
      this.name,
      this.category,
      this.price,
      this.taxRate,
      this.supplierId,
      this.attributes.clone(),
      this.weight,
      this.shippingCost,
    );
  }
}

الأمر مماثل لما فعلناه مع الـ DigitalProduct لكن دعنا نجرب استخدامعملية للدالة مع الـ PhysicalProduct

دعنا نجرب إنشاء object ونسخه ونعدل عليه

const shirt = new PhysicalProduct(
  "TSHIRT-WHT-S-0001",
  "Classic Cotton T-Shirt",
  "Apparel",
  19.99,
  0.14,
  "SUP-1042",
  new Attributs("cotton", "S", "white"),
  0.3,
  5.99,
);

هنا قمنا بإنشاء object بسيط من الـ PhysicalProduct
الآن لنجرب نسخه والتعديل

const redShirt = shirt.clone();
redShirt.attributes.color = "red";
redShirt.supplierId = "SUP-1055";

console.log(shirt.attributes.color); // "white"
console.log(shirt.supplierId); // "SUP-1042"

console.log(redShirt.attributes.color); // "red"
console.log(redShirt.supplierId); // "SUP-1055"

أنظر إلى الجمال والبساطة بحيث أن دالة واحدة تتيح لنا عمل نسخ مستقلة تمامًا من أي object نريده
فهنا قمنا بعمل object جديد يدعى redShirt وجعلناه يكون نسخة من الـ object الأول shirt
ثم قمنا بتعديل الـ color و supplierId الخاصة بالـ redShirt دون عن نؤثر على الـ object الأول shirt


وهنا تظهر ميزة إضافية الامتثال لمبدأ الـ Open/Closed
لو أردنا إضافة نوع منتج جديد مثل SubscriptionProduct فلن نعدل أي كود موجود
كل ما علينا هو إنشاء الكلاس وتنفيذ clone الخاصة به

class SubscriptionProduct extends Product {
  constructor(
    sku: string,
    name: string,
    category: string,
    price: number,
    taxRate: number,
    supplierId: string,
    attributes: Attributs,
    public billingCycle: string,
    private renewalPrice: number,
  ) {
    super(sku, name, category, price, taxRate, supplierId, attributes);
  }

  clone(): SubscriptionProduct {
    return new SubscriptionProduct(
      this.sku,
      this.name,
      this.category,
      this.price,
      this.taxRate,
      this.supplierId,
      this.attributes.clone(),
      this.billingCycle,
      this.renewalPrice,
    );
  }
}

لا تعديل على Product ولا على أي دالة خارجية، كل كلاس مسؤول عن نفسه

الـ Prototype Registry

الآن تخيل أن لديك مجموعة من المنتجات الجاهزة والمعدة مسبقًا تعمل كـ templates
وكلما احتجت منتجًا مشابهًا تريد فقط استنساخه وتعديل ما يلزم تعديله

قد تفكر في أنك يمكنك أن تحتفظ بـ object في متغير وتستدعي دالة الـ clone كلما احتجت نسخة
وهذا صحيح، لكن تخيل لو كان لديك 20 نوعًا مختلفًا من المنتجات الجاهزة
هل ستحتفظ بـ 20 متغيرًا في كل مكان تحتاج فيه إلى الاستنساخ؟
وماذا لو احتجت نفس النوع في أكثر من مكان في الكود؟

هنا يأتي دور الـ Prototype Registry
وهو ببساطة مكان مركزي يخزن prototypes جاهزة
بحيث تسجل الـ prototype مرة واحدة فقط فيه، وكلما احتجت نسخة جديدة تطلبها بـ key معين
فيعيد لك نسخة مستنسخة ومستقلة تماماً من هذا النوع

class ProductRegistry {
  private registry: Map<string, Product> = new Map();

  public register(key: string, product: Product): void {
    this.registry.set(key, product);
  }

  public get(key: string): Product {
    const product = this.registry.get(key);

    if (!product) {
      throw new Error(
        `There is no prototype registered with this key "${key}"`,
      );
    }

    return product.clone();
  }
}

الآن دعنا نحلل هذا الكلاس لنفهم ماذا يحدث بالضبط

تعريف registry ليمثل Hash Map

private registry: Map<string, Product> = new Map();

أولًا لدينا registry وهو Map يتكون من key من نوع string وقيمة من نوع Product
والـ key هو اسم نختاره نحن، مثل "tshirt" أو "ebook"، لنسهل استرجاع الـ Product لاحقًا

تعريف دالة الـ register

public register(key: string, product: Product): void {
  this.registry.set(key, product);
}

بكل بساطة هذه الدالة تأخذ key و product وتخزن الـ product في الـ Map تحت هذا الـ key
ووظيفتها فقط تسجيل prototype جديد في الـ Prototype Registry
لاحظ أن الـ product يخزن كما هو فنحن لا نستنسخه حاليًا، نحن فقط نضيفه إلى السجل كما هو

دالة الـ get

public get(key: string): Product {
  const product = this.registry.get(key);

  if (!product) {
    throw new Error(
      `There is no prototype registered with this key "${key}"`,
    );
  }

  return product.clone();
}

هذه الدالة في الكلاس ستأخذ الـ key فتبحث في الـ Map عن الـ prototype المسجل تحت هذا الـ key
إذا لم تجده، ترمي Error وتخبرك أنه لا يوجد prototype مسجل بهذا الاسم
وإذا وجدته، لا تعيده كما هو بل تعيد product.clone() أي نسخة جديدة مستقلة منه

وهذا هو الفرق الجوهري بين الـ Prototype Registry ومجرد Map عادي
ففي كل مرة تستدعي get تحصل على نسخة جديدة كليًا وليس على الـ object الأصلي نفسه
لأننا لو أعدنا الـ object نفسه، فأي تعديل خارجي سيؤثر على الـ object المخزن في الـ Prototype Registry
والفكرة أننا نحمي الـ prototypes الأصلية من أي تعديل غير مقصود ويكون لدينا مكان مركزي يخزن prototypes جاهزة
وكلما احتجنا لنسخة جديدة نطلبها من الـ Prototype Registry فيعيد لك نسخة مستنسخة ومستقلة تماماً من هذا النوع


الآن لنرى كيف نسجل الـ templates:

const registry = new ProductRegistry();

registry.register(
  "tshirt",
  new PhysicalProduct(
    "TSHIRT-WHT-S-0001",
    "Classic Cotton T-Shirt",
    "Apparel",
    19.99,
    0.14,
    "SUP-1042",
    new Attributs("cotton", "S", "white"),
    0.3,
    5.99,
  ),
);

registry.register(
  "ebook",
  new DigitalProduct(
    "EBOOK-TS-0001",
    "TypeScript Mastery",
    "Books",
    49.99,
    0.14,
    "SUP-2010",
    new Attributs("digital", "N/A", "N/A"),
    "https://example.com/downloads/ts-mastery.pdf",
    12.5,
  ),
);

هكذا بنينا tshirt و ebook كـ templates جاهزة، كل واحد مسجل تحت key معين
لاحظ أننا استخدمنا PhysicalProduct و DigitalProduct وليس Product
لأن الـ Product نفسه هو abstract class لا يمكن إنشاء object منه مباشرة
فالـ Registry يتعامل مع Product كـ interface لكن الـ objects الفعلية قد تكون من أي Concrete Prototype


الآن في أي مكان في الكود، كل ما تحتاجه هو:

const redShirt = registry.get("tshirt") as PhysicalProduct;
redShirt.attributes.color = "red";

const blueShirt = registry.get("tshirt") as PhysicalProduct;
blueShirt.attributes.color = "blue";

const arabicEbook = registry.get("ebook") as DigitalProduct;
arabicEbook.downloadLink =
  "https://example.com/downloads/ts-mastery-ar.pdf";

أنظر إلى البساطة والجمال
بثلاثة أسطر فقط حصلت على ثلاث نسخ مختلفة من منتجين أساسيين
وكل نسخة تعدلها كما تشاء دون أن تؤثر على الأخرى أو على الـ template الأصلي
فالـ redShirt و blueShirt كلاهما نسختان من نفس الـ template لكن لكل منهما لون مختلف ومستقل تماماً

الختام

الآن بعد أن رأينا الـ Prototype Pattern والـ Prototype Registry
دعنا نتأمل الفكرة الأساسية التي بدأنا بها:

اجعل كل كلاس مسؤولاً عن استنساخ نفسه

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

وأيضًا مشكلة الخصائص الـ private
بحيث عندما فكرنا بإنشاء دالة duplicateProduct لتنسخ واجهتنا مشاكل عديدة منها أنها لا تستطيع الوصول إلى الخصائص الـ private
والآن وبما أن دالة clone موجودة داخل الكلاس نفسه، تستطيع الوصول إلى كل شيء
حتى الخصائص الـ private لا تشكل أي عائق

وأيضًا إنتهاك لمبدأ الـ Open/Closed
بحيث أنه كلما أضفنا كلاس جديد مثل SubscriptionProduct
كنا مجبرين على تعديل دالة duplicateProduct وإضافة شرط else if جديد
أما الآن فكل كلاس جديد ينفذ clone الخاصة به
دون أي تعديل على أي كود موجود
وهذا هو مبدأ الـ Open/Closed الذي طبقناه عملياً دون أي تعقيد


هل لاحظت شيئاً؟
كل هذه المشاكل حلت بفكرة واحدة بسيطة اجعل كل كلاس مسؤولاً عن استنساخ نفسه

لكن دعني أحذرك من شيء قد يقع فيه الكثيرون

قد تظن أنك بتطبيق الـ Prototype Pattern قد ضمنت الـ Deep Copy تلقائياً
لكن الحقيقة أن الـ Deep Copy مسؤولية كل كلاس على حدة
فإذا كان الـ object يحتوي على تداخل من objects أخرى
فيجب أن تتأكد من أن كل object داخله ينفذ clone أيضاً أو تستطيع أن تحصل على نسخة Deep Copy منه بطريقة أو بأخرى دون أن تقصد


رسالة خاصة

أرسل ملاحظاتك أو رأيك بشكل خاص — لن يظهر للآخرين

التعليقات

شاركنا رأيك في هذه المقالة أو اسأل عن أي شيء يخصها