الـ Builder Pattern عامل البناء الشهير
السلام عليكم ورحمة الله وبركاته
المقدمة
اليوم سيكون يومًا ممتعًا لبناء بعض الأمور الكبيرة والمعقدة بطريقة سهلة وبسيطة
بإستخدام الـ 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,deletemessage: رسالة توضح الحدثtrackableModel: الـmodelالذي نتبعه أو الذي حدث عليه الحدث مثلUser,Product,OrdertrackableModelId: الـidالخاص بالـmodeloldProperties: القيم القديمة للـ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
تطبيق عملي للـ Builder Pattern
الآن ما هى الخطوة التالية ؟
حسنًا الآن عندما تقوم بعمل 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);
لاحظ الآن أننا بمجرد النظر إلى الكود عرفنا المتغيرات والقيم التي نريدها بشكل
واضح
دالة الـ Build
هل انتهينا ؟
بالطبع لا فنحن لم نقم بعمل 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
مقارنة وتطبيق الـ Builder Pattern على الأمثلة السابقة
الآن هل تتذكر الأمثلة السابقة التي كانت تحتوي على الكثير من البيانات وكانت مربكة
؟
دعني أذكرك بها
// إنشاء منتج جديد
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', 'eltabaraniahmed@gmail.com')
.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', 'eltabaraniahmed@gmail.com')
.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', 'eltabaraniahmed@gmail.com')
.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('eltabaraniahmed@gmail.com');
الآن بعد أن أجبرت على تعين الـ name و email ستجد أن الدالة setEmail تقوم
بإرجاع IUserOptionalSetter
وهبالتالي ستجد نفسك تجبر على استدعاء الدوال الاختيارية بعد الدالة setEmail
بالتالي تصبح حرية المستخدم في تعين القيم الاختيارية
const user = UserBuilder.make()
.setName('Ahmed')
.setEmail('eltabaraniahmed@gmail.com')
.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
وأظن أنني قد قمت بذلك لذا أتمنى أن تكون قد استفدت من هذا الشرح