الـ Factory Method Pattern الدوال المصنعة
السلام عليكم ورحمة الله وبركاته
يمكنك متابعة السلسلة بالترتيب أو الانتقال مباشرة إلى أي مقال:
المقدمة
في مقالة الـ Simple Factory Pattern المصنع البسيط شرحنا الـ Simple Factory Pattern وأنصح بقرأتها أولًا لتستطيع معرفة الفرق بينه وبين الـ Factory Method Pattern الذي سنشرحه اليوم
تعلمنا أن الـ Simple Factory Pattern يركز على جعل إنشاء الـ objects في مكان واحد وغالبًا تكون دالة فيها if-else وترجع الـ object الذي نحتاجه بناءا على شرط ما
لكننا أيضًا وصلنا في نهاية المقال إلى أن لهذا الحل عيبًا واضحًا
فهو ينتهك مبدأ الـ Open/Closed Principle
وكل مرة نحتاج لإضافة نوع جديد، نضطر للعودة وفتح كلاس الـ Factory نفسه وتعديله من جديد
وكلما اختلفت طريقة إنشاء كل object عن الآخر، أو عندما تتعقد عملية الإنشاء بحيث أن إنشاء الـ object يحتاج لعدة خطوات
ستجد أن الكلاس يزيد حجمه وتعقيده حتى يفقد بساطته التي كان يتميز بها في الأساس
ولعل هذا هو سبب افتقاره ليصبح Design Pattern حقيقي يعتمد عليه لأنه يتهالك أمامك ويصبح عقبة عندما تتعقد عملية الإنشاء وتكبر
لأن الـ Design Pattern بطبيعتها صممت للتعامل وتتكيف مع المشاريع الكبيرة والمعقدة
اليوم تستطيع أن تقول أننا سنكمل من حيث توقفنا في الـ Simple Factory Pattern، ونتعرف على حل أشمل لهذه المشكلة، وهو الـ Factory Method Pattern
وعلى عكس الـ Simple Factory Pattern، فالـ Factory Method Pattern هو أحد الأنماط الرسمية الموجودة في كتاب الـ Gang of Four
وهو يقدم طريقة مختلفة تمامًا للتفكير في عملية إنشاء الـ objects
فبدلًا من أن يكون لدينا كلاس واحد يعرف كل تفاصيل إنشاء كل الأنواع، ويقرر أيها سينشئ بناءًا على شرط
سنقوم بتفويض هذه المسؤولية للكلاسات الفرعية نفسها، بحيث يصبح كل كلاس فرعي مسؤولًا عن إنشاء النوع الخاص به فقط
ما المشكلة التي يحلها الـ Factory Method Pattern ؟
تذكر معي المثال الذي ختمنا به المقالة السابقة
كان لدينا أربع قنوات لإرسال الإشعارات Email و SMS و WhatsApp و Push
لكن اكتشفنا أن كل قناة تحتاج طريقة إنشاء مختلفة عن الأخرى
الـ Email لا يحتاج أي بيانات إضافية
الـ SMS يحتاج apiKey
الـ WhatsApp يحتاج token
أما الـ Push فيحتاج إلى عملية إنشاء كاملة من عدة خطوات بحيث نحضر الإعدادات أولًا من ConfigService ثم ننشئ PushClient ونقوم بعمل connect
وبعد ذلك كله نستطيع إنشاء object من الـ PushSender
class EmailSender implements INotificationSender {
public send(to: string, message: string): void {
console.log(`[Email] Sending notification to ${to}: ${message}`);
}
}
class SmsSender implements INotificationSender {
constructor(private apiKey: string) {}
public send(to: string, message: string): void {
console.log(`[SMS] Sending notification to ${to}: ${message}`);
}
}
class WhatsAppSender implements INotificationSender {
constructor(private token: string) {}
public send(to: string, message: string): void {
console.log(`[WhatsApp] Sending notification to ${to}: ${message}`);
}
}
class PushSender implements INotificationSender {
constructor(
private client: PushClient,
private deviceId: string,
) {}
public send(to: string, message: string): void {
console.log(`[Push] Sending notification to ${to}: ${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");
} 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");
}
}
هل ترى المشكلة ؟
دالة create الآن أصبحت تعرف الكثير من التفاصيل عن كل نوع على حدة
تعرف أن الـ SMS يحتاج apiKey، وأن الـ WhatsApp يحتاج token
وتعرف أن الـ Push يحتاج ConfigService و PushClient وعملية connect كاملة
وكل هذه التفاصيل المختلفة أصبحت متراكمة معًا في كلاس واحد
والآن، تخيل معي أننا نريد إضافة قناة جديدة مثل Telegram
سنضطر للعودة إلى هذا الكلاس بالتحديد، وفتحه من جديد، وإضافة شرط جديد بداخله
} else if (type === "telegram") {
return new TelegramSender("TELEGRAM-BOT-TOKEN");
}
وتخيل لو أن خطوات إنشاء object من Email و SMS و WhatsApp تعقدت وأصبحت تحتاج عدة خطوات مثل Push
وهذا بالضبط ما يخالف مبدأ الـ Open/Closed Principle
الذي ينص على أن الكود يجب أن يكون مفتوحًا للإضافة Open for Extension، لكن مغلقًا للتعديل Closed for Modification
بمعنى أنه يجب أن نستطيع إضافة خصائص ومميزات جديدة دون الحاجة لفتح وتعديل كود قديم يعمل بالفعل
والسؤال الذي يجب أن نسأله الآن:
هل يوجد طريقة تجعل كل نوع object مسؤولًا عن معرفة كيفية إنشاء نفسه بكل تفاصيله الخاصة
بحيث عندما نحتاج لإضافة نوع جديد، لا نضطر لفتح أو تعديل أي كود قديم على الإطلاق، بل فقط نضيف كودًا جديدًا ؟
ملحوظة: الفرق الجوهري بينSimple FactoryوFactory Methodهو أن الأول يركز إنشاء كل الأنواع في مكان واحد
بينما الثاني يوزع مسؤولية الإنشاء على كلاسات منفصلة تدعىConcrete Creators
استخدمFactory Methodعندما تختلف طريقة إنشاء كلobjectبشكل كبير، أو عندما تريد تطبيقOpen/Closedبشكل كامل
هنا يأتي دور الـ Factory Method Pattern
ما هو الـ Factory Method Pattern ؟
فكرة الـ Factory Method Pattern تقوم على مبدأ مختلف تمامًا عن الـ Simple Factory Pattern
حتى أن الكود وطريقة التنفيذ سيختلف تمامًا لذا أريدك أن تركز معي قليلًا
حسنًا الفكرة قائمة على أنه بدلًا من أن يكون لدينا كلاس واحد يقرر أي object سننشئه بناءًا على شرط if-else أو switch
بنقل مسؤولية إنشاء الـ object إلى كلاس مستقل مسؤوليته هى إنشاء الـ الـ object
فعلى سبيل المثال في حالتنا لدينا الكلاسات EmailSender و SmsSender و WhatsAppSender و PushSender
هنا نحتاج لكلاس يكون هو المصنع الرئيسي ونسميه الـ Creator يكون من نوع abstract class
ثم نقوم بتعريف دالة بداخله تكون abstract method وهى التي نسميها الـ factory method
الآن نحتاج لإنشاء كلاس لكل نوع EmailSenderCreator و SmsSenderCreator و WhatsAppSenderCreator و PushSenderCreator
وكل كلاس من هؤلاء نسميه Concrete Creator والذي سيقوم بتنفيذ أو وراثة من كلاس الـ Creator الكلاس الأساسي
وينفذ الـ abstract method التي بداخله بشكل اجباري ليرجع الـ object المناسب له فقط
وهنا تظهر الفكرة الأساسية للـ Factory Method Pattern والدالة المتواجدة في الـ abstract class هى المقصودة بكلمة Factory Method
وتكون دائمًا abstract method لترغم باقي المصانع الصغيرة أو الـ Concrete Creators بتنفيذها
ومن هنا يأتي معنى الاسم Factory Method Pattern نفسه بحيث أن هذه الدالة هي التي تصنع أو تنشئ الـ object المطلوب
وهى تفعل ذلك داخل كل Concrete Creator على حدة بدلًا من أن تكون في مكان واحد كما كان في الـ Simple Factory
وهكذا نقوم بتوزيع مسؤولية الإنشاء على عدة مصانع صغيرة، كل واحدة مسؤولة عن نوع واحد فقط
فالـ EmailSenderCreator سيكون مسؤول عن إنشاء object من الـ EmailSender
والـ SmsSenderCreator سيكون مسؤول عن إنشاء object من الـ SmsSender
وهكذا
على أي حال الكلاس الأساسي Creator قد لا ينحصر دوره فقط بإنشاء الـ object
بل قد يحتوي أيضًا على دوال أخرى غير دالة الـ factory method قمثلًا قد يحتوي على دوال مشتركة بداخله تقوم بعمل implementation معين
باستخدام الـ object الناتج من الـ factory method
وسنرى هذا بأمثلة عملية لاحقًا لذا دعونا نطبق هذا على مثالنا لنرى الفكرة بشكل أوضح
التطبيق العملي للـ Factory Method Pattern
أولًا الكلاسات EmailSender و SmsSender و WhatsAppSender و PushSender التي رأيناها بالأعلى لن تتغير
وهى بالفعل تنفذ الـ Interface الموحد INotificationSender لذا سنبقيها كما هى دون تغير
الذي سيتغير هو مكان كتابة كود إنشاء هذه الكلاسات، وهذا ما سنراه الآن
إنشاء الـ Creator
الخطوة الأولى هي تعريف الكلاس الأساسي أو ما يعرف بالـ Creator
وبداخله نعرف الـ factory method كـ abstract، بجانب أي دالة مشتركة نريد تنفيذها باستخدام الـ object الناتج من الـ factory method
abstract class NotificationCreator {
// Factory Method
protected abstract createSender(): INotificationSender;
// Shared Methods (Business Logic)
public notify(to: string, message: string): void {
const sender = this.createSender();
sender.send(to, message);
}
}
لاحظ عدة أمور مهمة هنا
الدالة createSender هي الـ factory method، وهي abstract لأن كل كلاس فرعي سينفذها بطريقته الخاصة
وهي أيضًا protected لأننا لا نريد لأي كود خارجي استدعاءها مباشرة، فهي مخصصة فقط للكلاسات الفرعية أو ما نسميها باالـ Concrete Creators
وهى المصانع الصغيرة التي سترث NotificationCreator المصنع الرئيسي والأساسي
الدالة notify هي التي تحتوي على كود مشترك يتشارك مع أي Concrete Creators بغض النظر عن نوعه
والدالة تستدعي createSender للحصول على الـ sender المناسب، ثم تستخدمه لإرسال الإشعار
دون أن تعرف ما هو نوع هذا الـ sender بالضبط، ودون أن تعرف كيف تم إنشاؤه أو ما التفاصيل التي احتاجها لذلك
لأن تفاصيل الإنشاء ستختزل داخل دالة الـ createSender داخل كل Concrete Creator
إنشاء الـ Concrete Creators
الآن بعد إنشاء الـ Creator الذي اسميناه NotificationCreator والذي يحتوي على دالة الـ createSender التي تمثل الـ factory method
سنقوم بإنشاء الـ Concrete Creators وهى تتمثل بأنه كلاس لكل قناة إشعار يرث من NotificationCreator وينفذ الـ factory method بالطريقته الخاصة
class EmailNotificationCreator extends NotificationCreator {
protected createSender(): INotificationSender {
return new EmailSender();
}
}
class SmsNotificationCreator extends NotificationCreator {
protected createSender(): INotificationSender {
return new SmsSender("SMS-API-KEY");
}
}
class WhatsAppNotificationCreator extends NotificationCreator {
protected createSender(): INotificationSender {
return new WhatsAppSender("WHATSAPP-TOKEN");
}
}
class PushNotificationCreator extends NotificationCreator {
protected createSender(): INotificationSender {
const configService = new ConfigService();
const config = configService.getPushConfig();
const client = new PushClient(config.appId, config.secret);
client.connect();
return new PushSender(client, "DEVICE-ID");
}
}
تذكر معي كيف كانت تفاصيل إنشاء كل object مثل الـ apiKey والـ token والـ ConfigService والـ PushClient وعملية الـ connect متجمعة معًا داخل دالة create واحدة في الـ Simple Factory Pattern
اما الآن أصبحت تفاصيل الإنشاء لكل نوع معزولة تمامًا في كلاس خاص
فمثلًا SmsNotificationCreator هو الوحيد الذي يعرف أن الـ SMS يحتاج apiKey
وكلاس PushNotificationCreator هو الوحيد الذي يعرف تفاصيل الـ ConfigService والـ PushClient وعملية الـ connect
ولا أحد من هذه الكلاسات يعرف شيئًا عن تفاصيل إنشاء الكلاسات الأخرى
بالطبع هذا المثال بسيط
لكن تخيل أن تفاصيل إنشاء object من Email و SMS و WhatsApp تتطلب عدة خطوات مثل Push
في هذه الحالة تعقيد الإنشاء لك نوع ستكون معزولة في كلاس منفصل
استخدام الـ Concrete Creators
بعد أن قمنا بعزل تفاصيل إنشاء كل قناة إشعار داخل الـ Concrete Creator الخاص بها، أصبح بإمكاننا الآن استخدام هذه المصانع الفرعية بشكل مباشر لإنشاء وإرسال الإشعارات
تذكر أن الدالة المشتركة notify متواجدة في الكلاس الأساسي NotificationCreator، وكل ما علينا فعله هو تحديد أي مصنع فرعي نريد استخدامه:
// استخدام مصنع البريد الإلكتروني
const emailCreator: NotificationCreator = new EmailNotificationCreator();
emailCreator.notify("[email protected]", "مرحباً بك في مدونة الطبراني!");
// استخدام مصنع الرسائل النصية
const smsCreator: NotificationCreator = new SmsNotificationCreator();
smsCreator.notify("+201000000000", "كود التحقق الخاص بك هو 1234");
// استخدام مصنع إشعارات الهاتف
const pushCreator: NotificationCreator = new PushNotificationCreator();
pushCreator.notify("DEVICE-ID-12345", "تم شحن طلبك بنجاح!");
لاحظ كيف أننا نتعامل دائمًا مع النوع العام NotificationCreator وهذا طبعًا مفهوم الـ Polymorphism
ولاحظ أنك كـ Client لا تعرف تفاصيل إنشاء كل object
والتفاصيل إنشاء أي object معزولة عنك في كل كلاس
بمعنى أن الخطوات الكثيرة لإنشاء الـ PushSender مختزلة داخل الـ PushNotificationCreator
تطبيق الـ Factory Method Pattern على الامثلة السابقة
الآن لنرى كيف تستخدم كلاسات مثل OrderService و ShipmentService الـ Factory Method Pattern
في الـ Simple Factory Pattern، كانت هذه الكلاسات تستقبل channel: string، وتستخدم NotificationSenderFactory.create(channel) مباشرة للحصول على الـ sender
أما الآن، فهذه الكلاسات لن تتعامل مع channel
بل سنطبق مبدأ الـ Dependency Inversion بحيث أن نجعل الـ Service تعرف object من نوع NotificationCreator في الـ constructor
بالتالي كل Service ستستقبل الـ object الذي يحتاجه من الخارج بدلًا من أن يقوم هو نفسه باختيار أو إنشاء هذا الـ object
class OrderService {
constructor(private notifier: NotificationCreator) {}
public confirmOrder(orderId: number, userContact: string): void {
// كود معالجة الطلب ...
this.notifier.notify(
userContact,
`Your order #${orderId} has been confirmed`,
);
}
}
class ShipmentService {
constructor(private notifier: NotificationCreator) {}
public markAsShipped(shipmentId: number, userContact: string): void {
// كود تحديث حالة الشحنة ...
this.notifier.notify(
userContact,
`Your order #${shipmentId} has been shipped`,
);
}
}
والاستخدام يصبح بهذا الشكل
const orderService = new OrderService(new PushNotificationCreator());
orderService.confirmOrder(101, "DEVICE-ID-XYZ");
const shipmentService = new ShipmentService(
new EmailNotificationCreator(),
);
shipmentService.markAsShipped(202, "[email protected]");
لاحظ أن OrderService لا يحتوي على أي كود يهتم بإنشاء أي object
ولا يهتم بمعرفة بوجود أيًا من كلاسات الـ Concrete Creators
ولا يهتم بإنشاء أي object أو استدعاء أي Factory لجلب أو إنشاء اي object
كل ما يعرفه هو أن لديه notifier من نوع NotificationCreator، وأنه يستطيع استدعاء دالة notify منه عند الحاجة
سؤال مهم قد يطرأ على بالك الآن حسنًا الآن
الآن لدينا على سبيل المثال OrderService و ShipmentService والتي جعلناها تستقبل NotificationCreator لكن السؤال هو من الذي يقرر أي Concrete Creator يجب أن نرسلها للـ OrderService و ShipmentService ؟
هل سنضطر إلى استخدام الـ if-else في مكان ما ليحدد ؟
الإجابة: نعم، لا بد من وجود نقطة واحدة في التطبيق تقرر ذلك
لكن الفرق الجوهري أن هذا القرار سيتخذ في طبقة مختلفة خارج أي Service
فقد تكون في مرحلة الـ Controller أو Setup أو أي نقطة بعيدًا عن الـ Business Logic المتواجد في الـ Service
إضافة قناة إشعار جديدة
ماذا لو أردنا إضافة قناة Slack كقناة جديدة للإشعارات ؟
وكان إنشاء الـ object يتطلب عدة خطوات مثل إعداد webhookUrl وربما channelId ؟
كل ما نحتاجه هو كلاسين جديدين فقط، ولن نلمس أي كود قديم:
class SlackSender implements INotificationSender {
constructor(
private webhookUrl: string,
private channelId: string,
) {}
public send(to: string, message: string): void {
console.log(
`[Slack] Sending notification to ${this.channelId}: ${message}`,
);
}
}
class SlackNotificationCreator extends NotificationCreator {
protected createSender(): INotificationSender {
// تفاصيل الإنشاء معزولة هنا تمامًا
// تفاصيل كثيرة للإنشاء ...
// تفاصيل كثيرة للإنشاء ...
// تفاصيل كثيرة للإنشاء ...
return new SlackSender("https://hooks.slack.com/...", "C123456");
}
}
وهذا كل ما يحتاجه الأمر لاستخدامها
const orderService = new OrderService(new SlackNotificationCreator());
orderService.confirmOrder(303, "slack-chat-id");
لاحظ معي ماذا لم نفعله
لم نفتح كلاس NotificationCreator
لم نفتح أس من الكلاسات الأخرى مثل EmailNotificationCreator أو SmsNotificationCreator أو WhatsAppNotificationCreator أو PushNotificationCreator
ولم نفتح OrderService أو ShipmentService
كل ما فعلناه هو إضافة كود جديد، دون تعديل سطر واحد في أي كود قديم
وهذا بالضبط هو ما يعنيه مبدأ الـ Open/Closed Principle، الذي كان الـ Simple Factory Pattern يخالفه
الفرق بين استخدام Factory Method Pattern والـ Simple Factory Pattern
إذا ما الفروقات أو الفائدة من استخدام Factory Method Pattern ؟
لاحظ أننا مع الـ Simple Factory Pattern كان كل OrderService و ShipmentService أو أي Service أخرى يقوم بنفسه باستدعاء الـ Factory ويرسل له النوع ويستقبل الـ object
فعملية الإنشاء كانت تحدث داخل كل Service
class OrderService {
public confirmOrder(
orderId: number,
channel: string,
userContact: string,
) {
// كود معالجة الطلب ...
const sender = NotificationSenderFactory.create(channel);
sender.send(userContact, `Your order #${orderId} has been confirmed`);
}
}
لاحظ هنا أن الـ OrderService يحتاج ويهتم بمتغير يدعى channel ويهتم ويتعامل مع المصنع NotificationSenderFactory
ويهتم بعملية استدعاء واختيار وإنشاء الـ object داخله
وهذا يخلق عدة مشاكل مثل أن الـ Service أصبحت مربوطة بشكل مباشر بالكلاس NotificationSenderFactory بالتالي فأن الـ OrderService أو أي Service لا يجب أن تكون مجبرة على معرفة كيفية ونوع القناة channel: string وتمريرها لكلاس معين لتحصل على object معين
هذه تفاصيل خارج نطاق مسؤوليتها
ولا داعي لأذكرك بمشاكل الـ Simple Factory Pattern وانتهاكه لمبدأ الـ Open/Closed
وكما رأينا سابقًا أن أي قناة إشعار جديدة نريد إضافتها للمصنع تتطلب فتح NotificationSenderFactory وتعديله
مما يجعل المصنع أو الكلاس يهتم بتفاصيل إنشاء كل object على حدة في نفس الدالة مع شروط if-else
وهذا يعد من أكبر عيوبه لأنه يمنع المشروع من أن يكبر لأنه ليس Scalable أو غير قابل للتوسع
بمعنى أننا لو افترضتنا أننا فريق قمنا بعمل مكتبة NotificationSender بها قنوات إشعارات معينة
تخيل شخص من خارج الفريق جاء يريد تطوير أو إضافة أنواع جديدة من قنوات الإشعارات
لكنه مضطر إلى أن يتواصل مع الفريق الخاص بالمكتبة ليضيفوا تلك الأنواع الجديدة في مكتبتهم داخل الـ NotificationSenderFactory
لأنه لا يستطيع أن يقوم بعملية extends للمكتبة أو ما شابه لأن أي إضافة تحتاج لتعديلات من داخل المكتبة
بسبب أن المكتبة لا تدعم مبدأ الـ Open/Closed لأنها تستعمل Simple Factory Pattern
الآن، انظر كيف تغيرت الصورة تمامًا عند استخدام الـ Factory Method Pattern:
class OrderService {
constructor(private notifier: NotificationCreator) {}
public confirmOrder(orderId: number, userContact: string): void {
// كود معالجة الطلب ...
this.notifier.notify(
userContact,
`Your order #${orderId} has been confirmed`,
);
}
}
لاحظ أن الـ OrderService تماماً. لم يعد يهتم بوجود متغير اسمه channel ولم يعد مربوط بكلاس مصنع محدد
كل ما يطلبه الـ Service الآن هو مجرد object يقوم بتنفيذ الـ interface أو abstract class مثل في حالتنا مع الـ NotificationCreator
وهذا الـ object يتم تمريره إلي الـ OrderService من الخارج عبر الـ Constructor
والـ OrderService لم يعد يهتم بإنشاء أي object أو استدعاء أي Factory
كل ما يعرفه هو أن لديه notifier من نوع NotificationCreator، وأنه يستطيع استدعاء دالة تدعى notify منه عند الحاجة
وهذا يعد تحقيق لمبدأ الـ Dependency Inversion و بسبب هذا وبسبب أنه يطبق مبدأ الـ Open/Closed
وهنا يظهر الفرق الجوهري مع الـ Factory Method Pattern، فهو يعالج مشكلة قابلية التوسع أو الـ Scalable التي تحدثنا عنها والذي سقط فيها الـ Simple Factory
بحيث أنه تخيل أننا سنقوم ببناء مكتبة NotificationSender باستخدام Factory Method Pattern هذه المرة
نحن سنقوم بتوفير الـ interface أو abstract class الأساسي NotificationCreator الذي يحتوي على دالة الـ Factory Method تدعى createSender()
و سنوفر مجموعة من الـ Concrete Creators الجاهزة مثل EmailNotificationCreator و SmsNotificationCreator التي تنفذ هذه الدالة
الآن، ماذا لو أن شخص ما قام بتحميل واستخدام المكتبة وأراد إضافة قناة جديدة مثل Discord ؟
لن يحتاج إلى الاتصال بفريق المكتبة، ولن يطلب منهم أي تعديلات داخلية في مكتبتهم
بل كل ما سيفعله هو أن يكتب كلاسين جديدين داخل مشروعه الخاص بعيدًا عن المكتبة
class DiscordSender implements INotificationSender {
constructor(private webhookUrl: string) {}
public send(to: string, message: string): void {
console.log(
`[Discord] Sending notification to ${this.channelId}: ${message}`,
);
}
}
class DiscordNotificationCreator extends NotificationCreator {
protected createSender(): INotificationSender {
// تفاصيل كثيرة للإنشاء ...
// تفاصيل كثيرة للإنشاء ...
// تفاصيل كثيرة للإنشاء ...
return new DiscordSender("https://discord.com/api/webhooks/...");
}
}
هكذالقد تمكن من استخدام مميزات المكتبة وإضافة كلاس جديد أو مميزات جديدة دون الحاجة للرجوع إلى الفريق الأصلي، أو تعديل أي كود داخل المكتبة
لأن المكتبة صممت على مبدأ Open/Closed بفضل الـ Factory Method Pattern
وهذا بالضبط ما يعنيه أن يكون المشروع Scalable وبالمقارنة مع المشاكل التي رأيناها
سنرى أننا مع الـ Factory Method Pattern، لم تعد الـ Service مربوطة بـ NotificationSenderFactory ولا بـ channel ولا بأي تفاصيل إنشاء لأي object
هي فقط تتعامل مع NotificationCreator الذي يعد abstract class ونطبق فقط مفاهيم الـ Polymorphism ومبدأ الـ Dependency Inversion
ونحقق مبدأ الـ Open/Closed كما رأينا
وكل الآن كل Concrete Creator مختص بنوع واحد فقط، ويعزل تعقيداته عن البقية ويحقق إلى خد ما مبدأ الـ Single Responsibility
بينما كان الـ Simple Factory Pattern يجبرك على التعامل مع كتلة واحدة ضخمة من التفاصيل
الآن مع الـ Factory Method Pattern يمنحك طريقة مرنة تسمح لك بتوسيع المشروع بمميزات وأكواد جديدة دون تعديل أي شيء وكل كود منعزل عن الباقي
لاحظ أنك قد تكتفي بإنشاء كلاس الـ Creator الخاص بالـ Factory Method Pattern وتجهز بيئة العمل
دون الحاجة لإنشاء أي Concrete Creator بعد
وتترك مهمة إنشاء الـ Concrete Creator في ما بعد أو تعطيها لشخص آخر في الفريق أو أنك لا تعرف بعد الـ Concrete Creator التي يجب إنشاءها في الوقت الحالي
حتى أنك يمكنك أن تجعل أي Service أن يستقبل الـ Creator ويتعامل معه على أنه Interface بدون حتى أن يكون لدي أي Concrete Creator بعد في مشروعك
تطبيق الـ Factory Method Pattern على مثال آخر
لتتأكد من فهمك للـ Factory Method Pattern، دعنا نطبقه على مثال آخر من مجال مختلف
تخيل أنك تبني نظامًا لمعالجة المدفوعات يدعم 3 بوابات دفع مختلفة
PayTabsيحتاجsecretKeyHyperPayيحتاجclientIdوclientSecretPaymobيحتاجprofileIdوserverKey
كل بوابة لها طريقة إنشاء مختلفة عن الأخرى
interface IPaymentProcessor {
charge(amount: number, currency: string): void;
}
class PayTabsProcessor implements IPaymentProcessor {
constructor(private secretKey: string) {}
public charge(amount: number, currency: string): void {
console.log(`[PayTabs] Charging ${amount} ${currency}`);
}
}
class HyperPayProcessor implements IPaymentProcessor {
constructor(
private clientId: string,
private clientSecret: string,
) {}
public charge(amount: number, currency: string): void {
console.log(`[HyperPay] Charging ${amount} ${currency}`);
}
}
class PaymobProcessor implements IPaymentProcessor {
constructor(
private profileId: string,
private serverKey: string,
) {}
public charge(amount: number, currency: string): void {
console.log(`[Paymob] Charging ${amount} ${currency}`);
}
}
الآن بدلًا من استخدام Simple Factory Pattern وكتابة if-else لكل بوابة داخل دالة واحدة
نستخدم Factory Method Pattern لتفويض مسؤولية الإنشاء لكل بوابة على حدة
لذا سنقوم بإنشاؤ الـ Creator ولنسميه PaymentGatewayCreator
// Creator
abstract class PaymentGatewayCreator {
// Factory Method
protected abstract createProcessor(): IPaymentProcessor;
public processPayment(amount: number, currency: string): void {
const processor = this.createProcessor();
processor.charge(amount, currency);
}
}
الآن سنقوم بإنشاء كلاس لكل نوع مسؤول عن إنشاء الـ object لهذا النوع بالتفاصيل التي يحتاجها
// Concrete Creator
class PayTabsCreator extends PaymentGatewayCreator {
protected createProcessor(): IPaymentProcessor {
// تفاصيل كثيرة للإنشاء ...
return new PayTabsProcessor("sk_test_4eC39HqLyjWDarjtT1zdp7dc");
}
}
// Concrete Creator
class HyperPayCreator extends PaymentGatewayCreator {
protected createProcessor(): IPaymentProcessor {
// تفاصيل كثيرة للإنشاء ...
return new HyperPayProcessor("client-id", "client-secret");
}
}
// Concrete Creator
class PaymobCreator extends PaymentGatewayCreator {
protected createProcessor(): IPaymentProcessor {
// تفاصيل كثيرة للإنشاء ...
return new PaymobProcessor("profile-id", "server-key");
}
}
وهكذا نكون قد انتهينا والاستخدام يصبح بهذا الشكل
const paymentGateway: PaymentGatewayCreator = new PayTabsCreator();
paymentGateway.processPayment(100, "EGP");
لاحظ أن PaymentGatewayCreator لا يعرف أي شيء عن تفاصيل PayTabs أو HyperPay أو Paymob
ولا يعرف أن PayTabs يحتاج secretKey وأن HyperPay يحتاج clientId و clientSecret
كل هذه التفاصيل معزولة داخل Concrete Creator الخاص بكل بوابة دفع
ولو أردنا إضافة بوابة دفع جديدة مثل Fawry
class FawryProcessor implements IPaymentProcessor {
constructor(private accessToken: string) {}
public charge(amount: number, currency: string): void {
console.log(`[Fawry] Charging ${amount} ${currency}`);
}
}
class FawryCreator extends PaymentGatewayCreator {
protected createProcessor(): IPaymentProcessor {
return new FawryProcessor("fawry-access-token");
}
}
مرة أخرى لم نفتح أي كود قديم، فقط أضفنا كودًا جديدًا
هذا مثال آخر يوضح كيف يستخدم الـ Factory Method Pattern في تطبيقات واقعية مختلفة
ويلخص نفس الفكرة بعزل تفاصيل الإنشاء في كل Concrete Creator خاص بكل نوع
متى لا نستطيع استخدام الـ Factory Method Pattern
بعد كل ما رأيناه، قد تظن أن الـ Factory Method Pattern هو الحل الأمثل لكل مشاكل الإنشاء
وهو بالفعل حل رائع، لكنه يحل مشكلة نوع واحد من المنتجات أو عائلة واحدة من المنتجاتس
لكن ماذا لو كانت مشكلتك أكبر من ذلك ؟
ماذا لو لم تكن تتعامل مع نوع واحد من المنتجات، بل مع عدة أنواع مختلفة يجب أن تعمل معًا ؟
دعنا نأخذ مثالًا، تخيل معي أنك تبني نظامًا لمعالجة المدفوعات، وتريد دعم أكثر من بوابة دفع داخل نظامك
ولنفترض أن لدينا بوابتي دفع هما PayTabs و HyperPay
كل بوابة توفر مجموعة من الخدمات مثل Payment و Refund و Subscription
فبوابة PayTabs سيكون لديها PayTabs Payment و PayTabs Refund و PayTabs Subscription
أما بوابة HyperPay سيكون لديها HyperPay Payment و HyperPay Refund و HyperPay Subscription
لذا نقول أن خدمات PayTabs كلها خدمات من نفس العائلة
وكذلك خدمات HyperPay كلها خدمات من نفس العائلة
ولا يمكننا خلط الخدمات من بوابة مع خدمات من بوابة أخرى، فلا يصح مثلًا أن نستخدم PayTabs Payment مع HyperPay Refund
بل يجب أن نستعمل PayTabs Payment مع PayTabs Refund مع PayTabs Subscription
ونستعمل HyperPay Payment مع HyperPay Refund مع HyperPay Subscription
لأن كل بوابة لها طريقة عملها الخاصة، وقد تعتمد على إعدادات أو بروتوكولات أو بيانات مختلفة عن الأخرى
أو لنأخذ مثالًا آخر، تخيل معي أنك تطور لعبة، ولديك عدة عناصر مثل Character و Weapon و Vehicle
وداخل اللعبة هناك أكثر من عالم أو بيئة، وكل عالم له خصائصه الخاصة،
بمعنى أننا لو افترضنا أن لدينا عالم Medieval وعالم Sci-Fi
عالم الـ Medieval سيكون فيه أنواع من الـ Character مثل Knight و Archer ولديه أنواع من الـ Weapon مثل Sword و Bow
ولديه أنواع من الـ Vehicle مثل Horse و Catapult
أما عالم الـ Sci-Fi سيكون فيه أنواع من الـ Character مثل Space Marine و Alien و Robot
ولديه أنواع من الـ Weapon مثل Laser Gun و Plasma Rifle
ولديه أنواع من الـ Vehicle مثل Spaceship و Hover Bike
لذا نقول أن عالم الـ Medieval مع الـ Knight و الـ Sword و الـ Horse كلها عناصر من نفس العائلة
وكذا عالم الـ Sci-Fi مع الـ Space Marine و الـ Laser Gun و الـ Spaceship كلها عناصر من نفس العائلة
لا يمكننا خلط العناصر من عائلة مع عناصر من عائلة أخرى، لأن كل عائلة لها خصائصها الخاصة وطريقة عملها المختلفة عن الأخرى
هنا الـ Factory Method Pattern ممتاز عندما يكون لدينا نوع واحد فقط من العناصر، أو عائلة واحدة من العناصر، وكل عنصر له طريقة إنشاء مختلفة عن الآخر
أما عندما يصبح لدينا عدة مجموع من العناصر أو الخدمات، وكل مجموعة من الخدمات لها أنواع مختلف بحسب كل عائلات
ويجب أن تعمل جميع عناصر العائلة الواحدة معًا بشكل متناسق، فإن الـ Factory Method Pattern لن يكون كافيًا
لأنه لا يستطيع التعامل مع أكثر من عائلة من العناصر المختلفة، بل يقتصر على عائلة واحدة فقط
في هذه الحالة، نحتاج إلى حل يستطيع التعامل مع أكثر من عائلة من العناصر المختلفة، بحيث نضمن أن كل عنصر من عائلة معينة يعمل مع عناصر العائلة نفسها فقط
وهذا بالضبط ما يقدمه الـ Abstract Factory Pattern، والذي سنتعرف عليه بالتفصيل في المقالة القادمة
الخلاصة
يقوم الـ Factory Method Pattern بحل إحدى أهم مشاكل الـ Simple Factory Pattern، وهي اضطرارك لتعديل كلاس المصنع كلما أضفت نوعًا جديدًا
فبدلًا من وجود مصنع واحد يعرف كيفية إنشاء جميع الـ objects، يتم توزيع مسؤولية الإنشاء على مصانع صغيرة تسمى Concrete Creators، بحيث يصبح كل مصنع مسؤولًا عن نوع واحد فقط
وبهذا حققنا عدة مبادئ دفعة واحدة
حققنا مبدأ الـ Open/Closed Principle لأننا الآن نستطيع إضافة أنواع جديدة بدون أن نلمس أي كود قديم
وحققنا مبدأ الـ Dependency Inversion لأن الـ Services مثل OrderService لم تعد مربوطة بأي مصنع محدد، بل تتعامل فقط مع الـ abstract class العام
وحققنا مبدأ الـ Single Responsibility لأن كل Concrete Creator أصبح مختصًا بنوع واحد فقط، معزولًا عن تفاصيل الأنواع الأخرى
ولعل أهم ما يميز الـ Factory Method Pattern هو أنه يجعل مشروعك قابلًا للتوسع
سواء كنت تبني مشروعًا خاصًا أو مكتبة يستخدمها غيرك، فإن أي شخص يستطيع أن يضيف نوعًا جديدًا بكلاسين فقط دون أن يطلب من أحد فتح أي كود قديم أو تعديله
هذا هو الفرق الجوهري بين حل يعمل، وحل يعمل ويستمر في العمل مهما كبر المشروع وتعقد
التعليقات
شاركنا رأيك في هذه المقالة أو اسأل عن أي شيء يخصها