سلام دوستان! ساختن آبجکت‌ها همیشه به سادگی یک new کردن نیست. گاهی وقت‌ها ساختن یک آبجکت و آماده کردن اون برای استفاده، به پارامترها و ورودی‌های زیادی بستگی داره. برای مثال توی بیشتر زبان‌ها از Query Builder برای ساختن کوئری‌های SQL استفاده میشه:

const user = db.users.find(...);
// or
const post = db.posts.where(...).select(...).find(...);

همونطور که می‌بینیم آبجکت‌های user و post به ورودی‌های مختلفی نیاز دارن و طی چندین مرحله ساخته میشن.

پس اگه مثل کد بالا آبجکتی داریم که باید طی چند مرحله ساخته بشه، از الگوی Builder استفاده می‌کنیم.‍

 

الگوی Builder چیه؟

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

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

 

پیاده‌سازی الگوی Builder

فرض کنیم می‌خوایم یک Query Builder برای ساختن درخواست‌های SQL بسازیم. چیزی که می‌سازیم باید بتونه برای انواع دیتابیس‌ها مثل MySQL و MongoDB کارایی داشته باشه. به عبارت دیگه، برای کلاینت (قسمتی از برنامه که داره از این ویژگی استفاده می‌کنه) نباید مهم باشه که چه نوع دیتابیسی مورد استفاده قرار گرفته.

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

interface QueryBuilder {
  table(table): QueryBuilder;
  select(cols): QueryBuilder;
  limit(): QueryBuilder;
  where(): QueryBuilder;
  getQuery(): String;
  
  /* +100 Other SQL Methods  */
}

توی Query Builder ها معمولاً متدهایی مثل where و limit و چندین دستور SQL داریم که اینجا به طور خلاصه ۴ تا از اونها رو تعریف کردیم. همچنین نوع خروجی همه متدها رو برابر با خودِ اینترفیس یعنی QueryBuilder قرار دادیم. این کار برای این هست که متدهای زیرکلاس‌ها رو مجبور کنیم با return this، خودِ کلاس رو return کنن تا بتونیم متدهای زنجیره‌ای (Method Chaining) داشته باشیم. یعنی:

obj.foo().bar().baz().blah().blah().blah();

 

مرحله بعد باید برای هر نوع دیتابیس یک کلاس اختصاصی بسازیم که از این اینترفیس تبعیت می‌کنن. ابتدا برای MySQL می‌نویسیم:

class MySqlQueryBuilder implements QueryBuilder {
  private query: String;
  private tableName: String;

  public constructor() {
    this.query = '';
  }

  public table(table): QueryBuilder {
    this.tableName = table;

    return this;
  }

  public select(cols): QueryBuilder {
    /** ... */
    return this;
  }

  public limit(): QueryBuilder {
    /** ... */
    return this;
  }
  
  public where(col, value): QueryBuilder {
    /** ... */
    return this;
  }

  public getQuery(): String {
    return 'This is a MySQL query';
  }
}

متدهای کلاس MySqlQueryBuilder باید دستورات خام MySQL رو تولید کنن. مثلاً متد select برای MySQL باید چنین چیزی تولید کنه:

SELECT col1, col2 FROM table_name

و به همین صورت می‌تونیم برای بقیه انواع دیتابیس هم چنین کلاسی بسازیم:

class MongoDbQueryBuilder implements QueryBuilder {
  private query: String;
  private tableName: String;

  public constructor() {
    this.query = '';
  }

  public table(table): QueryBuilder {
    this.tableName = table;

    return this;
  }

  public select(cols): QueryBuilder {
    /** ... */
    return this;
  }

  public limit(value: Number): QueryBuilder {
    /** ... */
    return this;
  }
  
  public where(col, value): QueryBuilder {
    /** ... */
    return this;
  }

  public getQuery(): String {
    return 'This is a MongoDB query';
  }
}

متدهای کلاس MongoDbQueryBuilder هم مسئول تولید کردن کوئری‌های خام MongoDB هستن.

خب حالا وقت اینه که از این کد استفاده کنیم. ابتدا قسمت Client (قسمت اصلی برنامه‌ی ما که از این الگو استفاده می‌کنه) رو می‌نویسیم:

function client(builder: QueryBuilder) {
  const query = builder.table('posts').where('id', 429).limit(10).select(['id', 'title']).getQuery();

  console.log(query);
}

همونطور که گفتیم قسمت Client لازم نیست بدونه که چه نوع دیتابیسی مورد استفاده قرار گرفته. برای همین مفهوم Abstraction اینجا به کمک ما میاد. Client بدون اطلاع از نوع دیتابیس داره کار خودش رو انجام میده. اما به هر حال جایی از برنامه باید مشخص کنیم که چه نوع دیتابیسی می‌خوایم. مشخص کردن نوع دیتابیس یا به طور کلی نوع Builder قبل از اجرای واقعی قسمت Client اتفاق می‌افته:

const config = {
  database1: MongoDbQueryBuilder,
  database2: MySqlQueryBuilder
}

client(new config.database1); // This is a MongoDB query
client(new config.database2); // This is a MySQL query

خب این بود الگوی Builder 😉

اگه از این مثال خوشتون اومد، کد کامل اون رو از ببینین و Star بزنین ⭐😉

 

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

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

function makeMySqlQuery(table, type, constraints, cols, values, limit = null, offset = 0) {
  const query = {};

  query.table = table;
  query.type = type;
  
  if (cols) {
    // ...  
  }

  // + ...

  return query.toSql();
}

makeMySqlQuery('users', 'select', 'id = 429', null, null, null, 10);
makeMySqlQuery('post', 'update', 'id = 900', ['name'], ['john'], 15);

مشکلی که اینجا دیده میشه اینه که مثل خط ۱۶ می‌خوایم پارامترهای پایانی رو مقداردهی کنیم اما به بعضی از پارامترهای میانی احتیاجی نداریم. مشکل زمانی بیشتر میشه که بخوایم یک نوع دیتابیس دیگه هم اضافه کنیم. باید یک تابع دیگه بنویسیم و همه قسمت‌هایی که Client داره از تابع قبلی استفاده می‌کنه رو ویرایش کنیم.

اما دیدیم که این مشکل به راحتی با الگوی Builder حل شد. پس اگه جایی از برنامه یک تابع/متد دارین که پارامترهای زیادی داره، شاید به این الگو نیاز باشه.

 

مزایای الگوی Builder

۱. با تعریف کردن مراحل و ترکیب کردن اونها به طور دلخواه، بدون دستکاری کد می‌تونیم آبجکت‌های متنوع و با خروجی کاملاً متفاوتی داشته باشیم. برای مثال کد زیر رو در نظر بگیرید:

builder.table('posts').where(...).select(...).find(...);

بسته به نوع کانفیگ برنامه، خروجی این کد می‌تونه یک نمونه از کلاس MySql باشه یا MongoDb که با هم دیگه تفاوت خواهند داشت. بنابراین، مراحل یکسان، خروجی متفاوت.

۲. قسمت Client فقط درگیر انجام مسئولیت خودش میشه، نه ساختن و کانفیگ کردن آبجکت. مثلاً توی قسمت Client تنها هدف ما باید این باشه که کاربرا رو از دیتابیس بخونیم و نمایش بدیم (اصل اول سالید: Single Responsibility)

۳. می‌تونیم بی‌نهایت Builder بسازیم بدون اینکه تغییری توی کدها به وجود بیاریم (اصل دوم سالید: Open/Closed)

۴. قسمت Client وابسته به Abstraction هست، نه کلاس‌های واقعی یا Concrete (اصل پنجم سالید: Dependency Inversion)

 

معایب الگوی Builder

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

 

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

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