انتقال للمقال
وقت القراءة: ≈ 10 دقائق

الـ Simple Factory Pattern المصنع البسيط

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

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


المقدمة

اليوم سنتعرف على أحد أشهر الأنماط المستخدمة في إنشاء الـ Objects
وهو الـ Simple Factory Pattern

الـ Simple Factory Pattern مختلف قليلًا عن باقي الـ Design Patterns الرسمية
لأنه في الأصل ليس جزءًا من الكتاب الشهير لمجموعة الـ Gang of Four الكتاب الذي وضع أساس كل الـ Design Patterns
لكن مع ذلك فهو مفهوم شائع جدًا وستجده في أغلب المشاريع التي ستتعامل معها
لأنه يقدم طريقة بسيطة لتنظيم عملية إنشاء الـ Objects وإخفاء تفاصيل إنشائها عن بقية أجزاء النظام

وبكل صراحة من كثر ما هو بسيط فستجد أن البعض لا يشرحه بشكل منفرد بل يضمونه مع شرح الـ Factory Method Pattern
وللأسف الكثير من الأشخاص يختلط عليهم الأمر ما بين الـ Simple Factory Pattern و الـ Factory Method Pattern
وبعض المصادر تشرح الـ Simple Factory Pattern على أنه الـ Factory Method Pattern
لكنني أحببت أن اشرحه بشكل منفرد لتوضيح الفروق
وسنبدأ بالـ Simple Factory Pattern

لكن بالطبع قبل أن نبدأ في الشرح، دعونا نتعرف على المشكلة التي يقوم بحلها

ما المشكلة التي يحلها الـ Simple Factory Pattern ؟

تخيل أنك تعمل على نظام إشعارات لمتجر إلكتروني
وهذا النظام يرسل إشعارات للمستخدمين عبر قنوات مختلفة بناءًا على تفضيلاتهم
فقد يفضل البعض استقبال الإشعارات عبر البريد الإلكتروني
وآخرون عبر رسائل SMS
وآخرون عبر WhatsApp
وبعضهم يفضل Push Notifications وهكذا

لذلك قمنا بإنشاء كلاس لكل نوع من أنواع الإشعارات

class EmailSender {
  public send(to: string, message: string): void {
    console.log(`[Email] Sending notification to ${to}: ${message}`);
  }
}

class SmsSender {
  public send(to: string, message: string): void {
    console.log(`[SMS] Sending notification to ${to}: ${message}`);
  }
}

class WhatsAppSender {
  public send(to: string, message: string): void {
    console.log(`[WhatsApp] Sending notification to ${to}: ${message}`);
  }
}

class PushNotificationSender {
  public send(to: string, message: string): void {
    console.log(`[Push] Sending notification to ${to}: ${message}`);
  }
}

جيد، لدينا 4 كلاسات كل منها مسؤول عن إرسال الإشعارات عبر قناة معينة
والآن السؤال: كيف ستستخدم هذه الكلاسات في

في أغلب الأحيان لو لدينا كلاس يدعى OrderService ستجد نفسك تكتب شيئًا كالتالي في كل مكان يحتاج لإرسال إشعار

class OrderService {
  public confirmOrder(
    orderId: number,
    channel: string,
    userContact: string,
  ) {
    // كود معالجة الطلب ...

    // إرسال الإشعار للمستخدم
    const message = `Your order #${orderId} has been confirmed`;

    if (channel === "email") {
      const sender = new EmailSender();
      sender.send(userContact, message);
    } else if (channel === "sms") {
      const sender = new SmsSender();
      sender.send(userContact, message);
    } else if (channel === "whatsapp") {
      const sender = new WhatsAppSender();
      sender.send(userContact, message);
    } else if (channel === "push") {
      const sender = new PushNotificationSender();
      sender.send(userContact, message);
    }
  }
}

ولو لدينا كلاس آخر يدعى ShipmentService يريد ارسال الاشعار ستكرر نفس الكود مرة أخرى

class ShipmentService {
  public markAsShipped(
    shipmentId: number,
    channel: string,
    userContact: string,
  ) {
    // كود تحديث حالة الشحنة ...

    // نفس الكود يتكرر هنا
    const message = `Your order #${shipmentId} has been shipped`;

    if (channel === "email") {
      const sender = new EmailSender();
      sender.send(userContact, message);
    } else if (channel === "sms") {
      const sender = new SmsSender();
      sender.send(userContact, message);
    } else if (channel === "whatsapp") {
      const sender = new WhatsAppSender();
      sender.send(userContact, message);
    } else if (channel === "push") {
      const sender = new PushNotificationSender();
      sender.send(userContact, message);
    }
  }
}

والأمر نفسه مع أي كلاس لديك يريد ارسال اشعار فتخيل لو لديك كلاس ثالث يدعى PaymentService يريد ارسال اشعار عند تأكيد الدفع
وأو كلاس يدعى RefundService يريد ارسال اشعار عند إتمام عملية الاسترداد
وهكذا في كل مكان يحتاج لإرسال إشعار

هل ترى المشكلة ؟

في كل مرة تريد إنشاء كلاس يريد ارسال إشعار ستجد نفسك تكتب نفس الـ if-else فيه
وهذا تكرار وأيضًا لة أردت إضافة قناة جديدة لإرسال الاشعارات فيها مثل Telegram فعليك تعديل كل الأماكن التي كتبت فيها if-else واحدًا تلو الآخر
ولو غيرت اسم أحد الكلاسات أو حذفت أحد الكلاسات ستضطر للبحث والتعديل في كل مكان في المشروع
وأيضًا لا أريد أن انبهك بأنه يخالف مبدأ الـ Single Responsibility Principle لأن الكلاسات أصبحت الآن مسؤولة عن إنشاء object الاشعار بجانب القيام بوظيفتها الأساسية

والحل ؟ أكيد لا داعي لأن أخبرك أن الحل هو الـ Simple Factory Pattern

ملحوظة: الـ Simple Factory Pattern مناسب للمشاريع المتوسطة حيث عدد الأنواع محدود وطريقة إنشائها بسيطة
لكنه يخالف مبدأ Open/Closed لأن إضافة نوع جديد تتطلب تعديل المصنع نفسه
إذا توقع أن عدد الأنواع سيكبر كثيرًا، فكر في Factory Method Pattern منذ البداية

ما هو الـ Simple Factory Pattern ؟

فكرة الـ Simple Factory Pattern بسيطة جدًا
وهي إنشاء دالة أو كلاس وسيط مهمته الوحيدة هي إنشاء الـ Object المناسب بناءًا على نوع أو شرط معين
بدلًا من أن يقوم كل جزء في الكود بإنشاء هذه الـ Objects بنفسه

وهنا يأتي فكرة اسمه كمصنع بسيط يستقبل منك نوع الـ object الذي تريده ويعطيه لك

التطبيق العملي للـ Simple Factory Pattern

لتنفيذ الـ Simple Factory Pattern سنحتاج لتجهيز الكلاسات

إنشاء الـ Interface

أول شيء علينا أن نجعل الكلاسات EmailSender و SmsSender و WhatsAppSender و PushNotificationSender تقوم بتنفيذ Interface
بحيث يكون لدينا شكل موحد وعام والذي يجب أن تكون عليه جميع الكلاسات المختلفة
ليتمكن الـ Simple Factory Pattern بإرجاع أي Object ينفذ الـ interface محققًا مفهوم الـ Polymorphism

لذا الخطوة الأولى هي تعريف الـ Interface الذي سيمثل الشكل العام لكل الكلاسات التي تقوم بإرسال الاشعارات
ولنسميه INotificationSender على سبيل المثال

interface INotificationSender {
  send(to: string, message: string): void;
}

الآن نقوم بجعل كل الكلاسات التي ترسل الاشعارات بتنفيذ هذاالـ Interface

class EmailSender implements INotificationSender {
  public send(to: string, message: string): void {
    console.log(`[Email] Sending notification to ${to}: ${message}`);
  }
}

class SmsSender implements INotificationSender {
  public send(to: string, message: string): void {
    console.log(`[SMS] Sending notification to ${to}: ${message}`);
  }
}

class WhatsAppSender implements INotificationSender {
  public send(to: string, message: string): void {
    console.log(`[WhatsApp] Sending notification to ${to}: ${message}`);
  }
}

class PushNotificationSender implements INotificationSender {
  public send(to: string, message: string): void {
    console.log(`[Push] Sending notification to ${to}: ${message}`);
  }
}

الكلاسات الآن جميعها تنفذ نفس الـ Interface بمعنى أنها أصبحت من عائلة واحدة ونستطيع تنفيذ مفهوم الـ Polymorphism عليها

إنشاء الـ Factory

هنا يأتي الجزء الأساسي للـ Simple Factory Pattern
سننشئ كلاس اسمه NotificationSenderFactory مهمته الوحيدة هي إنشاء الكلاس المناسب بناءًا على القناة المطلوبة

class NotificationSenderFactory {
  public static create(channel: string): INotificationSender {
    if (channel === "email") {
      return new EmailSender();
    } else if (channel === "sms") {
      return new SmsSender();
    } else if (channel === "whatsapp") {
      return new WhatsAppSender();
    } else if (channel === "push") {
      return new PushNotificationSender();
    } else {
      throw new Error(`Unsupported Channel: ${channel}`);
    }
  }
}

لاحظ عدة أمور مهمة هنا

الدالة create هي static لأننا لا نحتاج لإنشاء object من الـ Factory نفسه، فقط نستدعيها مباشرة من اسم الكلاس
والدالة فقط تستقبل نوع الـ channel
وأيضًا الدالة ترجع INotificationSender وليس نوعًا محددًا مثل EmailSender، وهذا يمكننا من التعامل مع أي كلاس ينفذ الـ Interface
بهذا الشكل

const sender: INotificationSender =
  NotificationSenderFactory.create("email");
sender.send("[email protected]", "Vist our awesome blog: eltabarani.com");

ببساطة نحن هكذا نطلب من الـ Factory والمصنع يقوم بإرجاع الـ object المناسب بناءًا على نوع الـ channel
دون أن يعرف أو يهتم بكيفية إنشاءه وهذا بفضل مفهوم الـ Polymorphism أيضًا

استخدام الـ Factory في الكلاسات

أحتاج فقط إلى إنعاش ذاكرتك قليلًا تذكر معي كيف كانت الكلاسات التي عرضتها عليك في بداية المقالة

قبل الـ Simple Factory Pattern

تذكر معي كيف كان يبدو كلاس الـ OrderService

class OrderService {
  public confirmOrder(
    orderId: number,
    channel: string,
    userContact: string,
  ) {
    // كود معالجة الطلب ...

    // إرسال الإشعار للمستخدم
    const message = `Your order #${orderId} has been confirmed`;

    if (channel === "email") {
      const sender = new EmailSender();
      sender.send(userContact, message);
    } else if (channel === "sms") {
      const sender = new SmsSender();
      sender.send(userContact, message);
    } else if (channel === "whatsapp") {
      const sender = new WhatsAppSender();
      sender.send(userContact, message);
    } else if (channel === "push") {
      const sender = new PushNotificationSender();
      sender.send(userContact, message);
    }
  }
}

هل ترى هذا الجزء المزعج من الـ if-else
كنت ستضطر لكتابته في كل كلاس يحتاج إرسال إشعار كنت ستكتب

// OrderService
if (channel === "email") {
  const sender = new EmailSender();
  sender.send(userContact, message);
} else if (channel === "sms") {
  const sender = new SmsSender();
  sender.send(userContact, message);
} else if (channel === "whatsapp") {
  const sender = new WhatsAppSender();
  sender.send(userContact, message);
} else if (channel === "push") {
  const sender = new PushNotificationSender();
  sender.send(userContact, message);
}

// ShipmentService - نفس الكود يتكرر
if (channel === "email") {
  const sender = new EmailSender();
  sender.send(userContact, message);
} else if (channel === "sms") {
  const sender = new SmsSender();
  sender.send(userContact, message);
} else if (channel === "whatsapp") {
  const sender = new WhatsAppSender();
  sender.send(userContact, message);
} else if (channel === "push") {
  const sender = new PushNotificationSender();
  sender.send(userContact, message);
}

// PaymentService - نفس الكود يتكرر مرة ثالثة
if (channel === "email") {
  const sender = new EmailSender();
  sender.send(userContact, message);
} else if (channel === "sms") {
  const sender = new SmsSender();
  sender.send(userContact, message);
} else if (channel === "whatsapp") {
  const sender = new WhatsAppSender();
  sender.send(userContact, message);
} else if (channel === "push") {
  const sender = new PushNotificationSender();
  sender.send(userContact, message);
}

لا احتاج لتوضيح وتكرار بشاعة الكود وأنك تضطر لتكرار الأكواد هنا وهناك مع كل كلاس يريد ارسال اشعار بسيط


الآن لنرى كيف سيبدو الكود في الكلاسات بعد استخدام الـ Factory

بعد الـ Simple Factory Pattern

class OrderService {
  public confirmOrder(
    orderId: number,
    channel: string,
    userContact: string,
  ) {
    // كود معالجة الطلب ...

    const sender = NotificationSenderFactory.create(channel);
    sender.send(userContact, `Your order #${orderId} has been confirmed`);
  }
}

class ShipmentService {
  public markAsShipped(
    shipmentId: number,
    channel: string,
    userContact: string,
  ) {
    // كود تحديث حالة الشحنة ...

    const sender = NotificationSenderFactory.create(channel);
    sender.send(userContact, `Your order #${shipmentId} has been shipped`);
  }
}

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

// OrderService
const sender = NotificationSenderFactory.create(channel);
sender.send(userContact, message);

// ShipmentService - نفس السطر الوحيد
const sender = NotificationSenderFactory.create(channel);
sender.send(userContact, message);

// PaymentService - نفس السطر الوحيد
const sender = NotificationSenderFactory.create(channel);
sender.send(userContact, message);

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

وهكذا حققنا مبدأ الـ Single Responsibility لأن الكلاسات OrderService و ShipmentService وغيرهما ستهتم فقط بتنفيذ ما هو مطلاوب منها ولن ترهق أو تتعب نفسها في الكود الخاص بإنشاء الـ object الخاص بكل اشعار
بل فقط ستقوم باستدعاء NotificationSenderFactory.create لتحصل على الـ object المناسب

إضافة قناة إشعار جديدة

من فوائد الـ Simple Factory Pattern أننا نملك مكان موحد ينشيء لنا الـ object
بالتالي تخيل لو قررنا إضافة قناة إشعار جديدة مثل ارسال الاشعارات عبر Telegram
قبل الـ Simple Factory Pattern كنت ستعدل الـ if-else في كل كلاس على حدة

أما الآن فالأمر يحتاج فقط لإنشاء كلاس جديد ولنسميه TelegramSender ونجعله ينفذ الـ Interface

class TelegramSender implements INotificationSender {
  public send(to: string, message: string): void {
    console.log(`[Telegram] Sending notification to ${to}: ${message}`);
  }
}

ثم نضيف إضافة الـ channel الجديدة في الـ Factory فقط

class NotificationSenderFactory {
  public static create(channel: string): INotificationSender {
    if (channel === "email") {
      return new EmailSender();
    } else if (channel === "sms") {
      return new SmsSender();
    } else if (channel === "whatsapp") {
      return new WhatsAppSender();
    } else if (channel === "push") {
      return new PushNotificationSender();
    } else if (channel === "telegram") {
      return new TelegramSender();
    } else {
      throw new Error(`Unsupported Channel: ${channel}`);
    }
  }
}

لاحظ أننا لم نقم بامس OrderService ولا ShipmentService ولا PaymentService
التغيير كله كان فقط في مكان واحد فقط

متى لا نستطيع استخدام الـ Simple Factory Pattern

برغم من أن الـ Simple Factory Pattern يساعدنا على تحقيق مبدأ الـ Single Responsibility
إلا أنه لديه بعض العيوب مثل أنه ينتهك مبدأ الـ Open/Closed Principle بشكل مباشر

بحيث أنك كل مرة تريد إضافة قناة إشعار جديد تضطر لفتح كلاس الـ Factory وتعديله
وهذا أحد الفروق بين الـ Simple Factory والـ Factory Method Pattern الذي سنتحدث عنه لاحقًا
حيث أن الـ Factory Method Pattern يعد حل أكبر وأشمل من الـ Simple Factory Pattern
ويحقق أيضًا مبدأ الـ Open/Closed Principle


لكن هذا لا يعني أن الـ Simple Factory Pattern سيء ويجب ألا نستخدمه أو يجب دائمًا أن نستخدم الـ Factory Method Pattern عوضًا عنه
كلا، بل ما يميز الـ Simple Factory Pattern أنه بسيط جدًا ومشكلة أنه لا يحقق الـ Open/Closed ليست نهائة العالم ونستطيع التكيف والتعامل مع الأمر دون مشاكل في معظم الحالات والمشاريع

ولعل أحد أهم أسباب أن الـ Simple Factory Pattern لا يتم الاعتراف به كـ Design Pattern
أنه ليس مناسبًا عندما تبدأ عملية الإنشاء نفسها تختلف بشكل واضح من نوع إلى نوع، بمعنى أن عملية الإنشاء لا تكون ببساطة إنشااء Object فقط
أو عندما يصبح المصنع مليئًا بالشروط والتفرعات حتى يفقد بساطته

فمثلًا تخيل لو أن الـ Email يحتاج فقط إنشاء كلاس بسيط
أما الـ SMS يحتاج apiKey والـ WhatsApp يحتاج token والـ Push يحتاج deviceId

هنا Simple Factory Pattern لن يكون بسيطصا في هذه الحالة بل سيصبح مزدحمًا

class EmailSender implements INotificationSender {
  public send(message: string): void {
    console.log(`Email: ${message}`);
  }
}

class SmsSender implements INotificationSender {
  constructor(private apiKey: string) {}

  public send(message: string): void {
    // Checking API Key ...
    console.log(`SMS: ${message}`);
  }
}

class WhatsAppSender implements INotificationSender {
  constructor(private token: string) {}

  public send(message: string): void {
    // Checking token ...
    console.log(`WhatsApp: ${message}`);
  }
}

الآن لو حاولنا استخدام الـ Simple Factory Pattern:

class NotificationSenderFactory {
  public static create(type: string): INotificationSender {
    if (type === "email") {
      return new EmailSender();
    } else if (type === "sms") {
      return new SmsSender("SMS-API-KEY");
    } else if (type === "whatsapp") {
      return new WhatsAppSender("WHATSAPP-TOKEN");
    }

    throw new Error("Unsupported type");
  }
}

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

class TelegramSender {
  constructor(private botToken: string) {}

  public send(message: string): void {
    console.log(`Telegram using ${this.botToken}: ${message}`);
  }
}

وسنضطر لتعديل المصنع:

class NotificationSenderFactory {
  public static create(type: string): any {
    if (type === "email") {
      return new EmailSender();
    } else if (type === "sms") {
      return new SmsSender("SMS-API-KEY");
    } else if (type === "whatsapp") {
      return new WhatsAppSender("WHATSAPP-TOKEN");
    } else if (type === "telegram") {
      return new TelegramSender("TELEGRAM-BOT-TOKEN");
    }

    throw new Error("Unsupported type");
  }
}

هنا ستبدأ المشاكل بالظهور وسترى أن الـ Simple Factory Pattern لا يعتمد عليه فعندما يكبر المشروع ويزداد نعقيدًا ستجده عائقًا أمامك ولا يساعدك


والتعقيد في الإنشاء ليس بالضرورة أن يكون بيانات تمررها في الـ constructor بل ممكن أن تكون عملية الإنشاء تحتاج لعدة خطوات أخرى
فمثلًا بعد إنشاء الـ object من كلاس معين تعديل لتمريره لكلاسات ودوال معينة واستدعاء دوال أخرى ليكون جاهزًا

class NotificationSenderFactory {
  public static create(type: string): INotificationSender {
    if (type === "email") {
      return new EmailSender();
    } else if (type === "sms") {
      return new SmsSender("SMS-API-KEY");
    } else if (type === "whatsapp") {
      return new WhatsAppSender("WHATSAPP-TOKEN");
    } else if (type === "push") {
      const configService = new ConfigService();
      const config = configService.getPushConfig();

      const client = new PushClient(config.appId, config.secret);
      client.connect();

      return new PushSender(client, "DEVICE-ID");
    }

    throw new Error("Unsupported type");
  }
}

فتخيل معي هنا أن يعرف أن Push يحتاج لتفاصيل وتهيئة معينة ليكون جاهزًا
فمثلًا هنا لكي ننشيء object من الـ PushSender نحتاج لـ PushConfig الذي نحصل عليه من الـ ConfigService

وهذا يجعل الـ Factory مرتبطًا بتفاصيل كثيرة جدًا، وكلما زاد عدد الأنواع زاد حجم المصنع وتعقيده

بالطبع أنا أضرب أمثله قد لا تكون مثالية، لكن الغرض منها أن أوصل لك الفكرة العامة لا أكثر
وهى أن Simple Factory Pattern سيبدأ بالاهتمام بتفاصيل كثيرة مختلفة لكيفية إنشاء كل Object
فتخيل أن كل نوع يحتاج لتفاصيل وعدة خطوات مثل ما فعلنا مع الـ PushSender

الخلاصة

في هذه المقالة تعرفنا على الـ Simple Factory Pattern، أبسط وأشهر طرق تنظيم إنشاء الـ Objects
بدأنا بمشكلة تكرار نفس كود الـ if-else لإنشاء المرسلات في كل كلاس مثل OrderService و ShipmentService
ثم طبقنا Simple Factory Pattern بإنشاء NotificationSenderFactory الذي يركز إنشاء جميع الأنواع في مكان واحد

لقد تعلمنا في هذه المقالة:

  • المشكلة التي يحلها: تكرار كود إنشاء الـ Objects في عدة أماكن، وانتهاك مبدأ Single Responsibility
  • الفكرة الأساسية: كلاس أو دالة وسيطة مهمتها الوحيدة إنشاء الـ Object المناسب بناءًا على شرط معين
  • الفوائد: مركزة كود الإنشاء، إخفاء تفاصيل الإنشاء عن بقية الكلاسات، سهولة إضافة أنواع جديدة
  • العيوب: يخالف مبدأ Open/Closed لأن إضافة نوع جديد تتطلب تعديل المصنع نفسه

الـ Simple Factory Pattern بسيط وعملي، ولا بأس في استخدامه في معظم المشاريع
لكن تابع حجم المصنع، وعندما يبدأ بالانتفاخ بالشروط والتفاصيل، استخدم Factory Method Pattern بدلًا منه

في المقالة القادمة سنتعرف على الـ Factory Method Pattern وكيف يحل مشكلة التوسع التي يعاني منها Simple Factory


رسالة خاصة

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

التعليقات

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