الـ 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
التعليقات
شاركنا رأيك في هذه المقالة أو اسأل عن أي شيء يخصها