درود دوستان! توی این قسمت می‌خوایم الگوی طراحی Abstract Factory رو بررسی کنیم که نسبتاً پراستفاده هست و پیاده‌سازی راحتی داره.

 

با مثال شروع کنیم

فرض کنیم کارخونه‌ای داریم که وظیفهٔ اون مونتاژ کردن وسایل الکترونیکی مثل موبایل، تبلت و ساعت هوشمند از شرکت‌های مختلف مثل اپل، سامسونگ و هوآوی هست. در ابتدای کار، شرکت فقط با محصولات اپل کار می‌کنه. الان می‌خوایم روی محصولات تست فنی انجام بدیم. کد زیر رو در نظر بگیرید:

function testProducts() {
  const tablet = new AppleTablet();
  tablet.switchOn();
  tablet.ring();

  const watch = new AppleWatch();
  watch.switchOn();
}

مثل همیشه وقتی در ابتدای کار هستیم همه چیز خوب به نظر می‌رسه. حالا با پیشرفت کردن کارخونه، می‌خوایم با یه شرکت دیگه هم (سامسونگ) همکاری کنیم. کد بالا رو باید به این صورت بنویسیم:

function testProducts(company) {
  if (company === 'Apple') {
    const tablet = new AppleTablet();
    tablet.switchOn();
    tablet.ring();
    
    const watch = new AppleWatch();
    watch.switchOn();
  } else if (company === 'Samsung') {
    const tablet = new SamsungTablet();
    tablet.turnOn();
    tablet.startRinging();
    
    const watch = new SamsungWatch();
    watch.turnOn();
  }
}

همونطور که می‌بینیم کدِ ما داره شلوغ‌تر و توسعهٔ اون سخت‌تر میشه. یکی از معایب این کد اینه که مثلاً تبلت‌های اپل با متد switchOn روشن میشن و تبلت‌های سامسونگ با متد turnOn. یعنی هر کدوم پیاده‌سازی‌های متفاوتی دارن و کسی که از این کدها استفاده می‌کنه باید از نحوه پیاده‌سازی محصولات آگاهی داشته باشه. مشکل زمانی بزرگ‌تر میشه که بخوایم با محصولات یه شرکت دیگه هم کار کنیم 🙈

اینجاست که الگوی Abstract Factory به کار ما میاد. اگه این الگو رو پیاده‌سازی کنیم، می‌تونیم بی‌نهایت محصول از شرکت‌های مختلف داشته باشیم، بدون توجه به اینکه نحوه پیاده‌سازی و استفاده از اونها به چه صورت هست 👌

قبل از اینکه با این الگو آشنا بشیم باید بدونیم که مفهوم Client چی هست. Client به قسمتی از برنامه گفته میشه که کار اصلی رو انجام میده. توی مثال بالا، تابع testProducts قسمت Client هست که با کلاس‌هایی که ساختیم در ارتباطه و از اونها نمونه‌سازی می‌کنه تا کاری که مد نظرمون هست (تست فنی محصولات) رو اجرا کنه.

 

الگوی Abstract Factory چیه؟

این الگو این امکان رو به Client میده تا بدون اینکه درگیر نوع آبجکت‌ها و نحوه پیاده‌سازی اونها بشه، وظیفه اصلی خودش رو انجام بده.

قبلاً توی قسمت Client برنامه، ما به صورت دستی آبجکت‌ها رو می‌ساختیم که مشکلات خاص خودش رو داشت. با اعمال کردن این الگو می‌تونیم کاری کنیم که ساختن و پیاده‌سازی آبجکت‌ها از دید Client مخفی بمونه. بعد از اعمال کردن این الگو، تابع testProduct به شکل زیر در میاد:

function testProduct(factory: DeviceFactory) {
  const tablet = factory.createTablet();
  tablet.switchOn();

  const smartphone = factory.createSmartphone();
  smartphone.ring();
}

همونطور که می‌بینیم این تابع دیگه درگیر ساختن و پیاده‌سازی محصولات نمیشه. همچنین براش مهم نیست که با چه شرکتی (اپل یا سامسونگ؟) داره کار می‌کنه.

استفاده از الگوی Abstract Factory برای زمانی خوبه که ما چند نوع محصول با برند (تنوع) های مختلف داریم و Client می‌خواد با همه این محصولات کار کنه، بدون اینکه براش مهم باشه که این محصولات متعلق به چه برندی هستن. برای مثال ما محصولاتی مثل گوشی، تبلت و ساعت از برندهای مختلفی مثل اپل و سامسونگ و هوآوی داریم:

// Products: Tablet - Watch - Smartphone
// Variants: Apple - Samsung - Huawei

و می‌خوایم روی همه محصولات تست فنی انجام بدیم، بدون اینکه برای تستر مهم باشه که با محصولات چه برندی داره کار می‌کنه.

این الگو زیر مجموعه الگوهای Creational هست. یعنی الگوهایی که توی اون ساختن آبجکت اتفاق می‌افته.

 

پیاده‌سازی الگوی Abstract Factory

ما چه نوع محصولاتی داریم؟ برای مثال ۲ نوع: گوشی هوشمند و تبلت. اولین کار اینه که برای هر نوع محصول، یک اینترفیس درست کنیم:

interface Tablet {
  switchOn();
}

interface Smartphone {
  switchOn();
  ring();
}

حالا کلاس‌های واقعی (Concrete) هر محصول، باید اینترفیس‌های مربوط به خودشون رو پیاده‌سازی کنن.

محصولات اپل:

class AppleSmartphone implements Smartphone {
  public switchOn() {
    console.log('Apple Smartphone: Switching on');
  }
  public ring() {
    console.log('Apple Smartphone: Ringing');
  }
}

class AppleTablet implements Tablet {
    public switchOn() {
      console.log('Apple Tablet: Switching on');
    }
}

محصولات سامسونگ:

class SamsungSmartphone implements Smartphone {
  public switchOn() {
    console.log('Samsung Smartphone: Switching on');
  }

  public ring() {
    console.log('Samsung Smartphone: Ringing');
  }
}


class SamsungTablet implements Tablet {
  public switchOn() {
    console.log('Samsung Tablet: Switching on');
  }
}

 

حالا باید برای هر برند، یک کلاس اختصاصی بسازیم. توی این کلاس‌ها متدهایی وجود داره که مخصوص ساختن و ارائه دادن محصولات هستن. به این کلاس‌ها میگن کلاس‌های Factory.

اما قبلش باید برای این کلاس‌های Factory، یک اینترفیس بسازیم تا همگی متدهای مشترکی داشته باشن. توی این اینترفیس، متدهای ساخت محصولات مختلف رو تعریف می‌کنیم:

interface DeviceFactory {
  createSmartphone(): Smartphone;
  createTablet(): Tablet;
}

به این اینترفیس میگن Abstract Factory که نقش اصلی رو توی این الگو به عهده داره. همونطور که می‌بینیم، متدهایی ساختیم که برای همه فکتوری‌ها مشترک هستن و نوع خروجی اونها رو برابر با اینترفیس محصولی که قراره تولید بشه تنظیم کردیم. حالا باید کلاس‌های فکتوری رو بسازیم که باید از این اینترفیس تبعیت کنن:

کلاس فکتوری برای اپل:

class AppleFactory implements DeviceFactory {
  public createSmartphone(): Smartphone {
    return new AppleSmartphone();
  }
    
  public createTablet(): Tablet {
    return new AppleTablet();
  }
}

کلاس فکتوری برای سامسونگ:

class SamsungFactory implements DeviceFactory {
  public createSmartphone(): Smartphone {
    return new SamsungSmartphone();
  }

  public createTablet(): Tablet {
    return new SamsungTablet();
  }
}

همونطور که می‌بینیم، آبجکت‌ها دارن توی کلاس فکتوری تولید میشن و در نتیجه از دید Client مخفی شدن.

و حالا موقع استفاده از این کد هست. قسمت Client رو می‌نویسیم:

function testProducts(factory: DeviceFactory) {
  const smartphone = factory.createSmartphone();
  smartphone.ring();

  const tablet = factory.createTablet();

  tablet.switchOn();
}

testProducts(new SamsungFactory);
// Samsung Smartphone: Ringing
// Samsung Tablet: Switching on


testProducts(new AppleFactory);
// Apple Smartphone: Ringing
// Apple Tablet: Switching on

با این کار، قسمت Client برنامه بدون اینکه درگیر ساختن آبجکت‌ها از برندهای مختلف بشه، داره وظیفه اصلی خودش یعنی تست کردن محصولات رو انجام میده. حالا اگه بخوایم یک برند دیگه رو اضافه کنیم چطور؟ برای مثال می‌خوایم هوآوی رو اضافه کنیم. کافیه محصولات اون رو طوری بسازیم که از اینترفیس‌های محصولات تبعیت کنن:

class HuaweiSmartphone implements Smartphone {
  public switchOn() {
    console.log("Huawei Smartphone: Switching on");
  }

  public ring() {
    console.log("Huawei Smartphone: Ringing");
  }
}

class HuaweiTablet implements Tablet {
  public switchOn() {
    console.log("Huawei Tablet: Switching on");
  }
}

و بعد برای این برند، یک فکتوری می‌سازیم:

class HuaweiFactory implements DeviceFactory {
  public createSmartphone(): Smartphone {
    return new HuaweiSmartphone();
  }

  public createTablet(): Tablet {
    return new HuaweiTablet();
  }
}

و نهایتاً با پاس دادن این فکتوری به Client می‌تونیم محصولات هوآوی رو هم تست کنیم:

testProducts(new HuaweiFactory);

// Huawei Smartphone: Ringing
// Huawei Tablet: Switching on

با پیاده‌سازی این الگو می‌تونیم بی‌نهایت برند اضافه کنیم، بدون اینکه قسمت Client هیچ تغییر کنه 👌

کد کامل این قسمت رو از ببینین. Star هم بزنین ⭐😉

 

چه زمانی از الگوی Abstract Factory استفاده کنیم؟

از این الگو زمانی استفاده می‌کنیم که Client می‌خواد با خانواده‌ای از محصولات (تبلت، ساعت و گوشی) کار کنه، بدون اینکه وابسته به این باشه که این آبجکت‌ها چطوری پیاده‌سازی میشن و از چه خانواده‌ای (اپل یا سامسونگ) هستن.

معمولاً از الگوهای Factory زمانی استفاده می‌کنیم که Client تا قبل از اجرا شدن برنامه نمی‌دونه چه آبجکت‌هایی رو پیاده‌سازی کنه و برای حل این موضوع بدون استفاده از الگوها، کد پر از if/else و new میشه.

الگوهای فکتوری مثل Abstract Factory و Factory Method با ساختن لایه‌های انتزاعی (Abstraction) عملیات ساختن آبجکت رو از دید Client مخفی می‌کنن.

 

مزایای استفاده از الگوی Abstract Factory

۱. با استفاده از این الگو + تزریق وابستگی (Dependency Injection)، وابستگی (یا به قول معروف Tight Coupling) بین Client و قسمت‌های دیگه از بین رفت

۲. کارِ ساختن و آماده‌سازی آبجکت‌ها رو منتقل کردیم به کلاس‌های مربوط به خودشون تا اصل اول سالید (Single Responsibility) رو پیاده‌سازی کنیم

۳. می‌تونیم بی‌نهایت خانواده (برند) محصولات داشته باشیم بدون اینکه کدهای Client رو دستکاری کنیم. (اصل دوم سالید Open/Closed)

برای آشنایی با اصول سالید می‌تونید پست‌های زیر رو بخونید:

 

 

معایب استفاده از الگوی Abstract Factory

با اضافه کردن تعداد زیادی اینترفیس و کلاس، ممکنه باعث شلوغی و پیچیدگی توی کد بشه. مثل بقیه الگوها، از این الگو زمانی باید استفاده بشه که مشکل از قبل شناسایی شده باشه.

 

خب دوستان امیدوارم از این قسمت هم استفاده کرده باشین. روزتون خوش😉✌️

منابعی که برای این قسمت استفاده کردم: