نمط التركيب
جزء من قسم Object Oriented Programming في رحلة C++ على Coddy — الدرس 101 من 104.
يتيح لك نمط Composite التعامل مع الكائنات الفردية ومجموعات الكائنات بشكل موحد. فهو يقوم بتركيب الكائنات في هياكل شجرية حيث تشترك كل من العناصر الفردية وحاويات العناصر في نفس الواجهة. وهذا مثالي لتمثيل التسلسلات الهرمية مثل أنظمة الملفات، أو المخططات التنظيمية، أو مكونات واجهة المستخدم.
يتكون النمط من ثلاثة أجزاء رئيسية: واجهة Component التي تحدد العمليات المشتركة، وفئات Leaf التي تمثل الكائنات الفردية، وفئات Composite التي تحتوي على عناصر تابعة وتفوض العمليات إليها:
#include <iostream>
#include <memory>
#include <vector>
#include <string>
// واجهة المكون (Component interface)
class FileSystemItem {
public:
virtual void display(int indent = 0) const = 0;
virtual int getSize() const = 0;
virtual ~FileSystemItem() = default;
};
// Leaf - تمثل الملفات الفردية
class File : public FileSystemItem {
std::string name;
int size;
public:
File(const std::string& n, int s) : name(n), size(s) {}
void display(int indent = 0) const override {
std::cout << std::string(indent, ' ') << name
<< " (" << size << " KB)\n";
}
int getSize() const override { return size; }
};
// Composite - يحتوي على مكونات أخرى
class Folder : public FileSystemItem {
std::string name;
std::vector<std::shared_ptr<FileSystemItem>> children;
public:
Folder(const std::string& n) : name(n) {}
void add(std::shared_ptr<FileSystemItem> item) {
children.push_back(item);
}
void display(int indent = 0) const override {
std::cout << std::string(indent, ' ') << "[" << name << "]\n";
for (const auto& child : children) {
child->display(indent + 2);
}
}
int getSize() const override {
int total = 0;
for (const auto& child : children) {
total += child->getSize();
}
return total;
}
};يقوم المركب Folder بتخزين العناصر التابعة وينفذ العمليات من خلال التكرار عبرها. عندما تستدعي getSize() على مجلد، فإنه يحسب بشكل متكرر الحجم الإجمالي لجميع العناصر الموجودة بداخله. لا يحتاج كود العميل إلى معرفة ما إذا كان يتعامل مع ملف أو مجلد - فكلاهما يستجيب لنفس الواجهة.
استخدم نمط Composite عندما تحتاج إلى تمثيل تسلسلات هرمية (جزء-كل) وتريد من العملاء التعامل مع الكائنات الفردية والتكوينات بشكل موحد.
التحدي
سهللنقم ببناء نظام مخطط تنظيمي (Organization Chart) باستخدام نمط Composite. ستقوم بإنشاء هيكل هرمي حيث يمكن معاملة كل من الموظفين الأفراد والأقسام (التي تحتوي على موظفين آخرين أو أقسام فرعية) بشكل موحد. هذا يحاكي كيفية هيكلة الشركات الحقيقية — حيث تحتوي الأقسام على أشخاص وأقسام أخرى، مما يشكل هيكلاً شجرياً.
ستقوم بتنظيم الكود الخاص بك عبر ثلاثة ملفات:
OrgComponent.h: تحديد واجهة المكون (component interface) التي سيقوم كل من الموظفين والأقسام بتنفيذها.قم بإنشاء فئة مجردة (abstract class) باسم
OrgComponentتحتوي على:getName()— تعيد اسم المكونgetSalary()— تعيد إجمالي الراتب (بالنسبة للموظفين، راتبهم الخاص؛ وبالنسبة للأقسام، مجموع جميع الرواتب الموجودة بداخلها)display(int indent = 0)— تعرض المكون مع مسافة بادئة مناسبة
قم بتضمين مدمر افتراضي (virtual destructor).
Organization.h: تنفيذ فئات الورقة (leaf) والمركب (composite).قم بإنشاء فئة
Employee(الورقة) التي تخزن الاسم والراتب. يجب أن تقوم دالةdisplay()الخاصة بها بطباعة معلومات الموظف بهذا التنسيق:[indent spaces]- [name] ($[salary])قم بإنشاء فئة
Department(المركب) التي تخزن اسماً ومجموعة من الأبناء من نوعOrgComponentباستخدامstd::shared_ptr. قم بتنفيذ:add(std::shared_ptr<OrgComponent> component)— تضيف ابناً إلى القسمgetSalary()— تحسب إجمالي رواتب جميع الأعضاء بشكل متكرر (recursively)display()— تطبع اسم القسم بين أقواس مربعة، ثم تعرض جميع الأبناء بمسافة بادئة متزايدة (أضف مسافتين لكل مستوى)
يجب أن يكون تنسيق عرض القسم كالتالي:
[indent spaces][Department Name] [children displayed with indent + 2]main.cpp: بناء وعرض الهيكل التنظيمي.اقرأ أربعة مدخلات:
- اسم الشركة (string)
- اسم القسم (string)
- اسم الموظف الأول وراتبه (بتنسيق:
name,salary) - اسم الموظف الثاني وراتبه (بتنسيق:
name,salary)
قم ببناء هذا الهيكل: أنشئ شركة (قسم في المستوى الأعلى)، وأضف قسماً فرعياً إليها، وأضف كلا الموظفين إلى ذلك القسم الفرعي. ثم اعرض المنظمة بأكملها واطبع إجمالي راتب الشركة.
بعد عرض الهيكل، اطبع:
Total Salary: $[amount]
على سبيل المثال، مع المدخلات TechCorp، و Engineering، و Alice,75000، و Bob,65000:
[TechCorp]
[Engineering]
- Alice ($75000)
- Bob ($65000)
Total Salary: $140000مع المدخلات StartupInc، و Development، و Carol,80000، و Dave,70000:
[StartupInc]
[Development]
- Carol ($80000)
- Dave ($70000)
Total Salary: $150000لاحظ كيف تعمل getSalary() بشكل موحد سواء تم استدعاؤها على موظف أو قسم — حيث يقوم القسم تلقائياً بتجميع الرواتب من جميع أعضائه. لا يحتاج كود العميل (client code) إلى التمييز بين الموظفين الأفراد والأقسام الكاملة عند حساب الإجماليات أو عرض الهيكل الهرمي.
ورقة مرجعية
يعامل **نمط التركيب (Composite pattern)** الكائنات الفردية ومجموعات الكائنات بشكل موحد من خلال تكوينها في هياكل شجرية حيث تشترك كل من العناصر المفردة والحاويات في نفس الواجهة.
يتكون النمط من ثلاثة أجزاء رئيسية:
- المكون (Component): واجهة تحدد العمليات المشتركة
- الورقة (Leaf): فئات تمثل الكائنات الفردية
- المركب (Composite): فئات تحتوي على أبناء وتفوض العمليات إليهم
مثال على تنفيذ نظام ملفات:
#include <iostream>
#include <memory>
#include <vector>
#include <string>
// واجهة المكون
class FileSystemItem {
public:
virtual void display(int indent = 0) const = 0;
virtual int getSize() const = 0;
virtual ~FileSystemItem() = default;
};
// الورقة - تمثل الملفات الفردية
class File : public FileSystemItem {
std::string name;
int size;
public:
File(const std::string& n, int s) : name(n), size(s) {}
void display(int indent = 0) const override {
std::cout << std::string(indent, ' ') << name
<< " (" << size << " KB)\n";
}
int getSize() const override { return size; }
};
// المركب - يحتوي على مكونات أخرى
class Folder : public FileSystemItem {
std::string name;
std::vector<std::shared_ptr<FileSystemItem>> children;
public:
Folder(const std::string& n) : name(n) {}
void add(std::shared_ptr<FileSystemItem> item) {
children.push_back(item);
}
void display(int indent = 0) const override {
std::cout << std::string(indent, ' ') << "[" << name << "]\n";
for (const auto& child : children) {
child->display(indent + 2);
}
}
int getSize() const override {
int total = 0;
for (const auto& child : children) {
total += child->getSize();
}
return total;
}
};يقوم المركب بتخزين الأبناء باستخدام std::shared_ptr وينفذ العمليات من خلال التكرار عبرها. تعمل العمليات مثل getSize() بشكل متكرر، حيث تحسب الإجماليات عبر التسلسل الهرمي بأكمله.
استخدم نمط التركيب (Composite) عندما تحتاج إلى تمثيل التسلسلات الهرمية للجزء والكل وتريد من العملاء التعامل مع الكائنات الفردية والتركيبات بشكل موحد.
جرّب بنفسك
#include <iostream>
#include <string>
#include <memory>
#include <sstream>
#include "Organization.h"
int main() {
// قراءة المدخلات
std::string companyName;
std::string departmentName;
std::string employee1Input;
std::string employee2Input;
std::getline(std::cin, companyName);
std::getline(std::cin, departmentName);
std::getline(std::cin, employee1Input);
std::getline(std::cin, employee2Input);
// دالة lambda مساعدة لتحليل تنسيق "name,salary"
auto parseEmployee = [](const std::string& input) -> std::pair<std::string, int> {
size_t commaPos = input.find(',');
std::string name = input.substr(0, commaPos);
int salary = std::stoi(input.substr(commaPos + 1));
return {name, salary};
};
auto [name1, salary1] = parseEmployee(employee1Input);
auto [name2, salary2] = parseEmployee(employee2Input);
// TODO: إنشاء الشركة كقسم رئيسي (top-level Department)
// TODO: إنشاء قسم فرعي
// TODO: إنشاء كائنين من نوع Employee باستخدام البيانات التي تم تحليلها
// TODO: إضافة الموظفين إلى القسم الفرعي
// TODO: إضافة القسم الفرعي إلى الشركة
// TODO: عرض هيكل المنظمة بالكامل
// TODO: طباعة إجمالي الرواتب بالتنسيق التالي: Total Salary: $[amount]
return 0;
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP
الملفات الخارجيةبناء وتجميع (Compilation) لغة C++ملفات الـ Header وملفات الـ Sourceالـ Namespaces والـ Scopeمقدمة في الـ OOP في C++الـ Classes مقابل الـ Objectsالمؤشر 'this'الـ Methods (الدوال الأعضاء)الـ Attributes (بيانات الأعضاء)أساسيات الـ Ctors والـ Dtorsمراجعة - آلة حاسبة بسيطة4خصائص الـ Class
أعضاء الـ Instance مقابل الـ Staticدوال الـ Getters والـ Settersدوال الأعضاء الثابتة (Const)الكلمة المفتاحية Mutableالدوال والمتغيرات الساكنة (Static)الدوال والفئات الصديقة (Friend)مراجعة - مدير الحساب البنكي7الوراثة
الوراثة الأساسيةمستويات الوصول في الوراثةترتيب استدعاء الـ Ctor والـ Dtorإعادة تعريف الدوال (Method Overriding)الدوال الافتراضية والـ VTableالوراثة المتعددةالوراثة الافتراضيةمراجعة - هيكلية الموظفين10نظرة عامة على STL
نظرة عامة وفلسفة STLحاويات STLالـ Iteratorsخوارزميات STLالـ Functors وتعبيرات Lambdaمراجعة - تكرار الكلمات13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factory و Abstract Factoryنمط Builderنمط Observerنمط Strategy2إدارة الذاكرة
ذاكرة Stack مقابل Heapالمؤشرات والمراجعالذاكرة الديناميكية (new/delete)المؤشرات الذكية في C++RAII في C++مراجعة - مدير المصفوفات الديناميكية5التغليف (Encapsulation)
محددات الوصول في C++محددات الوصول بشكل متعمقإخفاء المعلوماتStruct مقابل Classالأصناف المتداخلة والداخليةمراجعة - نظام سجلات الطلاب8تعدد الأشكال (Polymorphism)
تعدد الأشكال: وقت التجميع مقابل وقت التشغيلالتحميل الزائد للدوال (Function Overloading)مراجعة الدوال الافتراضية (Virtual Functions)الدوال الافتراضية البحتة (Pure Virtual Functions)الأصناف المجردة (Abstract Classes)تصميم الواجهات (Interface) في C++التحويل الديناميكي (Dynamic Casting) و RTTIملخص - حاسبة الأشكال11مفاهيم OOP المتقدمة
التركيب مقابل الوراثةالـ Mixins عبر CRTPنمط Pimplمحو النوع (Type Erasure)أصناف Enum والأنواع القويةمعالجة الاستثناءات في OOPتسلسلات الاستثناءات المخصصة14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط قالب الطريقةنمط الحالةنمط التركيبRAII كنمط3المنشئات والموادم
المنشئ الافتراضيالمنشئ ذو المعاملاتمنشئ النسخمنشئ النقلقوائم تهيئة المنشئالمنشئات المفوضةتعمق في الموادمقاعدة الثلاثة / الخمسة / الصفرمراجعة - فئة String6تحميل العوامل (Operator Overloading)
مقدمة في تحميل العواملتحميل العوامل الحسابيةتحميل عوامل المقارنةعوامل Streamتحميل عامل التعيينتحميل العوامل [] و ()عوامل تحويل النوعمراجعة - Matrix Class9القوالب
قوالب الدوالقوالب الأصنافتخصيص القوالبالقوالب المتغيرةأساسيات SFINAE و Type Traitsمراجعة - الحاويات العامة