فرض کنیم یک برنامه مدیریت اعضای یک سازمان داریم که توی اون میخوایم مشخصات همه اعضای سازمان رو بدست بیاریم و نمایش بدیم. همونطور که میدونیم یک سازمان شامل بخشهای مختلفی میشه و هر بخش هم میتونه زیربخشهایی داشته باشه:
|-- Organization
|-- new Founder("Alex")
|-- new HR("Morgan")
|-- Officers
|-- new CEO("John")
|-- new CTO("Mario")
|-- Employees
|-- IT
|-- Designers
|-- new Designer("Sarah")
|-- new Designer("John")
|-- new Designer("Emily")
|-- new Designer("Mario")
با توجه به ساختار بالا، چطوری میتونیم مشخصات همه اعضا رو بدست بیاریم؟ شاید چیزی که به ذهنمون میرسه این باشه که یک حلقه یا یک تابع بازگشتی بنویسیم:
organization.forEach(function(organ) {
const output = [];
if (organ instanceof Founder) {
>> output.push(organ.details());
}
if (organ instanceof Officers) {
organ.forEach(function(officer)) {
if (officer instanceof CEO) {
>> output.push(officer.information());
}
if (officer instanceof CTO) {
>> output.push(officer.getDetails());
}
}
// rest of the ugly code ...
}
});
توی این کد:
۱. داریم روی همه اعضا پیمایش میکنیم
۲. وقتی به هر قسمت رسیدیم بررسی میکنیم که این قسمت از نوع کدوم کلاس هست
۳. با توجه به نوع کلاس بررسی میکنیم که آیا این عضو زیربخشهایی هم داره یا نه
۴. اگه زیربخشی وجود داشت، دوباره یک حلقه دیگه میزنیم
۵. اگه زیربخشی وجود نداشت، متد مربوط به گرفتن مشخصات عضو رو صدا میزنیم
این کد عجیب چند نکته داره:
ما باید کلاس و نوع همه اعضا رو بدونیم
ما باید متد مربوط به گرفتن مشخصات اعضا رو بدونیم (خطهای ۵ و ۱۱ و ۱۵)
مشکل زمانی بیشتر میشه عمقهای بیشتری به سازمان اضافه کنیم
همین موارد باعث میشه که کیفیت کد ما پایین بیاد و برنامههایی غیرقابل توسعه بنویسیم. اما خب الگوها برای همین چیزها ساخته شدن 🤷♂️
الگویی که اینجا به کار ما میاد، الگوی Composite هست.
این الگو کمک میکنه تا بتونیم با گروهی از آبجکتها که نوعهای مختلفی دارن، اما باید به شکل یک ساختار درختی کنار هم قرار بگیرن، طوری کار کنیم که انگار همگی نوعهای یکسانی دارن
الگوی Composite زیر مجموعه الگوهای Structural - الگوهایی که کمک میکنن تا آبجکتهای موجود طوری با هم در تعامل باشن تا بتونیم ساختارهای بزرگتری طراحی کنیم - هست. این الگو که به اون Object Tree هم گفته میشه، بسیار ساده هست و پیادهسازی کاملاً راحتی داره.
توی پیادهسازی این الگو اینترفیسها به کار ما میان تا با ساختن یک لایه انتزاعی بتونیم با همه آبجکتها - بدون دونستن نوع اونها - کار کنیم. توی این الگو چندتا مفهوم وجود داره:
Leaf و Container
۱. Leaf یا برگ که به عضوی از ساختار درختی گفته میشه که هیچ زیرعضوی نداره. توی مثال بالا، Designer یک Leaf هست که هیچ زیربخشی نداره. به اون عضو ساده هم گفته میشه.
۲. Container که به عضوی گفته میشه که شامل زیربخشها و همچنین Leaf هاست. به اون عضو مرکب یا Composite هم گفته میشه. توی مثال بالا Organization و Employees یک Container هستن که شامل عضوهای ساده و زیربخشها میشن.
۳. قسمت کلاینت که به قسمتی از برنامه گفته میشه که کار اصلی و مورد نظرمون رو انجام میده. توی مثال بالا، کار مد نظر ما نمایش دادن مشخصات همه اعضای سازمان بود. حلقهای که نوشتیم قسمت کلاینت هست.
اسم این کلاس رو میذاریم SimpleOrgan. این کلاس باید از اینترفیس Organ تبعیت کنه:
class SimpleOrgan implements Organ {
constructor(name) {
this.name = name;
}
public getInformation() {
return `My name is ${this.name}`;
}
public getRevenue() {}
}
گفتیم که اعضای مرکب میتونن شامل اعضای ساده و همچنین اعضای مرکب دیگه باشن. اسم این کلاس رو میذاریم CompoundOrgan به معنی عضو مرکب. این کلاس هم باید اینترفیس Organ رو پیادهسازی کنه:
class CompoundOrgan implements Organ {
constructor(name) {
this.name = name;
}
public getInformation() {
return `This is ${this.name} organ`;
}
public getRevenue() {}
}
این کلاس چون شامل اعضای مرکب هست، ما باید به اون یک پراپرتی و یک متد اضافه کنیم برای اضافه کردن و نگهداری زیرعضوها:
class CompoundOrgan implements Organ {
>> public children = []; // Array of organs
// ...
>> public add(organ: Organ) {
>> this.children.push(organ);
>> }
public getInformation() {
let output = `This is ${this.name} organ`;
output += ` It contains ${this.children.length} members`;
return output;
}
}
نکتهای که توی کلاسهای مرکب باید در نظر داشته باشیم اینه که این کلاسها بیشتر کارشون رو محول میکنن به زیربخشهای خودشون. پس باید متد getInformation رو کاملتر کنیم:
class CompoundOrgan implements Organ {
// ...
public getInformation() {
let output = `This is ${this.name} organ`;
output += ` It contains ${this.children.length} members`;
>> this.children.forEach(organ => {
>> output += organ.getInformation();
>> });
return output;
}
}
توی این کد و توی خط ۸ روی زیربخشها یک حلقه زدیم تا اطلاعات زیربخشها رو هم داشته باشیم.
و تمام! حالا میتونیم قسمت کلاینت رو بنویسیم و از کدهایی که نوشتیم استفاده کنیم. ابتدا ساختار درختی سازمان رو میسازیم:
const organization = new CompoundOrgan('Main');
organization.add(new SimpleOrgan('Alex as Founder'));
organization.add(new SimpleOrgan('Morgan as HR'));
const officers = new CompoundOrgan('Officers');
officers.add(new SimpleOrgan('John as CEO'));
officers.add(new SimpleOrgan('John as CTO'));
organization.add(officers);
const employees = new CompoundOrgan('Employees');
const it = new CompoundOrgan('IT');
const designers = new CompoundOrgan('Desingers');
designers.add(new SimpleOrgan('Sarah as Desinger'));
designers.add(new SimpleOrgan('John as Desinger'));
designers.add(new SimpleOrgan('Emily as Desinger'));
designers.add(new SimpleOrgan('Mario as Desinger'));
it.add(designers);
employees.add(it);
organization.add(employees);
>> console.log(organization.getInformation());
توی این کد دقیقاً ساختار درختی سازمانی که ابتدای پست داشتیم رو پیادهسازی کردیم. ابتدا بخشهای مرکب رو ساختیم و به اون زیربخشها رو اضافه کردیم. برای گرفتن مشخصات همه اعضا، تنها کاری که باید انجام میدادیم، صدا زدن متد getInformation توی خط ۲۳ از بالاترین عضو بود 👌
خروجی که تولید میشه (با مقداری Indent برای خوانایی بهتر) به این صورت هست:
- This is Main organ. It contains 4 members
- My name is Alex as Founder
- My name is Morgan as HR
- This is Officers organ. It contains 2 members
- My name is John as CEO
- My name is John as CTO
- This is Employees organ. It contains 1 members
- This is IT organ. It contains 1 members
- This is Desingers organ. It contains 4 members
- My name is Sarah as Desinger
- My name is John as Desinger
- My name is Emily as Desinger
- My name is Mario as Desinger