الـ Abstract Factory Pattern مصنع المصانع
السلام عليكم ورحمة الله وبركاته
يمكنك متابعة السلسلة بالترتيب أو الانتقال مباشرة إلى أي مقال:
المقدمة
في مقالة الـ Factory Method Pattern تفويض الإنشاء للكلاسات الفرعية تعرفنا على الـ Factory Method Pattern وكيف يوزع مسؤولية إنشاء الـ objects على كلاسات فرعية مستقلة تسمى Concrete Creators
وتعلمنا أنه يحل مشكلة انتهاك مبدأ الـ Open/Closed Principle التي كانت تعاني منها الـ Simple Factory Pattern
لكن في نهاية تلك المقالة تركنا سؤالًا مفتوحًا
ماذا لو كانت مشكلتك أكبر من نوع واحد من المنتجات ؟
ماذا لو كنت تتعامل مع عدة أنواع مختلفة يجب أن تعمل معًا كعائلة واحدة ؟
للإجابة على هذا السؤال، دعنا نأخذ مثالين جديدين لنرى المشكلة بشكل أوسع
اليوم سنتعرف على هذه المشكلة بالتفصيل ونعرف كيف نحلها مع الـ Abstract Factory Pattern
ما المشكلة التي يحلها الـ Abstract Factory Pattern ؟
تخيل أنك تبني نظامًا لمعالجة المدفوعات، وتريد دعم أكثر من بوابة دفع
ولنفترض أن لدينا ثلاث بوابات دفع هي PayTabs و HyperPay و Paymob
وكل بوابة توفر خدمات مختلفة مثل الـ Payment و Refund و Subscription تكون متوافقة مع بعضها البعض
| البوابة | الـ Payment | الـ Refund | الـ Subscription |
|---|---|---|---|
| PayTabs | PayTabsPayment |
PayTabsRefund |
PayTabsSubscription |
| HyperPay | HyperPayPayment |
HyperPayRefund |
HyperPaySubscription |
| Paymob | PaymobPayment |
PaymobRefund |
PaymobSubscription |
وهنا تظهر المشكلة
لا يمكننا خلط الخدمات من بوابة مع خدمات من بوابة أخرى
فلا يمكننا أن نستخدم خدمة الـ payment من PayTabs مع خدمة الـ refund من HyperPay
أو أن نخلط بين خدمة الـ subscription من HyperPay مع خدمة الـ payment من Paymob
ونفس المشكلة يمكننا تخيلها مع مثال آخر كمثال لعبة حيث لدينا أكثر من عالم أو بيئة مثل Medieval و Sci-Fi و Cyberpunk
وكل عالم يملك أنواع معينة من الـ Character و Weapon و Vehicle تكون متوافقة مع العالم نفسه
عالم الـ Medieval سيكون فيه أنواع من الـ Character مثل Knight و Archer
ولديه أنواع من الـ Weapon مثل Sword و Bow
ولديه أنواع من الـ Vehicle مثل Horse و Catapult
أما عالم الـ Sci-Fi سيكون فيه أنواع من الـ Character مثل Space Marine و Alien
ولديه أنواع من الـ Weapon مثل Laser Gun و Plasma Rifle
ولديه أنواع من الـ Vehicle مثل Spaceship و Hover Bike
وعالم الـ Cyberpunk سيكون فيه أنواع من الـ Character مثل Hacker و Cyborg
ولديه أنواع من الـ Weapon مثل Railgun و CyberBlade
ولديه أنواع من الـ Vehicle مثل CyberBike و HoverCar
لكل عالم، هذه العناصر الثلاثة يجب أن تعمل معًا بشكل متناسق
| العالم | الـ Character | الـ Weapon | الـ Vehicle |
|---|---|---|---|
| Medieval | Knight |
Sword |
Horse |
| Sci-Fi | Space Marine |
Laser Gun |
Spaceship |
| Cyberpunk | Hacker |
Railgun |
CyberBike |
وهنا أيضًا لا يمكننا خلط العناصر من عالم مع عناصر من عالم آخر
فلا يمكننا أن نخلط بين Knight من عالم Medieval مع Railgun من عالم Cyberpunk
أو أن نخلط بين Hacker من عالم Cyberpunk مع Horse من عالم Medieval
لماذا لا يكفي الـ Factory Method Pattern هنا ؟
يستطيع الـ Factory Method Pattern أن يتعامل مع نوع واحد أو عائلة واحدة من المنتجات
لذا فهو يخبرنا بأن نصنع مصنعًا منفصلًا لكل نوع
لكن المشكلة هنا أننا نملك ثلاثة أنواع من الخدمات وهي الـ Payment و Refund و Subscription
ولدينا ثلاث بوابات مختلفة PayTabs و HyperPay و Paymob
هكذا إذا استخدمنا الـ Factory Method Pattern، سنحتاج إلى مصنع منفصل لكل خدمة في كل بوابة
فمثلًا بوابة PayTabs ستحتاج PayTabsPaymentCreator لإنشاء Payment، و PayTabsRefundCreator لإنشاء Refund، و PayTabsSubscriptionCreator لإنشاء Subscription
وبوابة HyperPay ستحتاج HyperPayPaymentCreator و HyperPayRefundCreator و HyperPaySubscriptionCreator
وبوابة Paymob ستحتاج PaymobPaymentCreator و PaymobRefundCreator و PaymobSubscriptionCreator
| البوابة | Payment Creator Class | Refund Creator Class | Subscription Creator Class |
|---|---|---|---|
| PayTabs | PayTabsPaymentCreator |
PayTabsRefundCreator |
PayTabsSubscriptionCreator |
| HyperPay | HyperPayPaymentCreator |
HyperPayRefundCreator |
HyperPaySubscriptionCreator |
| Paymob | PaymobPaymentCreator |
PaymobRefundCreator |
PaymobSubscriptionCreator |
أي أننا سنحتاج 9 مصانع لـ 3 بوابات فقط
ومع كل بوابة جديدة، نضيف 3 مصانع إضافية
وحتى مع هذه المصانع الـ 9 فلدينا مشكلة أخرى وهى أنه لا يوجد أي ضمان بأن الخدمات التي نحصل عليها من هذه المصانع الـ 3 المنفصلة تنتمي لنفس البوابة
ولنوضح ذلك أريدك أن تنظر معي إلى الكود التالي:
const paymentCreator = new PayTabsPaymentCreator();
const refundCreator = new HyperPayRefundCreator();
const subscriptionCreator = new PaymobSubscriptionCreator();
const payment = paymentCreator.createPayment(); // PayTabsPayment from PayTabs
const refund = refundCreator.createRefund(); // HyperPayRefund from HyperPay
const subscription = subscriptionCreator.createSubscription(); // PaymobSubscription from Paymob
هل ترى المشكلة؟
الكود في الحقيقة لن يرمي لك أي Exception أو Error، لكنه خطأ من ناحية المنطق
بحيث أننا حصلنا على خدمة الـ payment من بوابة PayTabs مع خدمة الـ refund من بوابة HyperPay مع خدمة الـ subscription من بوابة Paymob
هنا حصل خلط بين خدمات مختلفة تنتمي لثلاث بوابات مختلفة
ولا يوجد شيء يساعدنا أو ينبهنا على هذا الخطأ، لأن كل مصنع يعمل بشكل مستقل عن الآخر
لهذا السبب نحتاج إلى حل يضمن لنا أن كل الخدمات التي نحصل عليها من المصانع المختلفة تنتمي لنفس البوابة
هنا بالضبط يأتي دور الـ Abstract Factory Pattern
ملحوظة: المشكلة هنا ليست في إنشاء الـobjectsبحد ذاتها، بل في ضمان توافقها
الـAbstract Factory Patternمصمم خصيصًا لهذا الغرض، بجعل كل مصنع مسؤولًا عن إنشاء عائلة كاملة ومتوافقة من المنتجات ويمنعك تمامًا من الخلط بينها
ملحوظة: عندما نتحدث عن عائلة من المنتجات، فإننا نعني مجموعة من المنتجات أو الخدمات التي يجب أن تعمل معًا بشكل متناسق
وعندما أستخدم كلمة منتج أو عنصر أو خدمة فأنا أعني نفس الشيء، مجرد اختلاف في المصطلحات حسب السياق
فعلى سبيل المثال، في مثال المدفوعات لدينا خدمات مثلPaymentوRefundوSubscription
وفي مثال اللعبة لدينا عناصر مثلCharacterوWeaponوVehicle
كلها مجموعة من المنتجات والأمور التي يجب أن تعمل معًا بشكل متناسق، وكل مجموعة منها تنتمي لعائلة واحدة
ما هو الـ Abstract Factory Pattern ؟
فكرة الـ Abstract Factory Pattern تقوم على مبدأ واحد محوري
وهو أنه بدلًا من أن يكون لدينا مصانع منفصلة لكل نوع من المنتجات
ننشئ مصنعًا واحدًا يعرف كيف ينشئ مجموعة كاملة من المنتجات المترابطة دفعة واحدة
في حالتنا، مصنع بوابة PayTabs سيكون مسؤولًا عن إنشاء PayTabsPayment و PayTabsRefund و PayTabsSubscription معًا
ومصنع بوابة HyperPay سيكون مسؤولًا عن إنشاء HyperPayPayment و HyperPayRefund و HyperPaySubscription معًا
ومصنع بوابة Paymob سيكون مسؤولًا عن إنشاء PaymobPayment و PaymobRefund و PaymobSubscription معًا
تذكر مشكلة الـ 9 مصانع التي واجهناها مع الـ Factory Method؟
مع الـ Abstract Factory، كل ما سنحتاجه هو 3 مصانع فقط مصنع واحد لكل بوابة
وكل مصنع بداخله يحتوي على دوال Factory Methods لإنشاء جميع خدمات هذه البوابة
| البوابة | Concrete Factory | Factory Methods |
|---|---|---|
| PayTabs | PayTabsFactory |
createPayment()، createRefund()، createSubscription() |
| HyperPay | HyperPayFactory |
createPayment()، createRefund()، createSubscription() |
| Paymob | PaymobFactory |
createPayment()، createRefund()، createSubscription() |
هنا مثلًا سيكون لدينا مصنع PayTabsFactory بداخله ثلاث دوال Factory Methods
وهى createPayment() لإنتاج PayTabsPayment، و createRefund() لإنتاج PayTabsRefund، و createSubscription() لإنتاج PayTabsSubscription
هذا المصنع الذي ينشئ عائلة متكاملة نسميه Concrete Factory
وجميع هذه المصانع ستنفذ نفس الـ interface الذي سنسميه Abstract Factory
بهذا الشكل يصبح من المستحيل أن تطلب PayTabsPayment من مصنع HyperPay
لأن مصنع HyperPay مصمم لإعطائك فقط خدمات بوابة HyperPay لا غير
التطبيق العملي للـ Abstract Factory Pattern
حسنًا، بعد أن فهمنا الفكرة النظرية للـ Abstract Factory Pattern حان وقت التطبيق العملي
سنستخدم مثال المدفوعات حيث لدينا ثلاث بوابات PayTabs و HyperPay و Paymob
وكل بوابة تحتاج إلى ثلاثة خدمات Payment و Refund و Subscription
وكل خدمة من الثللاثة طريق تنفيذها يختلف بحسب البوابة التي تنتمي إليها
سنبدأ بتعريف الـ interface للخدمات مثل IPayment و IRefund و ISubscription
ثم نبني كلاسات الـ Concrete لهذه الخدمات بحسب كل بوابة
وأخيرًا نعرف الـ Abstract Factory Interface ونقوم بعمل المصانع التي ستنفذ هذا الـ Abstract Factory Interface والتي تسمى بالـ Concrete Factories أو Concrete Abstract Factories
هكذا سيكون لدينا ثلاث مصانع، مصنع لكل بوابة وكل مصنع هو كلاس يضم مجموعة من الـ Factory Methods
كل Factory Method سيكون مسؤول عن إنشاء الخدمة المناسبة سواء Payment أو Refund أو Subscription وفقًا لنوع البوابة
دعنا نبدأ خطوة بخطوة
إنشاء الـ Interfaces
الخطوة الأولى هي تعريف الـ interfaces لكل نوع من الخدمات وفي حالتنا سنحتاج لثلاثة
واحدة للـ Payment وواحدة للـ Refund وواحدة للـ Subscription
interface IPayment {
pay(amount: number): string;
}
interface IRefund {
refund(transactionId: string): void;
}
interface ISubscription {
subscribe(plan: string): string;
}
إنشاء الـ Concrete Products
الآن نقوم بإنشاء الكلاسات التي تنفذ هذه الـ interfaces
ونقسمها حسب البوابات الثلاث
بما أننا نريد أن نوضح الفكرة، سنقوم بإنشاء كلاسات بسيطة لكل خدمة في كل بوابة
وبما أننا نمتلك ثلاث بوابات، سننشئ ثلاثة كلاسات لكل نوع من الخدمات
بمعنى أننا سننشئ PayTabsPayment و PayTabsRefund و PayTabsSubscription لبوابة PayTabs
و HyperPayPayment و HyperPayRefund و HyperPaySubscription لبوابة HyperPay
و PaymobPayment و PaymobRefund و PaymobSubscription لبوابة Paymob
عائلة الـ PayTabs:
class PayTabsPayment implements IPayment {
public pay(amount: number): string {
console.log(`[PayTabs] Processing payment of $${amount}`);
return `paytabs_txn_${Date.now()}`;
}
}
class PayTabsRefund implements IRefund {
public refund(transactionId: string): void {
console.log(`[PayTabs] Refunding transaction ${transactionId}`);
}
}
class PayTabsSubscription implements ISubscription {
public subscribe(plan: string): string {
console.log(`[PayTabs] Creating ${plan} subscription`);
return `paytabs_sub_${Date.now()}`;
}
}
عائلة الـ HyperPay:
class HyperPayPayment implements IPayment {
public pay(amount: number): string {
console.log(`[HyperPay] Processing payment of $${amount}`);
return `hyperpay_txn_${Date.now()}`;
}
}
class HyperPayRefund implements IRefund {
public refund(transactionId: string): void {
console.log(`[HyperPay] Refunding transaction ${transactionId}`);
}
}
class HyperPaySubscription implements ISubscription {
public subscribe(plan: string): string {
console.log(`[HyperPay] Creating ${plan} subscription`);
return `hyperpay_sub_${Date.now()}`;
}
}
عائلة الـ Paymob:
class PaymobPayment implements IPayment {
public pay(amount: number): string {
console.log(`[Paymob] Processing payment of $${amount}`);
return `paymob_txn_${Date.now()}`;
}
}
class PaymobRefund implements IRefund {
public refund(transactionId: string): void {
console.log(`[Paymob] Refunding transaction ${transactionId}`);
}
}
class PaymobSubscription implements ISubscription {
public subscribe(plan: string): string {
console.log(`[Paymob] Creating ${plan} subscription`);
return `paymob_sub_${Date.now()}`;
}
}
الآن لدينا جميع الخدمات التي نحتاجها لكل بوابة
كل خدمة تنتمي لبوابتها الخاصة، وكل بوابة لديها خدماتها الخاصة بها
الآن نحتاج لتجميع هذه الخدمات في مصنع واحد لكل بوابة
بمعنى أننا سننشئ مصنع لبوابة الـ PayTabs وسيحتوي على دوال لإنشاء PayTabsPayment و PayTabsRefund و PayTabsSubscription
و مصنع لبوابة الـ HyperPay وسيحتوي على دوال لإنشاء HyperPayPayment و HyperPayRefund و HyperPaySubscription
و مصنع لبوابة الـ Paymob وسيحتوي على دوال لإنشاء PaymobPayment و PaymobRefund و PaymobSubscription
إنشاء الـ Abstract Factory
قبل إنشاء المصانع، علينا أولًا إنشاء الـ Abstract Factory Interface الذي سيحدد الدوال التي يجب أن تنفذها جميع المصانع
لذا نقوم بتعريف interface يمثل المصنع الذي يستطيع إنشاء عائلة كاملة من الخدمات
// Abstract Factory Interface
interface IPaymentFactory {
createPayment(): IPayment;
createRefund(): IRefund;
createSubscription(): ISubscription;
}
لاحظ عدة أمور مهمة هنا
الـ interface يحتوي على ثلاث دوال
واحدة لكل نوع من الخدمات
وأي مصنع ينفذ هذا الـ interface مجبور على تنفيذ الثلاثة معًا
وهذا بالضبط ما يضمن لك أن أي مجموعة من الخدمات ستكون متوافقة مع بعضها البعض
إنشاء الـ Concrete Factories
بعد أن أصبح لدينا الـ interface الخاص بالـ Abstract Factory
الآن سننشئ مصنع لكل بوابة، كل مصنع سيكون مسؤول عن إنشاء الخدمات المتوافقة من بوابته فقط
هذه المصانع هى ما تسمى بالـ Concrete Abstract Factories
مصنع بوابة PayTabs:
// Concrete Abstract Factory
class PayTabsFactory implements IPaymentFactory {
public createPayment(): IPayment {
return new PayTabsPayment();
}
public createRefund(): IRefund {
return new PayTabsRefund();
}
public createSubscription(): ISubscription {
return new PayTabsSubscription();
}
}
مصنع بوابة HyperPay:
// Concrete Abstract Factory
class HyperPayFactory implements IPaymentFactory {
public createPayment(): IPayment {
return new HyperPayPayment();
}
public createRefund(): IRefund {
return new HyperPayRefund();
}
public createSubscription(): ISubscription {
return new HyperPaySubscription();
}
}
مصنع بوابة Paymob:
// Concrete Abstract Factory
class PaymobFactory implements IPaymentFactory {
public createPayment(): IPayment {
return new PaymobPayment();
}
public createRefund(): IRefund {
return new PaymobRefund();
}
public createSubscription(): ISubscription {
return new PaymobSubscription();
}
}
الجميل هنا أن كل مصنع يعرف فقط ما يخصه
فكلاس PayTabsFactory يعرف فقط PayTabsPayment و PayTabsRefund و PayTabsSubscription
ولا يعرف أي شيء عن HyperPayPayment أو PaymobRefund
وهذا تحقيق لمبدأ الـ Single Responsibility كل مصنع مسؤول عن عائلة واحدة فقط
لاحظ الفرق الجوهري بين الـ Abstract Factory والـ Factory Method هنا
مع الـ Factory Method كنا سنحتاج 9 مصانع منفصلة وهى PayTabsPaymentCreator و PayTabsRefundCreator و PayTabsSubscriptionCreator و HyperPayPaymentCreator... إلخ
وكل مصنع مسؤول عن نوع واحد فقط
أما مع الـ Abstract Factory، لدينا 3 مصانع فقط مصنع لكل بوابة
وكل مصنع بداخله يحتوي على 3 من دوال الـ Factory Methods
وهى createPayment() و createRefund() و createSubscription() لإنشاء جميع خدمات هذه البوابة
وكما قلنا أن الـ Abstract Factory ما هو إلا مصنع يجمع عدة Factory Methods معًا ليضمن أن جميع الخدمات التي تخرج منه من نفس البوابة
استخدام الـ Abstract Factory
الآن لنرى كيف تستخدم هذه المصانع في التطبيق العملي
سننشئ PaymentService يستقبل الـ factory من الخارج ويستخدمه لإنشاء العائلة المطلوبة
class PaymentService {
private payment: IPayment;
private refund: IRefund;
private subscription: ISubscription;
constructor(factory: IPaymentFactory) {
// المصنع يضمن أن الثلاثة من نفس البوابة دائمًا
this.payment = factory.createPayment();
this.refund = factory.createRefund();
this.subscription = factory.createSubscription();
}
public processOrder(amount: number, plan: string): void {
const txnId = this.payment.pay(amount);
const subId = this.subscription.subscribe(plan);
console.log(`Order completed: ${txnId}, Subscription: ${subId}`);
}
public cancelOrder(txnId: string): void {
this.refund.refund(txnId);
}
}
لاحظ أن PaymentService لا يعرف شيئًا عن PayTabs أو HyperPay أو Paymob
ولا يعرف الفرق بين PayTabsPayment و HyperPayPayment و PaymobPayment
كل ما يعرفه هو أن لديه factory يعطيه payment و refund و subscription متوافقين مع بعضهم
الخطأ الذي رأيناه سابقًا أصبح مستحيلًا الآن
// Use PayTabsFactory
const paytabsService = new PaymentService(new PayTabsFactory());
paytabsService.processOrder(99.99, "premium");
// [PayTabs] Processing payment of $99.99
// [PayTabs] Creating premium subscription
// Order completed: paytabs_txn_..., Subscription: paytabs_sub_...
// Use HyperPayFactory
const hyperpayService = new PaymentService(new HyperPayFactory());
hyperpayService.processOrder(49.99, "basic");
// [HyperPay] Processing payment of $49.99
// [HyperPay] Creating basic subscription
// Order completed: hyperpay_txn_..., Subscription: hyperpay_sub_...
// Use PaymobFactory
const paymobService = new PaymentService(new PaymobFactory());
paymobService.processOrder(500, "enterprise");
// [Paymob] Processing payment of $500
// [Paymob] Creating enterprise subscription
// Order completed: paymob_txn_..., Subscription: paymob_sub_...
لاحظ البساطة في الاستخدام
كل ما نفعله هو تبديل الـ factory الذي نمرره سواء PayTabsFactory أو HyperPayFactory أو PaymobFactory
وكل شيء آخر يعمل بشكل صحيح تلقائيًا
والأهم من ذلك أنه لم يعد بإمكانك أن تخلط عن طريق الخطأ بين PayTabsPayment و HyperPayRefund
لأنك لا تنشئ هذه الـ objects منفردة، بل تطلبها معًا من مصنع واحد يضمن توافقها
تطبيق الـ Abstract Factory Pattern على مثال آخر
لتأكيد فهمك، دعنا نطبق الفكرة على مثال اللعبة
تخيل أنك تطور لعبة، ولدينا أكثر من عالم أو بيئة مثل Medieval و Sci-Fi و Cyberpunk
وكل عالم يملك أنواع معينة من الـ Character و Weapon و Vehicle تكون متوافقة مع العالم نفسه
- عالم
MedievalلديهKnightوSwordوHorse - عالم
Sci-FiلديهSpace MarineوLaser GunوSpaceship - عالم
CyberpunkلديهHackerوRailgunوCyberBike
أولًا نعرف الـ interfaces للعناصر الثلاثة
interface ICharacter {
attack(): void;
}
interface IWeapon {
use(): void;
}
interface IVehicle {
ride(): void;
}
الآن نعرف الكلاسات التي تنفذ هذه الـ interfaces لكل عالم
عائلة الـ Medieval:
class Knight implements ICharacter {
public attack(): void {
console.log("[Knight] Attacks with sword!");
}
}
class Sword implements IWeapon {
public use(): void {
console.log("[Sword] Slashes through the character!");
}
}
class Horse implements IVehicle {
public ride(): void {
console.log("[Horse] Gallops across the battlefield!");
}
}
عائلة الـ Sci-Fi:
class SpaceMarine implements ICharacter {
public attack(): void {
console.log("[Space Marine] Fires plasma rifle!");
}
}
class LaserGun implements IWeapon {
public use(): void {
console.log("[Laser Gun] Shoots laser beams!");
}
}
class Spaceship implements IVehicle {
public ride(): void {
console.log("[Spaceship] Flies through space!");
}
}
عائلة الـ Cyberpunk:
class Hacker implements ICharacter {
public attack(): void {
console.log("[Hacker] Launches a cyber attack!");
}
}
class Railgun implements IWeapon {
public use(): void {
console.log("[Railgun] Fires an electromagnetic projectile!");
}
}
class CyberBike implements IVehicle {
public ride(): void {
console.log("[CyberBike] Speeds through the neon streets!");
}
}
الآن نعرف الـ Abstract Factory Interface الذي يحدد الدوال التي يجب أن تنفذها جميع المصانع
// Abstract Factory Interface
interface IGameFactory {
createCharacter(): ICharacter;
createWeapon(): IWeapon;
createVehicle(): IVehicle;
}
ثم أخيرًا نعرف المصانع لكل عالم، كل مصنع مسؤول عن إنشاء جميع العناصر المتوافقة من نفس العالم
مصنع عالم Medieval:
// Concrete Abstract Factories
class MedievalFactory implements IGameFactory {
public createCharacter(): ICharacter {
return new Knight();
}
public createWeapon(): IWeapon {
return new Sword();
}
public createVehicle(): IVehicle {
return new Horse();
}
}
مصنع عالم Sci-Fi:
// Concrete Abstract Factories
class SciFiFactory implements IGameFactory {
public createCharacter(): ICharacter {
return new SpaceMarine();
}
public createWeapon(): IWeapon {
return new LaserGun();
}
public createVehicle(): IVehicle {
return new Spaceship();
}
}
مصنع عالم Cyberpunk:
// Concrete Abstract Factories
class CyberpunkFactory implements IGameFactory {
public createCharacter(): ICharacter {
return new Hacker();
}
public createWeapon(): IWeapon {
return new Railgun();
}
public createVehicle(): IVehicle {
return new CyberBike();
}
}
لاحظ الأمر نفسه هنا مع Factory Method كنا سنحتاج 9 مصانع منفصلة أما مع Abstract Factory، لدينا 3 مصانع فقط
مصنع واحد لكل عالم، وكل مصنع يحتوي على 3 دوال من الـ Factory Methods داخله تضمن أن جميع العناصر من نفس العالم
// Use MedievalFactory
const medievalGame = new GameService(new MedievalFactory());
medievalGame.play();
// [Knight] Attacks with sword!
// [Sword] Slashes through the character!
// [Horse] Gallops across the battlefield!
// Use SciFiFactory
const sciFiGame = new GameService(new SciFiFactory());
sciFiGame.play();
// [Space Marine] Fires plasma rifle!
// [Laser Gun] Shoots laser beams!
// [Spaceship] Flies through space!
// Use CyberpunkFactory
const cyberpunkGame = new GameService(new CyberpunkFactory());
cyberpunkGame.play();
// [Hacker] Launches a cyber attack!
// [Railgun] Fires an electromagnetic projectile!
// [CyberBike] Speeds through the neon streets!
لاحظ أن GameService لا يعرف أي شيء عن Medieval أو Sci-Fi أو Cyberpunk
كل ما يعرفه هو أن لديه مصنعًا يعطيه character و weapon و vehicle متوافقين
وهذا المثال يوضح الفكرة الجوهرية للـ Abstract Factory بحيث أنه يضمن التوافق بين المنتجات المختلفة من نفس العائلة ويمنع الخلط بينها
ولن تقع أبدًا في مشكلة أن تستخدم Knight مع Railgun أو CyberBike
لأنك تتعامل مع مصنع واحد يضمن لك أن كل شيء من نفس العائلة
إضافة عائلة جديدة
الجميل في الـ Abstract Factory Pattern أنه يطبق مبدأ الـ Open/Closed Principle تمامًا عند إضافة عائلات جديدة
لنكمل على مثال المدفوعات
لو أردنا إضافة بوابة دفع جديدة مثل Fawry مع خدماتها الخاصة، كل ما نحتاجه هو إضافة كود جديد فقط
أولًا نضيف كلاسات الـ Concrete الجديدة لبوابة Fawry الخاصة بالـ Payment و Refund و Subscription
class FawryPayment implements IPayment {
public pay(amount: number): string {
console.log(`[Fawry] Processing payment of $${amount}`);
return `fawry_txn_${Date.now()}`;
}
}
class FawryRefund implements IRefund {
public refund(transactionId: string): void {
console.log(`[Fawry] Refunding transaction ${transactionId}`);
}
}
class FawrySubscription implements ISubscription {
public subscribe(plan: string): string {
console.log(`[Fawry] Creating ${plan} subscription`);
return `fawry_sub_${Date.now()}`;
}
}
ثانيًا نضيف الـ Concrete Abstract Factory الجديد
// Concrete Abstract Factory
class FawryFactory implements IPaymentFactory {
public createPayment(): IPayment {
return new FawryPayment();
}
public createRefund(): IRefund {
return new FawryRefund();
}
public createSubscription(): ISubscription {
return new FawrySubscription();
}
}
وهذا كل شيء
const fawryService = new PaymentService(new FawryFactory());
fawryService.processOrder(250, "standard");
// [Fawry] Processing payment of $250
// [Fawry] Creating standard subscription
// Order completed: fawry_txn_..., Subscription: fawry_sub_...
لاحظ معي أننا لم نفتح PaymentService
ولم نفتح IPaymentFactory
ولم نفتح PayTabsFactory أو HyperPayFactory أو PaymobFactory
في الحقيقة فنحن لم نعدل أي سطر واحد في أي كود قديم
نحن فقط أضفنا 4 كلاسات جديدة وانتهى الأمر
وهذا بالضبط ما يعنيه مبدأ الـ Open/Closed Principle
الفرق بين الـ Factory Method والـ Abstract Factory
الـ Factory Method Pattern يتعامل مع نوع واحد من المنتجات
فعلى سبيل المثال لو كان لدينا خدمة واحدة فقط وثلاث بوابات دفع
ففي هذه الحالة الـ Factory Method Pattern سيستطيع بسهولة التعامل مع خدمة واحدة مثل IPayment ويترك لكل كلاس فرعي أن يقرر أي Payment ينشئ حسب بوابة الدفع
أما الـ Abstract Factory Pattern فيتعامل مع عائلة كاملة من المنتجات في نفس الوقت
فعلى سبيل المثال لوكان لديث ثلاث خدمات وليس خدمة واحدة ولدينا ثلاث بوابات دفع
فهنا الـ Abstract Factory Pattern سيستطيع بسهولة أن يتعامل مع الخدمات الثلاثة IPayment و IRefund و ISubscription
وسيضمن أنهم دائمًا متوافقون وينتمون لنفس بوابة الدفع
يمكنك أن تتخيل الأمر هكذا:
- الـ
Factory Method: مصنع متخصص في نوع واحد من المنتجات - الـ
Abstract Factory: مجمع مصانع ينتج عائلة كاملة متوافقة
ملحوظة: أظنك لاحظت أن الـAbstract Factoryيستخدم الـFactory Methodداخله
بمعنى أن كل دالة داخل الـConcrete Abstract FactoryمثلcreatePayment()وcreateRefund()هي في حد ذاتهاFactory Method
الـAbstract Factoryيوفر الإطار والضمان للتوافق فقط
متى لا نستطيع استخدام الـ Abstract Factory Pattern
بعد كل ما رأيناه، أظنك تمتلك الإجابة على هذا السؤال بالفعل
إذا كان لديك نوع واحد فقط من المنتجات فالـ Factory Method Pattern أبسط وأكفأ
لا داعي لكل هذه الطبقات الإضافية من أجل مشكلة يحلها مصنع بسيط
إذا كانت المنتجات غير مترابطة فلا معنى للـ Abstract Factory
الهدف الأساسي منه هو ضمان التوافق بين المنتجات ومنع الخلط بين الأنواع
فأما إذا كان ليس هناك خطر من الخلط بينها فلا داعي له
إذا كنت تتوقع إضافة أنواع منتجات جديدة باستمرار فالـ Abstract Factory قد يصبح عقبة أمامك
وهذه أبرز نقطة ضعف فيه
بمعنى تخيل أنك قررت إضافة نوع خدمة رابع وهو IInvoice إلى نظام المدفوعات
هذا يعني أنك ستضطر لتعديل الـ interface الأساسي IPaymentFactory وإضافة دالة createInvoice() إليه
interface IPaymentFactory {
createPayment(): IPayment;
createRefund(): IRefund;
createSubscription(): ISubscription;
createInvoice(): IInvoice; // إضافة نوع جديد يتطلب تعديل هنا
}
وهذا التعديل يعني أنك ستضطر أيضًا لفتح جميع الـ Concrete Abstract Factories وإضافة createInvoice() إليها
class PayTabsFactory implements IPaymentFactory {
// ...existing code...
public createInvoice(): IInvoice {
return new PayTabsInvoice();
}
}
class HyperPayFactory implements IPaymentFactory {
// ...existing code...
public createInvoice(): IInvoice {
return new HyperPayInvoice();
}
}
class PaymobFactory implements IPaymentFactory {
// ...existing code...
public createInvoice(): IInvoice {
return new PaymobInvoice();
}
}
class FawryFactory implements IPaymentFactory {
// ...existing code...
public createInvoice(): IInvoice {
return new FawryInvoice();
}
}
فالـ Abstract Factory Pattern يطبق مبدأ الـ Open/Closed عند إضافة عائلات جديدة
لكنه ينتهكه عند إضافة أنواع منتجات جديدة للـ interface نفسه
ملحوظة: لهذا السبب يجب أن تتمهل قبل تطبيق أيDesign Patternبشكل عام في مشروعك
لأن كلDesign Patternمهما كان جيد ويحل مشكلة حقيقية إلا أنه قد يكون معقدأوOverenginneringفي بعضالأحيان
لذا يجب أن تكون واثقًا من الـDesign Patternالذي ستنفذه مهما كان وهل ستتحمل تكلفة التعقيد الذي قد يأتي به أم لا
الخلاصة
في هذه المقالة تعرفنا على الـ Abstract Factory Pattern، الحل الأشمل لإنشاء عائلات من الـ Objects المترابطة
بدأنا بمثال المدفوعات حيث تحتاج كل بوابة إلى Payment و Refund و Subscription متوافقة
واكتشفنا أن الـ Factory Method Pattern وحده لا يستطيع ضمان التوافق بينهم
لذا طبقنا الـ Abstract Factory Pattern لإنشاء مصانع كاملة تضمن أن كل الخدمات من نفس البوابة
وبهذا حققنا عدة مبادئ دفعة واحدة
حققنا ضمان التوافق بين المنتجات لأن كل مصنع ينتج عائلة كاملة متوافقة فيما بينها
وضمنا أنه لن تحدث مشاكل في الخلطبين الأنواع المختلفة
وحققنا مبدأ الـ Open/Closed لأننا نستطيع إضافة بوابات جديدة دون أن نلمس أي كود قديم
وحققنا مبدأ الـ Single Responsibility لأن كل Concrete Abstract Factory مختص بعائلة واحدة فقط، معزول عن تفاصيل العائلات الأخرى
القاعدة العملية البسيطة التي استخلصناها هى أنه إذا كنت تنشئ نوعًا واحدًا من المنتجات، استخدم الـ Factory Method
أما إذا كنت تنشئ عائلة من المنتجات المترابطة التي يجب أن تعمل معًا، استخدم الـ Abstract Factory
التعليقات
شاركنا رأيك في هذه المقالة أو اسأل عن أي شيء يخصها