در پروژههای PHP گاهی با Objectهایی روبهرو میشویم که بعضی از آنها بهصورت مستقل کار میکنند و بعضی دیگر شامل چند Object فرزند هستند.
برای مثال در یک فروشگاه اینترنتی ممکن است ساختار زیر را داشته باشیم:
فروشگاه
├── دستهبندی موبایل
│ ├── گوشی سامسونگ
│ └── گوشی شیائومی
├── دستهبندی لپتاپ
│ ├── لپتاپ لنوو
│ └── لپتاپ ایسوس
└── دستهبندی لوازم جانبی
├── موس
└── کیبورد
در این ساختار:
- محصول یک Object مستقل است.
- دستهبندی شامل چند محصول یا دستهبندی دیگر است.
- فروشگاه شامل چند دستهبندی است.
اگر بخواهیم برای هرکدام منطق جداگانهای بنویسیم، ممکن است کد پیچیده شود.
مثلاً برای محاسبه قیمت، نمایش اطلاعات یا پیمایش این ساختار، مجبور شویم دائماً بررسی کنیم که Object فعلی یک محصول است یا یک گروه.
Composite Pattern برای حل همین مسئله استفاده میشود. این الگو اجازه میدهد یک Object مستقل و گروهی از Objectها را از طریق یک Interface مشترک مدیریت کنیم. <Cite refs={["turn0search8","turn0search1"]} />
Composite Pattern چیست؟
Composite Pattern یک Structural Design Pattern است که Objectها را در قالب ساختار درختی سازماندهی میکند.
هدف اصلی آن این است که:
> یک Object و مجموعهای از Objectها بتوانند از طریق یک Interface مشترک استفاده شوند.
در مستندات DesignPatternsPHP، هدف Composite اینگونه بیان شده است که گروهی از Objectها مانند یک نمونه مستقل از همان نوع مدیریت شوند. <Cite refs={["turn0search8","turn0search5"]} />
برای مثال در سیستم فایل:
File
Folder
هر فایل یک Object مستقل است، اما پوشه میتواند شامل چند فایل یا پوشه دیگر باشد.
با Composite میتوانیم هم فایل و هم پوشه را از طریق یک قرارداد مشترک مدیریت کنیم.
# ساختار Composite Pattern
Composite Pattern معمولاً از سه بخش اصلی تشکیل میشود:
Component
├── Leaf
└── Composite
├── Leaf
├── Leaf
└── Composite
1. Component
Component قرارداد مشترک تمام Objectها را مشخص میکند.
مثلاً:
interface FileSystemItem
{
public function getSize(): int;
}
هم فایل و هم پوشه باید این Interface را پیادهسازی کنند.
2. Leaf
Leaf یک Object نهایی است که فرزند دیگری ندارد.
مثلاً:
class File implements FileSystemItem
{
public function __construct(
private string $name,
private int $size
) {
}
public function getSize(): int
{
return $this->size;
}
}
در این مثال، File یک Leaf است.
3. Composite
Composite یک Object ترکیبی است که میتواند چند Component دیگر را نگهداری کند.
مثلاً:
class Folder implements FileSystemItem
{
private array $items = [];
public function add(FileSystemItem $item): void
{
$this->items[] = $item;
}
public function getSize(): int
{
$total = 0;
foreach ($this->items as $item) {
$total += $item->getSize();
}
return $total;
}
}
در اینجا Folder میتواند شامل فایل یا پوشه دیگری باشد.
# مشکل بدون استفاده از Composite Pattern
فرض کنید میخواهیم حجم یک پوشه را محاسبه کنیم.
بدون Composite ممکن است چنین کدی بنویسیم:
foreach ($folderItems as $item) {
if ($item instanceof File) {
$total += $item->getSize();
}
if ($item instanceof Folder) {
foreach ($item->getItems() as $child) {
// محاسبه حجم فرزندان
}
}
}
اگر ساختار چند سطحی شود، باید شرطها و حلقههای بیشتری اضافه کنیم.
اما با Composite، خود Folder مسئول محاسبه حجم فرزندانش میشود و Client فقط متد مشترک را صدا میزند:
$total = $folder->getSize();
این موضوع باعث میشود Client نیازی نداشته باشد بداند Object فعلی فایل است یا پوشه.
# پیادهسازی Composite Pattern در PHP
در این مثال یک سیستم فایل ساده میسازیم.
ساختار نهایی ما به شکل زیر خواهد بود:
FileSystemItem
├── File
└── Folder
مرحله اول: ساخت Component Interface
ابتدا Interface مشترک را ایجاد میکنیم:
interface FileSystemItem
{
public function getName(): string;
public function getSize(): int;
}
تمام Objectهای سیستم فایل باید این دو متد را داشته باشند.
مرحله دوم: ساخت کلاس File
کلاس File یک Leaf است:
class File implements FileSystemItem
{
public function __construct(
private string $name,
private int $size
) {
}
public function getName(): string
{
return $this->name;
}
public function getSize(): int
{
return $this->size;
}
}
مثلاً:
$file = new File(
'photo.jpg',
1200
);
در این مثال حجم فایل برابر با 1200 است.
مرحله سوم: ساخت کلاس Folder
حالا کلاس Folder را میسازیم:
class Folder implements FileSystemItem
{
private array $items = [];
public function __construct(
private string $name
) {
}
public function add(FileSystemItem $item): void
{
$this->items[] = $item;
}
public function getName(): string
{
return $this->name;
}
public function getSize(): int
{
$total = 0;
foreach ($this->items as $item) {
$total += $item->getSize();
}
return $total;
}
}
نکته مهم این است که متد add() یک FileSystemItem دریافت میکند:
public function add(FileSystemItem $item): void
بنابراین میتوانیم هم File و هم Folder را به آن اضافه کنیم.
مرحله چهارم: ساخت ساختار درختی
حالا چند فایل و پوشه میسازیم:
$documents = new Folder('documents');
$documents->add(
new File('resume.pdf', 500)
);
$documents->add(
new File('invoice.pdf', 800)
);
حالا یک پوشه دیگر ایجاد میکنیم:
$projects = new Folder('projects');
$projects->add(
new File('index.php', 300)
);
$projects->add(
new File('style.css', 200)
);
سپس پوشه projects را داخل documents قرار میدهیم:
$documents->add($projects);
ساختار نهایی:
documents
├── resume.pdf
├── invoice.pdf
└── projects
├── index.php
└── style.css
مرحله پنجم: محاسبه حجم کل
حالا فقط کافی است متد getSize() را روی پوشه اصلی صدا بزنیم:
echo $documents->getSize();
محاسبه به این شکل انجام میشود:
resume.pdf = 500
invoice.pdf = 800
index.php = 300
style.css = 200
حجم کل = 1800
خروجی:
1800
در اینجا Folder خودش حجم فایلهای فرزند و پوشههای داخلی را محاسبه میکند.
Client لازم نیست بداند داخل documents چه نوع Objectهایی قرار دارند.
# ساخت متد نمایش ساختار درختی
میتوانیم یک متد برای نمایش ساختار فایلها نیز اضافه کنیم.
ابتدا Interface را توسعه میدهیم:
interface FileSystemItem
{
public function getName(): string;
public function getSize(): int;
public function print(string $indent = ''): void;
}
کلاس File:
class File implements FileSystemItem
{
public function __construct(
private string $name,
private int $size
) {
}
public function getName(): string
{
return $this->name;
}
public function getSize(): int
{
return $this->size;
}
public function print(string $indent = ''): void
{
echo $indent . '- ' . $this->name . PHP_EOL;
}
}
کلاس Folder:
class Folder implements FileSystemItem
{
private array $items = [];
public function __construct(
private string $name
) {
}
public function add(FileSystemItem $item): void
{
$this->items[] = $item;
}
public function getName(): string
{
return $this->name;
}
public function getSize(): int
{
$total = 0;
foreach ($this->items as $item) {
$total += $item->getSize();
}
return $total;
}
public function print(string $indent = ''): void
{
echo $indent . '+ ' . $this->name . PHP_EOL;
foreach ($this->items as $item) {
$item->print($indent . ' ');
}
}
}
حالا:
$documents->print();
خروجی مشابه زیر خواهد بود:
+ documents
- resume.pdf
- invoice.pdf
+ projects
- index.php
- style.css
این مثال نشان میدهد که Composite میتواند عملیات را بهصورت بازگشتی روی تمام Objectهای درخت اجرا کند.
# مثال کاربردی: دستهبندی محصولات فروشگاه
Composite Pattern در فروشگاه اینترنتی کاربرد زیادی دارد.
فرض کنید محصولات و دستهبندیها هر دو باید قابل نمایش باشند.
ساختار:
CatalogItem
├── Product
└── Category
ابتدا Interface را میسازیم:
interface CatalogItem
{
public function getTitle(): string;
public function getPrice(): int;
}
کلاس محصول:
class Product implements CatalogItem
{
public function __construct(
private string $title,
private int $price
) {
}
public function getTitle(): string
{
return $this->title;
}
public function getPrice(): int
{
return $this->price;
}
}
کلاس دستهبندی:
class Category implements CatalogItem
{
private array $items = [];
public function __construct(
private string $title
) {
}
public function add(CatalogItem $item): void
{
$this->items[] = $item;
}
public function getTitle(): string
{
return $this->title;
}
public function getPrice(): int
{
$total = 0;
foreach ($this->items as $item) {
$total += $item->getPrice();
}
return $total;
}
}
استفاده:
$mobile = new Category('موبایل');
$mobile->add(
new Product('Samsung A55', 15000000)
);
$mobile->add(
new Product('Xiaomi Redmi', 12000000)
);
echo $mobile->getPrice();
خروجی:
27000000
در این مثال، Category مانند یک محصول قابل استفاده است، چون هر دو CatalogItem را پیادهسازی کردهاند.
# Composite Pattern در Laravel
در Laravel میتوان از Composite برای ساختارهایی مانند موارد زیر استفاده کرد:
- دستهبندیهای تو در تو؛
- منوی چندسطحی؛
- ساختار فایلها؛
- گروههای Permission؛
- فرمهای چندبخشی؛
- ساختار صفحات و بخشهای صفحه؛
- دستهبندی محصولات؛
- ساختار سازمانی؛
- گروهبندی گزارشها.
برای مثال در یک سیستم منوی سایت، هم یک لینک ساده و هم یک منوی دارای فرزند میتوانند Interface مشترکی داشته باشند.
نمونه منوی چندسطحی در Laravel
ابتدا قرارداد منو را ایجاد میکنیم:
namespace App\Contracts;
interface MenuItem
{
public function render(): string;
}
کلاس لینک ساده:
namespace App\Menu;
use App\Contracts\MenuItem;
class LinkItem implements MenuItem
{
public function __construct(
private string $title,
private string $url
) {
}
public function render(): string
{
return sprintf(
'<li><a href="%s">%s</a></li>',
e($this->url),
e($this->title)
);
}
}
کلاس گروه منو:
namespace App\Menu;
use App\Contracts\MenuItem;
class MenuGroup implements MenuItem
{
private array $items = [];
public function __construct(
private string $title
) {
}
public function add(MenuItem $item): void
{
$this->items[] = $item;
}
public function render(): string
{
$children = '';
foreach ($this->items as $item) {
$children .= $item->render();
}
return sprintf(
'<li><span>%s</span><ul>%s</ul></li>',
e($this->title),
$children
);
}
}
استفاده:
$menu = new MenuGroup('محصولات');
$menu->add(
new LinkItem('افزونه وردپرس', '/wordpress-plugins')
);
$menu->add(
new LinkItem('دورههای PHP', '/php-courses')
);
echo $menu->render();
در این ساختار، MenuGroup میتواند شامل LinkItem یا گروه دیگری باشد.
# Composite و بازگشت یا Recursion
یکی از ویژگیهای مهم Composite استفاده از Recursion است.
در ساختار درختی، یک Composite میتواند شامل Composite دیگری باشد.
برای مثال:
Root
└── Category A
└── Category B
└── Product
هنگام اجرای متدی مانند:
$root->render();
هر Composite عملیات را روی فرزندان خود اجرا میکند و اگر فرزند نیز Composite باشد، همین عملیات ادامه پیدا میکند.
به همین دلیل Composite برای دادههای سلسلهمراتبی مناسب است.
# تفاوت Composite و Iterator Pattern
Composite و Iterator هر دو ممکن است در ساختارهای مجموعهای استفاده شوند، اما هدف آنها متفاوت است.
Composite
Composite برای مدلکردن Objectها به شکل درختی استفاده میشود.
مثلاً:
Folder
├── File
└── Folder
Iterator
Iterator برای پیمایش یک مجموعه بدون آشکارکردن جزئیات داخلی آن استفاده میشود.
مثلاً:
foreach ($collection as $item) {
// ...
}
به زبان ساده:
Composite:
ساختار Objectها چگونه باشد؟
Iterator:
چگونه روی Objectها حرکت کنیم؟
این دو الگو حتی میتوانند در کنار یکدیگر استفاده شوند.
# تفاوت Composite و Decorator Pattern
Decorator نیز از Composition استفاده میکند، اما هدفش با Composite متفاوت است.
Composite
چند Object را در قالب یک ساختار درختی کنار هم قرار میدهد.
Decorator
رفتار جدیدی به یک Object اضافه میکند، بدون اینکه کلاس اصلی تغییر کند.
مثلاً:
Composite:
Folder شامل چند File است.
Decorator:
Logger یک Service را درون خود قرار میدهد.
در Composite معمولاً با چند فرزند سروکار داریم، اما در Decorator معمولاً یک Object اصلی درون Wrapper قرار میگیرد.
# تفاوت Composite و Bridge Pattern
Bridge برای جداکردن دو سلسلهمراتب مستقل استفاده میشود.
مثلاً:
Page + Renderer
Payment + Gateway
اما Composite برای ساختن ساختار درختی و مدیریت یک Object یا گروهی از Objectها از طریق قرارداد مشترک استفاده میشود.
به شکل خلاصه:
Bridge:
جداکردن دو بُعد مستقل
Composite:
ساختن ساختار درختی از Objectها
# مزایای Composite Pattern
1. سادهشدن Client
Client میتواند با Leaf و Composite از طریق یک Interface مشترک کار کند.
2. مناسب برای ساختارهای درختی
برای دستهبندی، منو، فایل و ساختار سازمانی بسیار مناسب است.
3. کاهش شرطهای نوعی
Client لازم نیست دائماً بررسی کند که Object فعلی از چه نوعی است.
4. توسعهپذیری
میتوان Leaf یا Composite جدید اضافه کرد، بدون اینکه Client تغییر زیادی کند.
5. پشتیبانی از عملیات بازگشتی
عملیاتهایی مانند محاسبه مجموع، نمایش، جستجو و پیمایش میتوانند روی تمام درخت اجرا شوند.
6. رعایت اصل Open/Closed
در صورت طراحی درست، اضافهکردن Component جدید نیازمند تغییر کدهای قبلی نیست.
# معایب Composite Pattern
1. طراحی Interface مشترک
گاهی پیدا کردن Interface مناسب برای Leaf و Composite دشوار است.
2. تفاوت رفتار Leaf و Composite
ممکن است بعضی متدها برای Leaf کاربرد نداشته باشند.
3. پیچیدگی غیرضروری
اگر ساختار شما فقط یک لیست ساده است، Composite ممکن است بیش از نیاز باشد.
4. کنترل دسترسی به فرزندان
باید مشخص کنید که چه Objectهایی اجازه اضافهشدن به Composite را دارند.
# چه زمانی از Composite Pattern استفاده کنیم؟
Composite زمانی مناسب است که:
- دادهها ساختار درختی دارند؛
- یک Object و گروهی از Objectها باید از یک قرارداد استفاده کنند؛
- ساختارها میتوانند چند سطح داشته باشند؛
- عملیات باید روی یک Object یا کل گروه اجرا شود؛
- نمیخواهید Client از نوع واقعی Object اطلاع داشته باشد.
نمونههای کاربرد:
- سیستم فایل؛
- دستهبندی محصولات؛
- منوی چندسطحی؛
- فرمهای تو در تو؛
- ساختار صفحات؛
- گروهبندی Permissionها؛
- درخت سازمانی؛
- Expression Tree؛
- ساختار کامپوننتهای رابط کاربری.
# چه زمانی از Composite استفاده نکنیم؟
اگر دادههای شما فقط یک آرایه ساده هستند، نیازی به Composite ندارید.
برای مثال:
$products = [
'Laptop',
'Mouse',
'Keyboard',
];
در این حالت استفاده از یک آرایه ساده کافی است.
همچنین اگر Objectها هیچ رابطه والد و فرزندی ندارند، ساختار Composite ممکن است باعث پیچیدگی بیدلیل شود.
# تست Composite Pattern با PHPUnit
برای تست میتوانیم یک فایل و یک پوشه بسازیم:
public function testFolderCalculatesTotalSize(): void
{
$folder = new Folder('documents');
$folder->add(
new File('a.txt', 100)
);
$folder->add(
new File('b.txt', 200)
);
$this->assertSame(
300,
$folder->getSize()
);
}
همچنین میتوانیم پوشه تو در تو را تست کنیم:
public function testNestedFoldersCalculateTotalSize(): void
{
$root = new Folder('root');
$child = new Folder('child');
$child->add(
new File('image.png', 500)
);
$root->add($child);
$root->add(
new File('readme.txt', 100)
);
$this->assertSame(
600,
$root->getSize()
);
}
این تست بررسی میکند که محاسبه حجم در ساختارهای چندسطحی نیز درست انجام میشود.
# اشتباهات رایج در استفاده از Composite Pattern
اشتباه اول: استفاده برای هر نوع مجموعه
هر آرایه یا Collection نیازمند Composite نیست.
اشتباه دوم: نداشتن قرارداد مشترک
اگر Leaf و Composite Interface مشترک نداشته باشند، Client مجبور میشود با هرکدام به شکل متفاوتی کار کند.
اشتباه سوم: وابستگی مستقیم به کلاسهای فرزند
بهتر است متد add() یک Interface دریافت کند:
public function add(FileSystemItem $item): void
نه فقط یک کلاس مشخص:
public function add(File $file): void
اشتباه چهارم: نادیدهگرفتن عمق درخت
اگر ساختار میتواند بسیار عمیق شود، باید مصرف حافظه و هزینه عملیات بازگشتی را در نظر بگیرید.
اشتباه پنجم: قرار دادن تمام منطق در Client
در Composite بهتر است هر Node مسئول عملیات خودش و فرزندانش باشد.
# نمونه کامل Composite Pattern در PHP
کد ساده و کامل این مثال به شکل زیر است:
<?php
interface FileSystemItem
{
public function getName(): string;
public function getSize(): int;
}
class File implements FileSystemItem
{
public function __construct(
private string $name,
private int $size
) {
}
public function getName(): string
{
return $this->name;
}
public function getSize(): int
{
return $this->size;
}
}
class Folder implements FileSystemItem
{
private array $items = [];
public function __construct(
private string $name
) {
}
public function add(FileSystemItem $item): void
{
$this->items[] = $item;
}
public function getName(): string
{
return $this->name;
}
public function getSize(): int
{
$total = 0;
foreach ($this->items as $item) {
$total += $item->getSize();
}
return $total;
}
}
$root = new Folder('root');
$root->add(
new File('readme.txt', 100)
);
$images = new Folder('images');
$images->add(
new File('photo.jpg', 1200)
);
$images->add(
new File('banner.png', 800)
);
$root->add($images);
echo $root->getSize();
خروجی:
2100
در این کد:
FileSystemItemنقش Component را دارد.Fileنقش Leaf را دارد.Folderنقش Composite را دارد.getSize()روی کل ساختار درختی اجرا میشود.
# جمعبندی Composite Pattern در PHP
Composite Pattern یک Structural Design Pattern برای ساختن ساختارهای درختی از Objectها است.
در این الگو، Leaf و Composite از یک Interface مشترک استفاده میکنند. Leaf یک Object نهایی است، اما Composite میتواند شامل چند Leaf یا Composite دیگر باشد.
ساختار کلی:
Component
├── Leaf
└── Composite
├── Leaf
└── Composite
مهمترین مزیت Composite این است که Client میتواند با یک Object مستقل و یک گروه از Objectها به شکل یکسان کار کند.
این الگو برای موارد زیر مناسب است:
- فایل و پوشه؛
- دستهبندی محصولات؛
- منوی چندسطحی؛
- ساختار سازمانی؛
- فرمهای تو در تو؛
- Expression Tree؛
- گروهبندی Objectها.
با این حال، اگر دادههای شما فقط یک لیست ساده هستند، استفاده از Composite ضرورتی ندارد. این الگو زمانی ارزش بیشتری دارد که Objectها واقعاً رابطه والد و فرزندی داشته باشند و عملیات روی کل ساختار به شکل بازگشتی اجرا شود.
# سوالات متداول Composite Pattern در PHP
Composite Pattern در PHP چیست؟
Composite Pattern الگویی Structural است که Objectها را به شکل درختی سازماندهی میکند و امکان استفاده یکسان از Object مستقل و گروهی از Objectها را فراهم میسازد.
Component در Composite چه نقشی دارد؟
Component قرارداد مشترک Leaf و Composite را تعریف میکند.
Leaf در Composite چیست؟
Leaf یک Object نهایی است که فرزند دیگری ندارد؛ مانند فایل، محصول یا لینک ساده.
Composite در Composite Pattern چیست؟
Composite Objectی است که میتواند چند Component دیگر را نگهداری کند؛ مانند پوشه، دستهبندی یا منوی چندسطحی.
آیا Composite در Laravel کاربرد دارد؟
بله. میتوان از آن برای منوهای چندسطحی، دستهبندی محصولات، ساختار فایلها، Permissionها و Componentهای تو در تو استفاده کرد.
تفاوت Composite و Iterator چیست؟
Composite ساختار درختی Objectها را مدل میکند، اما Iterator برای پیمایش مجموعه بدون آشکارکردن جزئیات داخلی آن استفاده میشود.
تفاوت Composite و Decorator چیست؟
Composite چند Object را در قالب ساختار درختی مدیریت میکند، اما Decorator رفتار جدیدی به یک Object اضافه میکند.
آیا Composite Pattern همیشه مناسب است؟
خیر. اگر دادهها فقط یک لیست ساده هستند یا رابطه والد و فرزندی ندارند، استفاده از Composite میتواند پیچیدگی غیرضروری ایجاد کند.





