الـ Builder Pattern عامل البناء المشهور والمحبوب

السلام عليكم ورحمة الله وبركاته

وقت القراءة: ≈ 15 دقيقة

المقدمة

اليوم سيكون يومًا ممتعًا لبناء بعض الأمور الكبيرة والمعقدة بطريقة سهلة وبسيطة بإستخدام الـ Builder Pattern
في هذه المقال البسيطة سنتعرف على أحد الـ Design Patterns وهو عامل البناء الشهير والمحبوب الـ Builder Pattern

الـ Builder Pattern ينتمي إلى عائلة الـ Creational Design Patterns المسؤولة عن عملية إنشاء الـ object بطريقة مرنة وبسيطة ومنظمة
والـ Builder Pattern يعد من أشهر الطرق لإنشاء الـ object المعقدة التي تحتوي على العديد من الخصائص والمتغيرات

لكن قبل أن نبدأ في شرح الـ Builder Pattern دعونا نتعرف على المشكلة التي يحلها الـ Builder Pattern

ما المشكلة التي يحلها الـ Builder Pattern ؟

حسنًا لنتخيل أن لدينا كلاس يدعى AuditLog وهذا الكلاس يهتم بتسجيل الأنشطة التي تحدث في التطبيق

وهذا الكلاس يحتوي على العديد من الخصائص مثل:

لنلقِ نظرة على كلاس الـ AuditLog:

class AuditLog {
  action: string;
  message?: string;
  trackableModel: any;
  trackableModelId: number;
  oldProperties?: object;
  newProperties?: object;
  metadata?: object;
  actorId?: number;

  constructor(
    action: string,
    message?: string,
    trackableModel: any,
    trackableModelId: number,
    oldProperties?: object,
    newProperties?: object,
    metadata?: object,
    actorId?: number,
  ) {
    this.action = action;
    this.message = message;
    this.trackableModel = trackableModel;
    this.trackableModelId = trackableModelId;
    this.oldProperties = oldProperties;
    this.newProperties = newProperties;
    this.metadata = metadata;
    this.actorId = actorId;
  }

  public log() {
    console.log("Logging the activity...");
  }
}

هنا أنشأنا كلاس AuditLog وقمنا بتعريف العديد من الخصائص والمتغيرات التي يحتاجها
ولدينا دالة log تقوم بأي شيء مثلا تسجل الحدث في الـ database القاعدة البيانات الخاصة بالمشروع

الآن لنفترض أننا نريد إنشاء AuditLog لتسجيل عدة أنشطة مختلفة في التطبيق

// إنشاء منتج جديد
new AuditLog("create", "Product created", "Product", 1, null, null, null, 1);

// تحديث منتج معين
new AuditLog(
  "update",
  "Product updated",
  "Product",
  1,
  { name: "Old Name" },
  { name: "New Name" },
  null,
  1,
);

// تم أرشفة المنتج تلقائيًا لأي سبب كان
new AuditLog(
  "Archive",
  "Product archived automatically",
  "Product",
  1,
  null,
  null,
  { reason: "Out of stock" },
  1,
);

// إغلاق قسم معين
new AuditLog("close", null, "Department", 1, null, null, null, 1);

عندما تتأمل في هذا الكود، هل تشعر بعدم الراحة أو أنك تشعر بأن هناك شيئًا غير صحيح ؟
الكود يؤدي وظيفته بشكل جيد، لكنه هل تشعر أنك قد تحفظ ترتيب كل البيانات الـ constructor ؟

الكود فيه عدة مشاكل منها:

والحل ؟ أكيد لا داعي لأن أخبرك بأن الحل هو الـ Builder Pattern البناء الشهير والمحبوب
فلنبدأ في شرح الـ Builder Pattern وكيف يمكننا استخدامه لحل هذه المشكلة

ما هو الـ Builder Pattern ؟

فكرة الـ Builder Pattern بسيطة جدًا وهي تقوم بعمل كلاس وسيط يقوم ببناء الـ object
وهذا الكلاس يقدم لك دوال متعددة وواضحة لتعين القيم التي تريدها في الـ object
ثم بعد ما تختار القيم التي تريدها يقوم الكلاس ببناء الـ object لك

وبما أننا نريد عمل واحد لكلاس الـ AuditLog سنقوم بعمل كلاس يدعى AuditLogBuilder ونضع كلمة Builder في نهاية الكلاس لتوضيح أن هذا الكلاس هو الـ Builder للكلاس AuditLog

لنبدأ في كتابة الـ AuditLogBuilder

أول شيء نحدد المتغيرات التي يجب أن تكون إجبارية في أي AuditLog مثل action, trackableModel و trackableModelId
ونحدد المتغيرات الأخرى التي يمكن أن تكون اختيارية مثل message, oldProperties, newProperties, metadata و actorId

بعد تحديدها نقوم بكتابة الـ AuditLogBuilder ونجبر المستخدم على تعيين القيم الإجبارية في الـ constructor

class AuditLogBuilder {
  private action: string;
  private message?: string;
  private trackableModel: any;
  private trackableModelId: number;
  private oldProperties?: object;
  private newProperties?: object;
  private metadata?: object;
  private actorId?: number;

  constructor(action: string, trackableModel: any, trackableModelId: number) {
    this.action = action;
    this.trackableModel = trackableModel;
    this.trackableModelId = trackableModelId;
  }
}

هنا سترى أنه لدينا كلاس AuditLogBuilder ولديه جميع المتغيرات التي كانت في الـ AuditLog
وأيضًا لدينا constructor يجبر المستخدم على تعيين القيم الإجبارية فقط مثل action, trackableModel و trackableModelId

الآن ما هى الخطوة التالية ؟

حسنًا الآن عندما تقوم بعمل object من الـ AuditLogBuilder ستجده فارغ

const auditLogBuilder = new AuditLogBuilder("create", "Product", 1);

هنا لدينا auditLogBuilder وهو فارغ ولا يحتوي على أي بيانات وأيضًا ما فائدته ؟
عليك التحلي بالصبر قد تظن أننا قمنا بوضح طبقة جديدة من الكود ولكن لا فائدة منها
لكن عندما ننتهي سترى كم سيقوم الـ Builder Pattern بتبسيط الأمور ويحل المشاكل التي كانت تواجهنا

الآن بعد ما وضعنا القيم الإجبارية في الـ constructor
علينا أن نهتم بتعين القيم الأخرى الاختيارية
لكن كيف نقوم بتعينها ؟ وتذكر أنها اختيارية لذا يجب أن نجعل من السهل تعينها واختيارها
ولا نريد أن نواجه المشاكل التي واجهناها مثل null أو نسيان ترتيب البيانات

لذا الفكرة البسيطة هو عمل دوال لتعين القيم الاختيارية بمعنى أن كل دالة تعين قيمة واحدة فقط
فمثلًا ننشيء دالة تدعى setMessage وهذه الدالة تعين قيمة message فقط
ودالة أخرى تدعى setOldProperties وهذه الدالة تعين قيمة oldProperties فقط وهكذا

class AuditLogBuilder {
  // ...

  public setMessage(message: string) {
    this.message = message;
    return this;
  }

  public setOldProperties(oldProperties: object) {
    this.oldProperties = oldProperties;
    return this;
  }

  public setNewProperties(newProperties: object) {
    this.newProperties = newProperties;
    return this;
  }

  public setMetadata(metadata: object) {
    this.metadata = metadata;
    return this;
  }

  public setActorId(actorId: number) {
    this.actorId = actorId;
    return this;
  }
}

هنا قمنا بعمل دوال لتعين القيم الاختيارية وكل دالة تعين قيمة واحدة فقط وتعين القيمة ثم تعيد الـ this
وكما تعلم الـ this يعني الـ object نفسه وبما أن الدالة تعين القيمة وتعيد الـ object نستطيع استدعاء الدوال بشكل متسلسل
وسنرى ذلك في الأمثلة التالية

هكذالو أردنا تعين قيمة message نقوم فقط باستدعاء الدالة setMessage ولو أردنا تعين قيمة actorId نقوم بإستدعاء الدالة setActorId

const auditLogBuilder = new AuditLogBuilder("create", "Product", 1);

auditLogBuilder
  .setMessage("Product created")
  .setActorId(1)
  .setMetadata({ initialStock: 1000 });

هنا قمنا بعمل object من الـ AuditLogBuilder وكما قلنا أن القيم الإجبارية يجب تعينها في الـ constructor
وهذا ما فعلناه بتعين القيم action, trackableModel و trackableModelId في الـ constructor
ثم بعد ذلك قمنا بتعين القيم الاختيارية بإستدعاء الدوال المناسبة
هنا قمنا بتعين قيمة message, actorId و metadata

لاحظ أننا نسدعي الدوالي بشكل متسلسل وهذا يعطينا مرونة كبيرة في تعين القيم واختيارها
هذا بسبب أن كل دالة تقوم بارجاع الـ this أي أنها تقوم بإرجاع الـ object نفسه
وبسبب تلك الفكرة نستطيع تعين القيم بشكل متسلسل ومرن باستخدام . كما فعلنا في الكود


الآن لاحظ الآتي
المشكلة كانت أننا كنا نقوم بعمل object من الـ AuditLog ونضع كل البيانات في الـ constructor

وإليك كيف كان الكود القديم

new AuditLog(
  "update",
  "Product updated",
  "Product",
  1,
  { name: "Old Name" },
  { name: "New Name" },
  { reason: "Copy-right issue" },
  1,
);

هنا المشكلة أنك بمجرد النظر لا تعرف اسماء المتغيرات التي تم تعينها
بمعنى هل تستطيع أن تقول لي الرقم 1 الذي يظهر في آخر parameter في الـ constructor تم تعينه لماذا
أول هل نحن نحن نكتب oldProperties أولًا أم newProperties أولًا ؟
أو هل الترتيب التي كتبته صحيح من الأساس أم لا ؟

الآن أنظر بعد استخدام الـ Builder Pattern كيف أصبح الكود

new AuditLogBuilder("update", "Product", 1)
  .setMessage("Product updated")
  .setOldProperties({ name: "Old Name" })
  .setNewProperties({ name: "New Name" })
  .setMetadata({ reason: "Copy-right issue" })
  .setActorId(1);

لاحظ الآن أننا بمجرد النظر إلى الكود عرفنا المتغيرات والقيم التي نريدها بشكل واضح


هل انتهينا ؟

بالطبع لا فنحن لم نقم بعمل object من الـ AuditLog بعد
نحن فقط قمنا بعمل object من الـ AuditLogBuilder وقمنا بتعين القيم التي نريدها بشكل بسيط ومنظم

ما فائدة عامل البناء المحبوب إذا كان معه كل الأدوات ولكنه لا يقوم ببناء شيء ؟

لذا كأخر خطوة علينا أن نقوم بعمل دالة تقوم ببناء الـ object من الـ AuditLogBuilder إلى الـ AuditLog

class AuditLogBuilder {
  // ...

  public build() {
    return new AuditLog(
      this.action,
      this.message,
      this.trackableModel,
      this.trackableModelId,
      this.oldProperties,
      this.newProperties,
      this.metadata,
      this.actorId,
    );
  }
}

و ... هذا كل شيء

هنا لدينا دالة تدعى build وهذه تقوم فقط بإرجاع object من الـ AuditLog وتعطيه كل البيانات التي تم تعينها داخل الـ AuditLogBuilder

الآن لاحظ كيف يمكننا عمل object من الـ AuditLog بسهولة

const auditLog = new AuditLogBuilder("update", "Product", 1)
  .setMessage("Product updated")
  .setOldProperties({ name: "Old Name" })
  .setNewProperties({ name: "New Name" })
  .setMetadata({ reason: "Copy-right issue" })
  .setActorId(1)
  .build();

هنا قمنا بعمل object من الـ AuditLog بسهولة وبشكل واضح عن طريق عمل طبقة وسيطة تسمى AuditLogBuilder
وهذه تغنينا عن الكثير من المشاكل التي ذكرناها في البداية مثل ترتيب البيانات ونسيان القيم الافتراضية و قيم الـ null

الآن كما ترى نحن نستخدم الـ AuditLogBuilder وقمنا بتعين القيم التي نريدها ثم في النهاية قمنا باستدعاء الدالة build لتقوم ببناء الـ object النهائي من الـ AuditLog


الآن هل تتذكر الأمثلة السابقة التي كانت تحتوي على الكثير من البيانات وكانت مربكة ؟
دعني أذكرك بها

// إنشاء منتج جديد
new AuditLog("create", "Product created", "Product", 1, null, null, null, 1);

// تحديث منتج معين
new AuditLog(
  "update",
  "Product updated",
  "Product",
  1,
  { name: "Old Name" },
  { name: "New Name" },
  null,
  1,
);

// تم أرشفة المنتج تلقائيًا لأي سبب كان
new AuditLog(
  "Archive",
  "Product archived automatically",
  "Product",
  1,
  null,
  null,
  { reason: "Out of stock" },
  1,
);

// إغلاق قسم معين
new AuditLog("close", null, "Department", 1, null, null, null, 1);

لنرى كيف أصبحت الأمور بعد استخدام الـ Builder Pattern

// إنشاء منتج جديد
new AuditLogBuilder("create", "Product", 1)
  .setMessage("Product created")
  .setActorId(1)
  .build();

// تحديث منتج معين
new AuditLogBuilder("update", "Product", 1)
  .setMessage("Product updated")
  .setOldProperties({ name: "Old Name" })
  .setNewProperties({ name: "New Name" })
  .setActorId(1)
  .build();

// تم أرشفة المنتج تلقائيًا لأي سبب كان
new AuditLogBuilder("Archive", "Product", 1)
  .setMessage("Product archived automatically")
  .setMetadata({ reason: "Out of stock" })
  .setActorId(1)
  .build();

// إغلاق قسم معين
new AuditLogBuilder("close", "Department", 1)
  .setActorId(1)
  .build();

هل ترى الفرق ؟
هل ترى كم أصبح الكود أكثر وضوحًا ومنظمًا ؟
هل أحتاج لأن افسر لك كم هذا الأمر سيغنيك عن الكثير من المشاكل ؟

أظنني قد أوضحت الفكرة بشكل جيد وأظنني قد أوضحت لك كم أن الـ Builder Pattern رائع ومفيد

لكن هل هناك أمور يجب أن تعرفها ؟

أجل يمكننا جعل الـ Builder Pattern أفضل وهناك بعض التقنيات التي يمكننا استخدامها لتحسين الأداء والكود
وسأعرض لك بعض الفوائد والاستخدامات الأخرى للـ Builder Pattern

بعض التحسينات على الـ Builder Pattern

لعلك لاحظت بعض الأمور اثناء شرح الـ Builder Pattern وهو أننا مازلنا نستخدم الـ constructor لتعين القيم الإجبارية
وهذا قد ينشيء لنا نفس المشكلة التي كنا نواجهها في البداية

new AuditLogBuilder("create", "Product", 1)
  .setMessage("Product created")
  .setActorId(1)
  .build();

سؤال هل تتذكر أسماء المتغيرات التي تم تعينها في الـ constructor ؟
هل كان أول parameter يدعى action أم event ؟
وإلى ماذا يشير الرقم 1 في الـ constructor ؟
وهكذا وإذا كثرت القيم الإجبارية ستجد نفسك تواجه نفس المشكلة
وهذه المشكلة يمكننا حلها بإستخدام الـ Builder Pattern أيضًا عن طريقة فكرة أو تقنية تسمى Fluent Builder

وأيضًا لاحظت أن إذا دخل عضو جديد في الفريق قد لا يدرك وجود الـ Builder Pattern
بالتالي قد يقوم بعمل object من الـ AuditLog بشكل عادي وبدون استخدام الـ Builder Pattern الذي قمنا بعمله

وهذه المشكلة حلها أيضًا سهل بجعل الـ constructor الخاص بالـ AuditLog يستقبل object من الـ AuditLogBuilder فقط وسنرى هذا
وأيضًا سنرى كيف يمكننا تحسين الـ Builder Pattern بعدة طرق أخرى


قبل أي شيء لنقم فقط باستخدام مثال بسيط لنعرف كيف يمكننا تحسين الـ Builder Pattern
والمثال سيكون على كلاس User وهذا الكلاس يحتوي على name, email, age, address و phone

class User {
  name: string;
  email: string;
  age?: number;
  address?: string;
  phone?: string;

  constructor(
    name: string,
    email: string,
    age?: number,
    address?: string,
    phone?: string,
  ) {
    this.name = name;
    this.email = email;
    this.age = age;
    this.address = address;
    this.phone = phone;
  }
}

لنقم بعمل UserBuilder ونقوم بتعين القيم الإجبارية في الـ constructor والقيم الاختيارية بالدوال

class UserBuilder {
  private name: string;
  private email: string;
  private age?: number;
  private address?: string;
  private phone?: string;

  constructor(name: string, email: string) {
    this.name = name;
    this.email = email;
  }

  public setAge(age: number) {
    this.age = age;
    return this;
  }

  public setAddress(address: string) {
    this.address = address;
    return this;
  }

  public setPhone(phone: string) {
    this.phone = phone;
    return this;
  }

  public build() {
    return new User(this.name, this.email, this.age, this.address, this.phone);
  }
}

وهكذا نستطيع عمل object من الـ User بسهولة

const user = new UserBuilder("Ahmed", "[email protected]")
  .setAge(25)
  .setAddress("North Sinai, Egypt")
  .setPhone("01000000000")
  .build();

بعد ما جهزنا مثالنا البسيط لنقم بتحسين الـ Builder Pattern

كيفية اجبار الأخرين على استخدام الـ Builder Pattern فقط

هنا لدينا مشكلة بسيطة وهى أن أحد الأعضاء الجدد قد يقوم بعمل object من الـ User بشكل عادي
وقد لا يدرك وجود الـ Builder Pattern الذي قمنا بعمله الذي يدعى UserBuilder
بالتالي سنواجه صعوبة كع الأعضاء الجدد في فهم الكود والتعامل معه

لذا الحل هو أن نجعل الـ constructor الخاص بالـ User يستقبل فقط object من الـ UserBuilder وليس قيم عادية

class User {
  name: string;
  email: string;
  age?: number;
  address?: string;
  phone?: string;

  constructor(userBuilder: UserBuilder) {
    this.name = userBuilder.name;
    this.email = userBuilder.email;
    this.age = userBuilder.age;
    this.address = userBuilder.address;
    this.phone = userBuilder.phone;
  }
}

class UserBuilder {
  // ...
  public build() {
    return new User(this);
  }
}

هكذا عندما يحاول أحد الأعضاء الجدد عمل object من الـ User بشكل عادي سيستنتج بشكل سريع أنه يجب عليه استخدام الـ UserBuilder
لأن الـ constructor يستقبل فقط object من الـ UserBuilder فسيدرك وجود الـ Builder Pattern الذي قمنا بعمله
ولاحظ أن دالة الـ build تقوم باستدعاء الـ constructor الخاص بالـ User وتمرر this له
والـ this هو object من الـ UserBuilder

وهكذا نحن نجبر الأعضاء الجدد على استخدام الـ Builder Pattern الذي قمنا بعمله

استخدام دالة static بدلا من الـ constructor في الـ Builder Pattern

هنا لدينا فكرة أخرى لتحسين الـ Builder Pattern وهي استخدام دالة static بدلا من الـ constructor في الـ UserBuilder

السبب لهذا هو عدم الحاجة لعمل object من الـ UserBuilder من خارج الكلاس بل نريد فقط استخدام الدوال الموجودة في الـ UserBuilder
لذا يمكننا استخدام إنشاء دالة static بدلا من الـ constructor ولنسميها create أو make

class UserBuilder {
  private name: string;
  private email: string;
  private age?: number;
  private address?: string;
  private phone?: string;

  private constructor(name: string, email: string) {
    this.name = name;
    this.email = email;
  }

  static make(name: string, email: string) {
    return new UserBuilder(name, email);
  }

  // ...
}

لاحظ أن دالة make هي من تقوم بإنشاء object من الـ UserBuilder الآن
هكذا عندما نريد عمل object من الـ UserBuilder نستخدم الدالة make وليس الـ constructor

const user = UserBuilder.make("Ahmed", "[email protected]")
  .setAge(25)
  .setAddress("North Sinai, Egypt")
  .setPhone("01000000000")
  .build();

هذا الأمر قد يكون تفضيل شخصي والأمر يرجع إليك هل تريد استخدام الـ constructor أم دالة static
والفكرة هنا أننا نريد جعل الـ UserBuilder يستخدم فقط من خلال الدوال فقط
والغاء دور الـ constructor في الـ UserBuilder

تطبيق الـ Chaining Interfaces لتعين القيم الإجبارية في دوال

ذكرنا أننا بعد تطبيق الـ Builder Pattern وهو أننا مازلنا نستخدم الـ constructor أو دالة static لتعين القيم الإجبارية
وهذا قد ينشيء لنا نفس المشكلة التي كنا نواجهها في البداية

فمثلًا لدينا كلاس يدعى Product ولنفترض أن لديه 5 خصائص وهي name, price, stock, category و brand وكلها إجبارية
ولدينا بعض القيم الاختيارية مثل description و metadata
ستجد نفسك تقوم بتعين القيم الإجبارية في الـ constructor أو دالة static هكذا

const product = ProductBuilder.make(
  "Product Name",
  1000,
  1000,
  "Electronics",
  "Brand Name",
)
  .setDescription("Product Description")
  .setMetadata({ color: "Red" })
  .build();

هكذا ستجد أن الـ Builder Pattern لم يحل المشكلة التي كنا نواجهها في البداية
بل قمنا فقط بنقل المشكلة من كلاس Product إلى كلاس ProductBuilder

لا تقلق هذه المشكلة يمكننا حلها بإستخدام الـ Builder Pattern أيضًا عن فكرة تسمى Fluent Builder
والـ Fluent Builder هي فكرة قائمة على جعل كل شيء يتم تعينه كدوال وتكون دوال مقروءة وواضحة

وأيضًا سنستخدمها فكرة تسمى Chaining Interfaces
وهى فكرة تقوم بعمل interface لكل دالة وكل دالة تقوم بإرجاع الـ interface الذي تمثل الدالة التالية
لتجبر المستخدم على استخدام الدوال بشكل متسلسل وبترتيب معين بشكل اجباري


لنقم بتطبيق الفكرة على كلاس الـ User للتبسيط

const user = UserBuilder.make("Ahmed", "[email protected]")
  .setAge(25)
  .setAddress("North Sinai, Egypt")
  .setPhone("01000000000")
  .build();

حاليًا كلاس الـ User يحتوي على name, email, age, address و phone
وفقط الـ name و email هما الإجباريان والباقي اختياري لذا الفكرة هناك أننا نقوم بعمل interface لدالة setName
و interface لدالة setEmail
و interface للدوال الاختيارية setAge, setAddress و setPhone

وهكذا سيكون الكود كالتالي

interface INameSetter {
  setName(name: string): IEmailSetter;
}

interface IEmailSetter {
  setEmail(email: string): IUserOptionalSetter;
}

interface IUserOptionalSetter {
  setAge(age: number): IUserOptionalSetter;
  setAddress(address: string): IUserOptionalSetter;
  setPhone(phone: string): IUserOptionalSetter;

  build(): User;
}

interface IUserBuilder extends INameSetter, IEmailSetter, IUserOptionalSetter {}

لاحظ أننا قمنا بعمل interface يدعى INameSetter وهذا الـ interface يحتوي فقط على دالة setName
ولاحظ أن الدالة setName تقوم بإرجاع IEmailSetter وهو الـ interface الذي يحتوي على دالة setEmail
ولاحظ أن الدالة setEmail تقوم بإرجاع IUserOptionalSetter وهو الـ interface الذي يحتوي على الدوال الاختيارية
وكل من الدوال الاختيارية تقوم بإرجاع IUserOptionalSetter

ولدينا interface يدعى IUserBuilder وهو ما سيمثل شكل الـ UserBuilder النهائي

هكذا نحن قمنا بعمل تسلسل معين للدوال باستخدام الـ interface
بحيث نجبر المستخدم على تعين الـ name أولًا ثم email بالترتيب ثم الدوال الاختيارية

الآن كيف سيبدو الـ UserBuilder ؟

class UserBuilder implements IUserBuilder {
  private name: string;
  private email: string;
  private age?: number;
  private address?: string;
  private phone?: string;

  private constructor() {}

  static make(): INameSetter {
    return new UserBuilder();
  }

  public setName(name: string): IEmailSetter {
    this.name = name;
    return this;
  }

  public setEmail(email: string): IUserOptionalSetter {
    this.email = email;
    return this;
  }

  public setAge(age: number): IUserOptionalSetter {
    this.age = age;
    return this;
  }

  public setAddress(address: string): IUserOptionalSetter {
    this.address = address;
    return this;
  }

  public setPhone(phone: string): IUserOptionalSetter {
    this.phone = phone;
    return this;
  }

  public build() {
    return new User(this);
  }
}

حسنًا تأمل في الكود واستنتج ما يحدث
ستلاحظ أن دالة make تقوم بإرجاع INameSetter وهو الـ interface الذي يحتوي على دالة setName
بالتالي عندما تبدأ بعمل object من الـ UserBuilder ستجبر على استخدام الدالة setName أولًا

const user = UserBuilder.make()
  .setName("Ahmed");

لأن دالة make تقوم بإرجاع INameSetter لذا أنت مجبر على استخدام هذا الـ interface أولًا
وإذا حاولت مخالفة الترتيب واستدعاء دالة أخرى ستجد نفسك تواجه خطأ في الكود

const user = UserBuilder.make()
  .setAge(25); // Property 'setAge' does not exist on type 'INameSetter'.

الآن بعد أن قمنا باستدعاء الدالة setName ستجد أن setName تقوم بإرجاع IEmailSetter
وهكذا ستجد نفسك تجبر على استدعاء الدالة setEmail بعد الدالة setName

const user = UserBuilder.make()
  .setName("Ahmed")
  .setEmail("[email protected]");

الآن بعد أن أجبرت على تعين الـ name و email ستجد أن الدالة setEmail تقوم بإرجاع IUserOptionalSetter
وهبالتالي ستجد نفسك تجبر على استدعاء الدوال الاختيارية بعد الدالة setEmail بالتالي تصبح حرية المستخدم في تعين القيم الاختيارية

const user = UserBuilder.make()
  .setName("Ahmed")
  .setEmail("[email protected]")
  .setAge(25)
  .setAddress("North Sinai, Egypt")
  .setPhone("01000000000")
  .build();

وهكذا حللنا مشكلة تعين القيم الإجبارية في الـ Builder Pattern بشكل جميل ومنظم عن طريق الـ Chaining Interfaces


حسنًا إليك هذا السؤال، ماذا لو كان يجب على المستخدم تعين الـ email أو الـ phone بشكل إجباري ؟
بمعنى أنه يختار تعين الـ email أو الـ phone ولا يستطيع تركهما فارغين فيجب عليه تعين أحدهما

هنا يمكنك تطبيق هذا بالـ Chaining Interfaces بإضافة interface جديدة تسمى IEmailOrPhoneSetter
وهذه الـ interface تحتوي على دالتين وهما setEmail و setPhone وكل منهما تقوم بإرجاع IUserOptionalSetter

وهكذا سيجبر المستخدم على تعين الـ email أو الـ phone بشكل إجباري

interface INameSetter {
  setName(name: string): IEmailOrPhoneSetter;
}

interface IEmailOrPhoneSetter {
  setEmail(email: string): IUserOptionalSetter;
  setPhone(phone: string): IUserOptionalSetter;
}

interface IUserOptionalSetter extends IEmailOrPhoneSetter {
  setAge(age: number): IUserOptionalSetter;
  setAddress(address: string): IUserOptionalSetter;
  build(): User;
}

interface IUserBuilder extends INameSetter, IEmailSetter, IUserOptionalSetter {}

class UserBuilder implements IUserBuilder {
  // ...
}

وهكذا يمكن للمستخدم تعين الـ email أو الـ phone بشكل إجباري

const user = UserBuilder.make()
  .setName("Ahmed")
  .setPhone("01000000000") // or use setEmail
  .build();

ما هو الـ Fluent Builder ؟

الـ Fluent Builder هو فكرة تقوم بجعل الـ Builder Pattern أكثر وضوحًا وسهولة

لو أردنا تطبيق الـ Fluent Builder على كلاس الـ AuditLogBuilder فسينتهي بنا الأمر من تحويله
من هذا الشكل

const auditLog = new AuditLogBuilder("update", "Product", 1)
  .setMessage("Product updated")
  .setOldProperties({ name: "Old Name" })
  .setNewProperties({ name: "New Name" })
  .setMetadata({ reason: "Copy-right issue" })
  .setActorId(1)
  .build();

إلى هذا الشكل

const auditLog = AuditLogBuilder.make()
  .updated()
  .by($user)
  .on($product)
  .from({ name: "Old Name" })
  .to({ name: "New Name" })
  .becauseOf("Copy-right issue")
  .build();

لاحظ أن تسلسل الدوال ومسمياتها كأنها جملة
Make an audit log that updated by user on product from old name to new name because of copy-right issue

هذا هو الـ Fluent Builder وهو فكرة تقوم بجعل الـ Builder Pattern أكثر وضوحًا وسهولة ومقروءة ومفهومة

الختام

هناك شيء أخير أريد أن أذكره وهو أننا يمكننا استخدام الـ Builder Pattern في العديد من الأمور
وأفكار الـ Builder Pattern لا تنتهي هنا ويمكننا استخدامها في العديد من الأمور
لكن أود أن اكتفي بما قدمته لك في هذه المقالة

لكن سأذكر لك شيء أخير يمكنك استخدام الـ Builder Pattern فيها

تخيل لو كان لدينا ProductBuilder ولدينا أنواع خاصة من المنتجات وتكرر دائمًا
يمكنا عمل دوال لكل نوع من هذه المنتجات تقوم بتعين القيم الخاصة بها بشكل تلقائي

class ProductBuilder {
  // ...

  public setElectronics() {
    this.setCategory("Electronics");
    this.setBrand("El Araby Group");
    this.setInitialStock(1000);
    return this;
  }

  public setClothes($size: string, $color: string) {
    this.setCategory("Clothes");
    this.setBrand("Sutra");
    this.setInitialStock(500);
    this.setMetadata({
      size: $size,
      color: $color,
    });
    // ...
    return this;
  }
}

const product_1 = ProductBuilder.make()
  .setName("Product Name")
  .setPrice(1000)
  .setElectronics()
  .build();

const product_2 = ProductBuilder.make()
  .setName("Product Name")
  .setPrice(1000)
  .setClothes("XL", "Red")
  .build();

لاحظ أننا هنا قمنا بعمل دوال مثل setElectronics و setClothes وهذه الدوال تقوم بتعين القيم الخاصة بكل نوع من المنتجات بشكل تلقائي
باختصار هي دوال تختصر الكود وتجعله أكثر وضوحًا وسهولة وأنت يمكنك عمل العديد من هذه الدوال حسب احتياجاتك


يوجد بالطبع أفكار وأمور أخرى يمكننا استخدام الـ Builder Pattern فيها
لكن أظن أن هذا يكفي في هذه المقالة، فالهدف في النهاية هو أن أعطيك فكرة عن الـ Builder Pattern
وأظن أنني قد قمت بذلك لذا أتمنى أن تكون قد استفدت من هذا الشرح