الـ Builder Pattern عامل البناء المشهور والمحبوب
السلام عليكم ورحمة الله وبركاته
الفهرس
- المقدمة
- ما المشكلة التي يحلها الـ Builder Pattern ؟
- ما هو الـ Builder Pattern ؟
- بعض التحسينات على الـ Builder Pattern
- كيفية اجبار الأخرين على استخدام الـ Builder Pattern فقط
- استخدام دالة static بدلا من الـ constructor في الـ Builder Pattern
- تطبيق الـ Chaining Interfaces لتعين القيم الإجبارية في دوال
- ما هو الـ Fluent Builder ؟
- الختام
المقدمة
اليوم سيكون يومًا ممتعًا لبناء بعض الأمور الكبيرة والمعقدة بطريقة سهلة وبسيطة
بإستخدام الـ Builder Pattern
في هذه المقال البسيطة سنتعرف على أحد الـ Design Patterns
وهو عامل البناء
الشهير والمحبوب الـ Builder Pattern
الـ Builder Pattern
ينتمي إلى عائلة الـ Creational Design Patterns
المسؤولة
عن عملية إنشاء الـ object
بطريقة مرنة وبسيطة ومنظمة
والـ Builder Pattern
يعد من أشهر الطرق لإنشاء الـ object
المعقدة التي تحتوي
على العديد من الخصائص والمتغيرات
لكن قبل أن نبدأ في شرح الـ Builder Pattern
دعونا نتعرف على المشكلة التي يحلها
الـ Builder Pattern
ما المشكلة التي يحلها الـ Builder Pattern ؟
حسنًا لنتخيل أن لدينا كلاس يدعى AuditLog
وهذا الكلاس يهتم بتسجيل الأنشطة التي
تحدث في التطبيق
وهذا الكلاس يحتوي على العديد من الخصائص مثل:
action
: الحدث الذي حدث مثلcreate
,update
,delete
message
: رسالة توضح الحدثtrackableModel
: الـmodel
الذي نتبعه أو الذي حدث عليه الحدث مثلUser
,Product
,Order
trackableModelId
: الـid
الخاص بالـmodel
oldProperties
: القيم القديمة للـmodel
في حالة حدث تغير في البياناتnewProperties
: القيم الجديدة للـmodel
في حالة حدث تغير في البياناتmetadata
: بيانات إضافية في حالة أردنا تسجيل المزيد من المعلوماتactorId
: الـid
الخاص بالشخص الذي قام بالحدث
لنلقِ نظرة على كلاس الـ 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
؟
الكود فيه عدة مشاكل منها:
- يوجد الكثير من البيانات يجب أن تمر في الـ
constructor
ومعظمها قد تكون اختيارية - يجب عليك حفظ ترتيب البيانات في الـ
constructor
ونوعها والقيم الافتراضية لها - بسبب أن معظم البيانات اختيارية، ستجد نفسك تقوم بوضع العديد من
null
في البيانات التي لا تحتاجها أو تسند قيمة افتراضية لها - سريعًا ما ستجد نفسك نسيت ترتيب البيانات أو نوعها أو القيم الافتراضية لها
- وأخيرًا، الكود يبدو مربكًا وغير منظم ... إلخ
والحل ؟ أكيد لا داعي لأن أخبرك بأن الحل هو الـ 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
أكثر وضوحًا وسهولة
- وتفضل أن تكون جميع القيم يتم تعينها كدوال سواء كانت إجبارية أو اختيارية وهذا
عن طريق:
- ارجاع الـ
this
من كل دالة لكي يمكنك استدعاء الدوال بشكل متسلسل - واستخدام الـ
Chaining Interfaces
لتحديد ترتيب الدوال بشكل إجباري ومتسلسل
- ارجاع الـ
- وأيضًا تفضل أن تكون مسميات الدوال واضحة ومفهومة كأنك تكتب جملة
لو أردنا تطبيق الـ 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
وأظن أنني قد قمت بذلك لذا أتمنى أن تكون قد استفدت من هذا الشرح