نمط المركَّب
نمط المركَّب هو نمط تصميم هيكلي يسمح لك بتركيب كائنات في هياكل شجرية لتعاملها على أنها وحدة واحدة أو كائن واحد.
المشكلة
يستخدم نمط المركَّب في حالة واحدة فقط، وهي حين يمكن تمثيل النموذج الذي يقوم عليه تطبيقك في هيئة شجرية، كأن يكون لديك نوعين من الكائنات هما المنتجات Products
والصناديق Boxes
. ويستطيع الأخير استيعاب منتجات بداخله وصناديق أخرى كذلك أصغر منه في الحجم، وتلك الصناديق الجديدة يمكنها احتواء صناديق أخرى بداخلها، وهكذا دواليك. ولنقل أنك تريد إنشاء نظام طلبات يستخدم تلك الفئات، تحتوي فيه الطلبات المختلفة على منتجات بسيطة بدون أي تغليف إضافة إلى صناديق ملأى بالمنتجات والصناديق الأخرى الأصغر منها، وتريد الآن أن تحسب الثمن الكلي لطلب به صناديق بداخلها صناديق أخرى ومنتجات بدون تغليف، كيف تفعل ذلك؟
ضع الصورة: قد يتكون الطلب الواحد من عدة منتجات مختلفة مغلفة في صناديق وغير مغلفة، وتغلف كلها في صناديق أكبر منها تحتويها، وهكذا ليبدو الشكل الكلي كشجرة مقلوبة.
إن سلكنا الطريق المباشرة فهذا يعني فتح الصناديق جميعها وحساب ثمن كل منتج بسيط ثم جمع تلك الأثمان لنحصل على الثمن الكلي للطلب كاملًا، لكن هذا الأسلوب وإن كان يصلح أحيانًا في الأمثلة التي نراها في الحياة اليومية، لكنها لا تصلح في البرمجة، إذ -وفقًا لهذا المنظور- يجب أن تعرف فئات كل المنتجات Products
والصناديق Boxes
التي ستفحصها، ومستوى تداخل الصناديق وأمور أخرى غير ذلك، وذلك يجعل المنظور المباشر غير عملي وقريبًا من المستحيل لحل مشكلة كهذه.
الحل
يقترح نمط المركَّب أن تعمل مع Boxes
و Products
من خلال واجهة مشتركة تصرح عن أسلوب لحساب الثمن الكلي للطلب، وسيفحص ذلك الأسلوب كل عنصر في الصندوق ويطلب ثمنه ثم يعيد إجمالي الصندوق، وإن كان أحد عناصر الصندوق صندوقًا أصغر منه فإن الأسلوب يكرر نفس الخطوات في فحص كل عنصر فيه وطلب ثمنه، وهكذا يعامل كل صندوق فرعي معاملةَ الصندوق الأول حتى يصل إلى آخر تلك السلسلة ويحسب أثمان كل المنتجات الفرعية، بل قد يحسب أثمان الصناديق أنفسها أيضًا في بعض الحالات التي تُحسب فيها قيمة الصناديق من تكلفة التغليف.
ضع الصورة: يسمح لك نمط المركَّب بتشغيل أسلوب ما بشكل تكراري في عناصر شجرة من الكائنات.
وأعظم فائدة لهذا الأسلوب في الحل أنك لا تحتاج إلى النظر في الفئات الحقيقية للكائنات التي تتكون منها الشجرة، فلا تحتاج إلى معرفة ما إن كان الكائن منتجًا بسيطًا أو صندوقًا معقدًا إذ يمكنك معالجتها جميعًا بنفس الأسلوب من خلال الواجهة المشتركة، ذلك أن الكائنات أنفسها تمرر الطلب إلى ما تحتها في كامل الشجرة كلما استدعيت أسلوبًا ما.
مثال واقعي
ضع الصورة: مثال لهيكلة أحد الجيوش.
تُشكَّل جيوش أغلب الدول بصورة هرمية يتكون فيها الجيش من أقسام رئيسية، في كل قسم مجموعة ألوية، في كل لواء منها مجموعة كتائب، ثم كل كتيبة بها مجموعة سرايا، وكل سرية فيها مجموعات من الجنود الذين تُمرر إليهم الأوامر من القيادات العليا للجيش لتعرف المهام التي عليها تنفيذها.
البُنية
ضع الصورة
- تصف واجهةُ الجزء (Component) العمليات المشتركةَ في كل من العناصر البسيطة والمعقدة في الشجرة.
- الورقة (Leaf) هي عنصر بسيط في الشجرة ليس له عناصر فرعية، وعادة ما تنفذ الورقة أغلب المهام الحقيقية بما أنها ليس لها عناصر تحتها تفوض إليه تلك المهام.
- الحاوية (المركَّب أيضًا) هي عنصر في الشجرة تحته عناصر فرعية سواء كانت حاويات أخرى أو أوراقًا، ولا تعرف الحاوية الفئات الحقيقية لما تحتها من فروع، وهي تعمل مع كل العناصر الفرعية من خلال واجهة الجزء فقط، وتفوض العمل إلى عناصرها الفرعية ثم تعالج النتائج الأولية وتعيد النتيجة النهائية للعميل.
- يعمل العميل مع كل العناصر من خلال واجهة الجزء، ويستطيع العمل نتيجة لهذا بنفس الطريقة مع العناصر البسيطة والمعقدة من الشجرة.
مثال توضيحي
يسمح لك نمط المركَّب في هذا المثال بتنفيذ تكديسات لأشكال هندسية (Geometric) داخل محرِّر مرئي.
ضع الصورة: مثال محرر الأشكال الهندسية.
فئة CompoundGraphic
هي الحاوية التي تشمل أي عدد من الأشكال الفرعية بما في ذلك الأشكال المجمَّعة الأخرى، والشكل المجمَّع له نفس الأساليب كالشكل البسيط، لكن بدلًا من فعل أي شيء بنفسه فإنه -أي المجمَّع- يمرر الطلب بشكل تكراري إلى كل فروعه ويجمع النتيجة في النهاية.
وتعمل شيفرة العميل مع الأشكال كلها من خلال واجهة واحدة مشتركة بين كل فئات الأشكال، لهذا لا يعرف العميل ما إن كان يعمل مع شكل بسيط أو مجمع، ويعمل العميل لهذا أيضًا مع هياكل لأشكال معقدة جدًا دون التقيد بالفئات الحقيقية التي تكوِّن تلك الهياكل.
// تصرح واجهة الجزء عن عمليات مشتركة للأشكال المجمعة والبسيطة لمركب ما.
interface Graphic is
method move(x, y)
method draw()
// تمثل فئة الورقةُ الكائناتِ النهائيةَ لمركب ما، فلا يمكن لكائن الورقة أن يحتوي على كائنات فرعية
// وعادة ما ينفذ المهام الحقيقية لذلك، بينما تفوض الكائنات المجمعة المهام إلى ما دونها.
class Dot implements Graphic is
field x, y
constructor Dot(x, y) { ... }
method move(x, y) is
this.x += x, this.y += y
method draw() is
// Y و X ارسم نقطة عند.
// جميع فئات الجزء تستطيع توسعة الأجزاء الأجزاء الأخرى.
class Circle extends Dot is
field radius
constructor Circle(x, y, radius) { ... }
method draw() is
// R وبنصف قطر Y و X ارسم دائرة عند.
// تمثل فئة المركب أجزاءً معقدة قد تكون لها كائنات فرعية، وتفوض كائناتُ المركبُ المهامَ عادة لما دونها
// ثم تجمع النتيجة.
class CompoundGraphic implements Graphic is
field children: array of Graphic
// يمكن أن يضيف كائن المركب أو يحذف أجزاء أخرى -بسيطة أومعقدة- إلى أو من قائمته الفرعية.
method add(child: Graphic) is
// إضافة جزء فرعي إلى مصفوفة الفروع.
method remove(child: Graphic) is
// حذف جزء فرعي إلى مصفوفة الفروع.
method move(x, y) is
foreach (child in children) do
child.move(x, y)
// ينفذ المركب منطقه الأساسي بشكل معين، إذ يمر مرة بعد مرة على فروعه كلها ويجمع نتائجها، وبما أن فروع
// المركب تمرر استدعاءتها إلى فروعها فإن المركب يمر على الشجرة كلها.
method draw() is
// 1. لكل جزء فرعي
// - ارسم الجزء
// - (Bounding Rectangle) حدِّث المستطيل المحدِّد
// 2. (Bounding Coordinates) باستخدام إحداثيات الحدود (Dashes) ارسم مستطيلًا خطه مكون من شُرط مقطعة .
// تعمل شيفرة العميل مع كل الأجزاء من خلال واجهتها الأساسية، وهكذا تستطيع شيفرة العميل أن تدعم أجزاء الورقة
// البسيطة والمركبات المعقدة على حد سواء.
class ImageEditor is
method load() is
all = new CompoundGraphic()
all.add(new Dot(1, 2))
all.add(new Circle(5, 3, 10))
// ...
// اجمع الأجزاء المختارة في جزءٍ مركب معقد واحد.
method groupSelected(components: array of Graphic) is
group = new CompoundGraphic()
group.add(components)
all.remove(components)
all.add(group)
// ستُرسم كل الأجزاء.
all.draw()
قابلية التنفيذ
- استخدم نمط المركَّب عند تنفيذ هيكل شجري لمجموعة كائنات.
يزودك نمط المركَّب بنوعين أساسيين من العناصر التي تشترك في واجهة واحدة، وهي الأوراق (Leaves) البسيطة، والحاويات (Containers) المعقدة، وقد تتكون الحاوية من الأوراق والحاويات الأخرى كذلك، ويسمح لك هذا بإنشاء هيكلة تكرارية متداخلة تشبه الشجرة.
- استخدم نمط المركَّب حين تريد لشيفرة العميل أن تعامل العناصر البسيطة والمعقدة بنفس الأسلوب.
تشترك كل العناصر المعرَّفة من قبل نمط المركَّب في واجهة واحدة، ولا يحتاج العميل أن يعرف الفئة الحقيقية للكائن الذي يتعامل معه.
كيفية الاستخدام
- تأكد أن النموذج الأساسي لتطبيقك يمكن تمثيله بهيكل شجري، ثم جزّء ذلك إلى عناصر بسيطة وحاويات. تذكر أن الحاويات يجب أن تكون قادرة على احتواء العناصر البسيطة إضافة إلى الحاويات الأخرى كذلك.
- صرِّح عن واجهة الجزء مع قائمة أساليب تصلح للعناصر البسيطة والحاويات المعقدة على حد سواء.
- أنشئ فئة ورقة (Leaf) لتمثل العناصر البسيطة، قد يحتوي البرنامج فئات مختلفة للأوراق.
- أنشئ فئة حاوية (Container) لتمثل العناصر المعقدة في تلك الفئة، وأضف حقل مصفوفة لتخزين المراجع إلى العناصر الفرعية، يجب أن تكون المصفوفة قادرة على تخزين الأوراق والحاويات، لذا تأكد أنها مصرح عنها مع نوع واجهة الجزء. تذكر أثناء تنفيذ أساليب واجهة الجزء أن الحاوية يفترض بها تفويض أغلب مهامها إلى العناصر الفرعية لها.
- أخيرًا، عرّف أساليب إضافة وحذف العناصر الفرعية في الحاوية.
لاحظ أن هذه العمليات يمكن التصريح عنها في واجهة الجزء (Component Interface)، وذاك قد يخرق مبدأ عزل الواجهة (Interface Segregation Principle) لأن الأساليب ستكون فارغة في فئة الورقة، لكن العميل سيتمكن من معاملة كل العناصر بالتساوي حتى عند تركيب (Composing) الشجرة.
المزايا والعيوب
المزايا
- تستطيع العمل مع هياكل شجرية معقدة بشكل أسهل: استخدم تعدد الأشكال (polymorphism) والتكرار المستمر (recursion) لصالحك.
- مبدأ المفتوح/المغلق. تستطيع إدخال أنواعًا جديدة من العناصر في التطبيق دون تعطيل الشيفرة الحالية، والتي تعمل الآن مع شجرة كائنات.
العيوب
- قد يكون من الصعب توفير واجهة مشتركة للفئات التي تختلف وظائفها عن بعضها بشكل كبير، فستحتاج في حالات معينة إلى المبالغة في تعميم واجهة الجزء إلى الحد الذي سيجعلها تستعصي على الفهم.
العلاقات مع الأنماط الأخرى
- يمكنك استخدام نمط الباني عند إنشاء أشجار مركبات معقدة لأنك تستطيع برمجة خطوات هيكلتها لتعمل بتكرارية.
- تُستخدم سلسلة المسؤوليات عادة بالتزامن مع المركَّب، وفي تلك الحالة تمرر الورقة الطلب الذي تحصل عليه خلال الأجزاء التي تعلوها في السلسلة الهرمية حتى تصل إلى جذر شجرة الكائنات.
- يمكنك استخدام المكررات لاجتياز أشجار المركبات.
- يمكنك استخدام نمط الزائر لتنفيذ عملية على شجرة مركب بكاملها.
- يمكنك تنفيذ عُقد ورقية مشتركة (Shared Leaf Nodes) لشجرة المركب لتكوّن أنماطًا من نوع وزن الذبابة، من أجل توفير الذاكرة العشوائية RAM.
- لنمطيْ المركب والمزخرِف هياكل متشابهة بما أنهما يعتمدان على التركيب التكراري لتنظيم عدد مفتوح النهاية (open-ended) من الكائنات.
- المزخرف مثل المركب لكنه لا يملك إلا جزءًا فرعيًا واحدًا، ويضيف المزخرف مسؤوليات إضافية إلى الكائن المُغلَّف، بينما ما يقوم به المركَّب هو جمع نتائج الفروع فقط. لكن بأي حال فإن هذه الأنماط تتعاون فيما بينها، فتستطيع استخدام المزخرِف لتوسيع سلوك كائن معين في شجرة مركَّب ما.
- التصميمات التي تستخدم المركب والمزخرف بكثرة يمكنها الاستفادة من نمط النموذج الأولي، إذ استخدامه يسمح باستنساخ هياكل معقدة بدلًا من إعادة بنائهم من الصفر.
الاستخدام في لغة جافا
المستوى: ★ ★ ☆
الانتشار: ★ ★ ★
أمثلة الاستخدام: يكثر استخدام نمط المركَّب في لغة بايثون، إذ يُستخدم لتمثيل هرميات لأجزاء من واجهة المستخدم أو شيفرة تعمل مع الرسوم التوضيحية (Graphs). إليك بعض الأمثلة على نمط المركب من مكتبات جافا قياسية:
- (java.awt.Container#add(Component تقريبًا في كل أجزاء واجهة Swing.
- ()javax.faces.component.UIComponent#getChildren تقريبًا في كل أجزاء واجهات JSF.
يمكن ملاحظة نمط المركب من خلال الأساليب السلوكية التي تأخذ نسخة من نفس النوع التجريدي/التنفيذي إلى هيكل شجري.
مثال: أشكال مرئية بسيطة ومجمَّعة
أشكال
shapes/Shape.java: واجهة مشتركة للشكل
package refactoring_guru.composite.example.shapes;
import java.awt.*;
public interface Shape {
int getX();
int getY();
int getWidth();
int getHeight();
void move(int x, int y);
boolean isInsideBounds(int x, int y);
void select();
void unSelect();
boolean isSelected();
void paint(Graphics graphics);
}
shapes/BaseShape.java: شكل تجريدي بوظائف بسيطة
package refactoring_guru.composite.example.shapes;
import java.awt.*;
abstract class BaseShape implements Shape {
public int x;
public int y;
public Color color;
private boolean selected = false;
BaseShape(int x, int y, Color color) {
this.x = x;
this.y = y;
this.color = color;
}
@Override
public int getX() {
return x;
}
@Override
public int getY() {
return y;
}
@Override
public int getWidth() {
return 0;
}
@Override
public int getHeight() {
return 0;
}
@Override
public void move(int x, int y) {
this.x += x;
this.y += y;
}
@Override
public boolean isInsideBounds(int x, int y) {
return x > getX() && x < (getX() + getWidth()) &&
y > getY() && y < (getY() + getHeight());
}
@Override
public void select() {
selected = true;
}
@Override
public void unSelect() {
selected = false;
}
@Override
public boolean isSelected() {
return selected;
}
void enableSelectionStyle(Graphics graphics) {
graphics.setColor(Color.LIGHT_GRAY);
Graphics2D g2 = (Graphics2D) graphics;
float dash1[] = {2.0f};
g2.setStroke(new BasicStroke(1.0f,
BasicStroke.CAP_BUTT,
BasicStroke.JOIN_MITER,
2.0f, dash1, 0.0f));
}
void disableSelectionStyle(Graphics graphics) {
graphics.setColor(color);
Graphics2D g2 = (Graphics2D) graphics;
g2.setStroke(new BasicStroke());
}
@Override
public void paint(Graphics graphics) {
if (isSelected()) {
enableSelectionStyle(graphics);
}
else {
disableSelectionStyle(graphics);
}
// ...
}
}
shapes/Dot.java: نقطة
package refactoring_guru.composite.example.shapes;
import java.awt.*;
public class Dot extends BaseShape {
private final int DOT_SIZE = 3;
public Dot(int x, int y, Color color) {
super(x, y, color);
}
@Override
public int getWidth() {
return DOT_SIZE;
}
@Override
public int getHeight() {
return DOT_SIZE;
}
@Override
public void paint(Graphics graphics) {
super.paint(graphics);
graphics.fillRect(x - 1, y - 1, getWidth(), getHeight());
}
}
shapes/Circle.java: دائرة
package refactoring_guru.composite.example.shapes;
import java.awt.*;
public class Circle extends BaseShape {
public int radius;
public Circle(int x, int y, int radius, Color color) {
super(x, y, color);
this.radius = radius;
}
@Override
public int getWidth() {
return radius * 2;
}
@Override
public int getHeight() {
return radius * 2;
}
public void paint(Graphics graphics) {
super.paint(graphics);
graphics.drawOval(x, y, getWidth() - 1, getHeight() - 1);
}
}
shapes/Rectangle.java: مستطيل
package refactoring_guru.composite.example.shapes;
import java.awt.*;
public class Rectangle extends BaseShape {
public int width;
public int height;
public Rectangle(int x, int y, int width, int height, Color color) {
super(x, y, color);
this.width = width;
this.height = height;
}
@Override
public int getWidth() {
return width;
}
@Override
public int getHeight() {
return height;
}
@Override
public void paint(Graphics graphics) {
super.paint(graphics);
graphics.drawRect(x, y, getWidth() - 1, getHeight() - 1);
}
}
shapes/CompoundShape.java: شكل مجمع يتكون من كائنات أشكال أخرى
package refactoring_guru.composite.example.shapes;
import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class CompoundShape extends BaseShape {
protected List<Shape> children = new ArrayList<>();
public CompoundShape(Shape... components) {
super(0, 0, Color.BLACK);
add(components);
}
public void add(Shape component) {
children.add(component);
}
public void add(Shape... components) {
children.addAll(Arrays.asList(components));
}
public void remove(Shape child) {
children.remove(child);
}
public void remove(Shape... components) {
children.removeAll(Arrays.asList(components));
}
public void clear() {
children.clear();
}
@Override
public int getX() {
if (children.size() == 0) {
return 0;
}
int x = children.get(0).getX();
for (Shape child : children) {
if (child.getX() < x) {
x = child.getX();
}
}
return x;
}
@Override
public int getY() {
if (children.size() == 0) {
return 0;
}
int y = children.get(0).getY();
for (Shape child : children) {
if (child.getY() < y) {
y = child.getY();
}
}
return y;
}
@Override
public int getWidth() {
int maxWidth = 0;
int x = getX();
for (Shape child : children) {
int childsRelativeX = child.getX() - x;
int childWidth = childsRelativeX + child.getWidth();
if (childWidth > maxWidth) {
maxWidth = childWidth;
}
}
return maxWidth;
}
@Override
public int getHeight() {
int maxHeight = 0;
int y = getY();
for (Shape child : children) {
int childsRelativeY = child.getY() - y;
int childHeight = childsRelativeY + child.getHeight();
if (childHeight > maxHeight) {
maxHeight = childHeight;
}
}
return maxHeight;
}
@Override
public void move(int x, int y) {
for (Shape child : children) {
child.move(x, y);
}
}
@Override
public boolean isInsideBounds(int x, int y) {
for (Shape child : children) {
if (child.isInsideBounds(x, y)) {
return true;
}
}
return false;
}
@Override
public void unSelect() {
super.unSelect();
for (Shape child : children) {
child.unSelect();
}
}
public boolean selectChildAt(int x, int y) {
for (Shape child : children) {
if (child.isInsideBounds(x, y)) {
child.select();
return true;
}
}
return false;
}
@Override
public void paint(Graphics graphics) {
if (isSelected()) {
enableSelectionStyle(graphics);
graphics.drawRect(getX() - 1, getY() - 1, getWidth() + 1, getHeight() + 1);
disableSelectionStyle(graphics);
}
for (refactoring_guru.composite.example.shapes.Shape child : children) {
child.paint(graphics);
}
}
}
المحرر
editor/ImageEditor.java: محرر الشكل
package refactoring_guru.composite.example.editor;
import refactoring_guru.composite.example.shapes.CompoundShape;
import refactoring_guru.composite.example.shapes.Shape;
import javax.swing.*;
import javax.swing.border.Border;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class ImageEditor {
private EditorCanvas canvas;
private CompoundShape allShapes = new CompoundShape();
public ImageEditor() {
canvas = new EditorCanvas();
}
public void loadShapes(Shape... shapes) {
allShapes.clear();
allShapes.add(shapes);
canvas.refresh();
}
private class EditorCanvas extends Canvas {
JFrame frame;
private static final int PADDING = 10;
EditorCanvas() {
createFrame();
refresh();
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
allShapes.unSelect();
allShapes.selectChildAt(e.getX(), e.getY());
e.getComponent().repaint();
}
});
}
void createFrame() {
frame = new JFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
JPanel contentPanel = new JPanel();
Border padding = BorderFactory.createEmptyBorder(PADDING, PADDING, PADDING, PADDING);
contentPanel.setBorder(padding);
frame.setContentPane(contentPanel);
frame.add(this);
frame.setVisible(true);
frame.getContentPane().setBackground(Color.LIGHT_GRAY);
}
public int getWidth() {
return allShapes.getX() + allShapes.getWidth() + PADDING;
}
public int getHeight() {
return allShapes.getY() + allShapes.getHeight() + PADDING;
}
void refresh() {
this.setSize(getWidth(), getHeight());
frame.pack();
}
public void paint(Graphics graphics) {
allShapes.paint(graphics);
}
}
}
Demo.java: شيفرة العميل
package refactoring_guru.composite.example;
import refactoring_guru.composite.example.editor.ImageEditor;
import refactoring_guru.composite.example.shapes.Circle;
import refactoring_guru.composite.example.shapes.CompoundShape;
import refactoring_guru.composite.example.shapes.Dot;
import refactoring_guru.composite.example.shapes.Rectangle;
import java.awt.*;
public class Demo {
public static void main(String[] args) {
ImageEditor editor = new ImageEditor();
editor.loadShapes(
new Circle(10, 10, 10, Color.BLUE),
new CompoundShape(
new Circle(110, 110, 50, Color.RED),
new Dot(160, 160, Color.RED)
),
new CompoundShape(
new Rectangle(250, 250, 100, 100, Color.GREEN),
new Dot(240, 240, Color.GREEN),
new Dot(240, 360, Color.GREEN),
new Dot(360, 360, Color.GREEN),
new Dot(360, 240, Color.GREEN)
)
);
}
}
OutputDemo.png: نتائج التنفيذ
ضع الصورة (ملف:dpcj.OutputDemo.png)
الاستخدام في لغة #C
المستوى: ★ ★ ☆
الانتشار: ★ ★ ★
أمثلة الاستخدام: يكثر استخدام نمط المركَّب في لغة #C، إذ يُستخدم لتمثيل هرميات لأجزاء من واجهة المستخدم أو شيفرة تعمل مع الرسوم التوضيحية (Graphs).
يمكن ملاحظة نمط المركب من خلال الأساليب السلوكية التي تأخذ نسخة من نفس النوع التجريدي/التنفيذي إلى هيكل شجري.
مثال: مثال تصوري
يوضح هذا المثال بنية نمط المركَّب، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
Program.cs: مثال تصوري
using System;
using System.Collections.Generic;
namespace RefactoringGuru.DesignPatterns.Composite.Conceptual
{
// تصرح فئة الجزء الأساسي عن العمليات المشتركة للكائنات البسيطة والمعقدة لمركب ما.
abstract class Component
{
public Component() { }
// قد ينفذ الجزء الأساسي بعض السلوك الافتراضي أو لا ينفذه للفئات الحقيقية من خلال التصريح عن الأسلوب
// الحاوي للسلوك على أنه تجريدي.
public abstract string Operation();
// قد يفيد في بعض الحالات أن تعرّف عمليات إدارة الفروع في فئة الجزء الأساسي مباشرة، وهكذا لن تحتاج إلى
// كشف الفئات الحقيقية للأجزاء إلى شيفرة العميل حتى أثناء تجميع الشجرة. لكن المشكلة هنا هي أن هذه
// الأساليب ستكون فارغة للأجزاء التي في مستوى الورقة.
public virtual void Add(Component component)
{
throw new NotImplementedException();
}
public virtual void Remove(Component component)
{
throw new NotImplementedException();
}
// تستطيع إضافة أسلوب يسمح لشيفرة العميل أن تعرف إن كان جزء ما يستطيع استيعاب فروع له.
public virtual bool IsComposite()
{
return true;
}
}
// تمثل فئة الورقةُ الكائناتِ النهائيةَ لمركب ما، فلا يمكن لكائن الورقة
// أن يحتوي على كائنات فرعية
//
// عادة ما تنفذ كائنات الأوراق المهام التي توكل إليها بينما تفوض
// الكائنات المركبة تلك المهام إلى ما دونها.
class Leaf : Component
{
public override string Operation()
{
return "Leaf";
}
public override bool IsComposite()
{
return false;
}
}
// تمثل فئة المركب الأجزاء المعقدة التي قد تحتوي على فروع، وعادة ما تفوض
// كائنات المركب الأعمال الحقيقية لما دونها ثم تجمع النتيجة النهائية.
{
protected List<Component> _children = new List<Component>();
public override void Add(Component component)
{
this._children.Add(component);
}
public override void Remove(Component component)
{
this._children.Remove(component);
}
// ينفذ المركب منطقه الأساسي بأن يمر على فروعه كلها بشكل تكراري جامعًا نتائجها، وبما أن فروع المركب
// تمرر استدعاءاتها لما دونها فإن الشجرة بكاملها تُفحص نتيجة لهذا.
public override string Operation()
{
int i = 0;
string result = "Branch(";
foreach (Component component in this._children)
{
result += component.Operation();
if (i != this._children.Count - 1)
{
result += "+";
}
i++;
}
return result + ")";
}
}
class Client
{
// تعمل شيفرة العميل مع كل الأجزاء من خلال الواجهة الأساسية.
public void ClientCode(Component leaf)
{
Console.WriteLine($"RESULT: {leaf.Operation()}\n");
}
// تستطيع شيفرة العميل أن تعمل مع أي جزء بسيط أو معقد دون الاعتماد على الفئات الحقيقية للأجزاء
// وذلك بفضل كون التصريح عن عمليات إدارة الفروع في فئة الجزء الأساسي.
public void ClientCode2(Component component1, Component component2)
{
if (component1.IsComposite())
{
component1.Add(component2);
}
Console.WriteLine($"RESULT: {component1.Operation()}");
}
}
class Program
{
static void Main(string[] args)
{
Client client = new Client();
// تستطيع شيفرة العميل بهذه الطريقة أن تدعم أجزاء الورقة البسيطة.
Leaf leaf = new Leaf();
Console.WriteLine("Client: I get a simple component:");
client.ClientCode(leaf);
// ... إضافة إلى المركبات المعقدة.
Composite tree = new Composite();
Composite branch1 = new Composite();
branch1.Add(new Leaf());
branch1.Add(new Leaf());
Composite branch2 = new Composite();
branch2.Add(new Leaf());
tree.Add(branch1);
tree.Add(branch2);
Console.WriteLine("Client: Now I've got a composite tree:");
client.ClientCode(tree);
Console.Write("Client: I don't need to check the components classes even when managing the tree:\n");
client.ClientCode2(tree, leaf);
}
}
}
Output.txt: المخرجات
Client: I get a simple component:
RESULT: Leaf
Client: Now I've got a composite tree:
RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf))
Client: I don't need to check the components classes even when managing the tree:
RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf)+Leaf)
الاستخدام في لغة PHP
المستوى: ★ ★ ☆
الانتشار: ★ ★ ★
أمثلة الاستخدام: يكثر استخدام نمط المركَّب في لغة PHP عند التعامل مع أشجار الكائنات، وأبسط مثال له قد يكون تطبيقه على عناصر شجرة DOM لتعمل مع نمط المركب والعناصر البسيطة للشجرة بنفس الأسلوب.
مثال: مثال تصوري
يوضح هذا المثال بنية نمط المركَّب، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
index.php: مثال تصوري
<?php
namespace RefactoringGuru\Composite\Conceptual;
/**
* The base Component class declares common operations for both simple and
* complex objects of a composition.
*/
abstract class Component
{
/**
* @var Component
*/
protected $parent;
/**
* Optionally, the base Component can declare an interface for setting and
* accessing a parent of the component in a tree structure. It can also
* provide some default implementation for these methods.
*/
public function setParent(Component $parent)
{
$this->parent = $parent;
}
public function getParent(): Component
{
return $this->parent;
}
/**
* In some cases, it would be beneficial to define the child-management
* operations right in the base Component class. This way, you won't need to
* expose any concrete component classes to the client code, even during the
* object tree assembly. The downside is that these methods will be empty
* for the leaf-level components.
*/
public function add(Component $component): void { }
public function remove(Component $component): void { }
/**
* You can provide a method that lets the client code figure out whether a
* component can bear children.
*/
public function isComposite(): bool
{
return false;
}
/**
* The base Component may implement some default behavior or leave it to
* concrete classes (by declaring the method containing the behavior as
* "abstract").
*/
abstract public function operation(): string;
}
/**
* The Leaf class represents the end objects of a composition. A leaf can't have
* any children.
*
* Usually, it's the Leaf objects that do the actual work, whereas Composite
* objects only delegate to their sub-components.
*/
class Leaf extends Component
{
public function operation(): string
{
return "Leaf";
}
}
/**
* The Composite class represents the complex components that may have children.
* Usually, the Composite objects delegate the actual work to their children and
* then "sum-up" the result.
*/
class Composite extends Component
{
/**
* @var \SplObjectStorage
*/
protected $children;
public function __construct()
{
$this->children = new \SplObjectStorage;
}
/**
* A composite object can add or remove other components (both simple or
* complex) to or from its child list.
*/
public function add(Component $component): void
{
$this->children->attach($component);
$component->setParent($this);
}
public function remove(Component $component): void
{
$this->children->detach($component);
$component->setParent(null);
}
public function isComposite(): bool
{
return true;
}
/**
* The Composite executes its primary logic in a particular way. It
* traverses recursively through all its children, collecting and summing
* their results. Since the composite's children pass these calls to their
* children and so forth, the whole object tree is traversed as a result.
*/
public function operation(): string
{
$results = [];
foreach ($this->children as $child) {
$results[] = $child->operation();
}
return "Branch(" . implode("+", $results) . ")";
}
}
/**
* The client code works with all of the components via the base interface.
*/
function clientCode(Component $component)
{
// ...
echo "RESULT: " . $component->operation();
// ...
}
/**
* This way the client code can support the simple leaf components...
*/
$simple = new Leaf;
echo "Client: I've got a simple component:\n";
clientCode($simple);
echo "\n\n";
/**
* ...as well as the complex composites.
*/
$tree = new Composite;
$branch1 = new Composite;
$branch1->add(new Leaf);
$branch1->add(new Leaf);
$branch2 = new Composite;
$branch2->add(new Leaf);
$tree->add($branch1);
$tree->add($branch2);
echo "Client: Now I've got a composite tree:\n";
clientCode($tree);
echo "\n\n";
/**
* Thanks to the fact that the child-management operations are declared in the
* base Component class, the client code can work with any component, simple or
* complex, without depending on their concrete classes.
*/
function clientCode2(Component $component1, Component $component2)
{
// ...
if ($component1->isComposite()) {
$component1->add($component2);
}
echo "RESULT: " . $component1->operation();
// ...
}
echo "Client: I don't need to check the components classes even when managing the tree:\n";
clientCode2($tree, $simple);
Output.txt: المخرجات
Client: I get a simple component:
RESULT: Leaf
Client: Now I get a composite tree:
RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf))
Client: I don't need to check the components classes even when managing the tree::
RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf)+Leaf)
مثال: حالة واقعية
يستطيع نمط المركب أن ينظم العمل مع أي هيكل تكراري شجري، كما في مثال شجرة HTML DOM، ففي حين تتصرف عناصر الإدخال كالأوراق، تلعب العناصر المعقدة مثل الاستمارات ووسوم fieldsets دور المركَّبات.
ويمكنك استخدام نمط المركب لتطبيق سلوكيات مختلفة إلى كامل شجرة HTML بنفس أسلوب تطبيقها على عناصرها الداخلية دون ربط شيفرتك بالفئات الحقيقية لشجرة DOM ولا تصديرها إلى صيغ مختلفة أو التأكد من صحة أجزائها، إلخ.
ولا تحتاج كذلك مع نمط المركب إلى تفقد ما إن كان العنصر من النوع البسيط أو المعقد قبل تنفيذ السلوك، إذ يُنفَّذ مباشرة أو يُمرر إلى العناصر الفرعية للعنصر، اعتمادًا على نوع العنصر نفسه.
index.php: حالة واقعية
<?php
namespace RefactoringGuru\Composite\RealWorld;
/**
* The base Component class declares an interface for all concrete components,
* both simple and complex.
*
* In our example, we'll be focusing on the rendering behavior of DOM elements.
*/
abstract class FormElement
{
/**
* We can anticipate that all DOM elements require these 3 fields.
*/
protected $name;
protected $title;
protected $data;
public function __construct(string $name, string $title)
{
$this->name = $name;
$this->title = $title;
}
public function getName(): string
{
return $this->name;
}
public function setData($data): void
{
$this->data = $data;
}
public function getData(): array
{
return $this->data;
}
/**
* Each concrete DOM element must provide its rendering implementation, but
* we can safely assume that all of them are returning strings.
*/
abstract public function render(): string;
}
/**
* This is a Leaf component. Like all the Leaves, it can't have any children.
*/
class Input extends FormElement
{
private $type;
public function __construct(string $name, string $title, string $type)
{
parent::__construct($name, $title);
$this->type = $type;
}
/**
* Since Leaf components don't have any children that may handle the bulk of
* the work for them, usually it is the Leaves who do the most of the heavy-
* lifting within the Composite pattern.
*/
public function render(): string
{
return "<label for=\"{$this->name}\">{$this->title}</label>\n" .
"<input name=\"{$this->name}\" type=\"{$this->type}\" value=\"{$this->data}\">\n";
}
}
/**
* The base Composite class implements the infrastructure for managing child
* objects, reused by all Concrete Composites.
*/
abstract class FieldComposite extends FormElement
{
/**
* @var FormElement[]
*/
protected $fields = [];
/**
* The methods for adding/removing sub-objects.
*/
public function add(FormElement $field): void
{
$name = $field->getName();
$this->fields[$name] = $field;
}
public function remove(FormElement $component): void
{
$this->fields = array_filter($this->fields, function ($child) use ($component) {
return $child == $component;
});
}
/**
* Whereas a Leaf's method just does the job, the Composite's method almost
* always has to take its sub-objects into account.
*
* In this case, the composite can accept structured data.
*
* @param array $data
*/
public function setData($data): void
{
foreach ($this->fields as $name => $field) {
if (isset($data[$name])) {
$field->setData($data[$name]);
}
}
}
/**
* The same logic applies to the getter. It returns the structured data of
* the composite itself (if any) and all the children data.
*/
public function getData(): array
{
$data = [];
foreach ($this->fields as $name => $field) {
$data[$name] = $field->getData();
}
return $data;
}
/**
* The base implementation of the Composite's rendering simply combines
* results of all children. Concrete Composites will be able to reuse this
* implementation in their real rendering implementations.
*/
public function render(): string
{
$output = "";
foreach ($this->fields as $name => $field) {
$output .= $field->render();
}
return $output;
}
}
/**
* The fieldset element is a Concrete Composite.
*/
class Fieldset extends FieldComposite
{
public function render(): string
{
// Note how the combined rendering result of children is incorporated
// into the fieldset tag.
$output = parent::render();
return "<fieldset><legend>{$this->title}</legend>\n$output</fieldset>\n";
}
}
/**
* And so is the form element.
*/
class Form extends FieldComposite
{
protected $url;
public function __construct(string $name, string $title, string $url)
{
parent::__construct($name, $title);
$this->url = $url;
}
public function render(): string
{
$output = parent::render();
return "<form action=\"{$this->url}\">\n<h3>{$this->title}</h3>\n$output</form>\n";
}
}
/**
* The client code gets a convenient interface for building complex tree
* structures.
*/
function getProductForm(): FormElement
{
$form = new Form('product', "Add product", "/product/add");
$form->add(new Input('name', "Name", 'text'));
$form->add(new Input('description', "Description", 'text'));
$picture = new Fieldset('photo', "Product photo");
$picture->add(new Input('caption', "Caption", 'text'));
$picture->add(new Input('image', "Image", 'file'));
$form->add($picture);
return $form;
}
/**
* The form structure can be filled with data from various sources. The Client
* doesn't have to traverse through all form fields to assign data to various
* fields since the form itself can handle that.
*/
function loadProductData(FormElement $form)
{
$data = [
'name' => 'Apple MacBook',
'description' => 'A decent laptop.',
'photo' => [
'caption' => 'Front photo.',
'image' => 'photo1.png',
],
];
$form->setData($data);
}
/**
* The client code can work with form elements using the abstract interface.
* This way, it doesn't matter whether the client works with a simple component
* or a complex composite tree.
*/
function renderProduct(FormElement $form)
{
// ..
echo $form->render();
// ..
}
$form = getProductForm();
loadProductData($form);
renderProduct($form);
Output.txt: المخرجات
<form action="/product/add">
<h3>Add product</h3>
<label for="name">Name</label>
<input name="name" type="text" value="Apple MacBook">
<label for="description">Description</label>
<input name="description" type="text" value="A decent laptop.">
<fieldset><legend>Product photo</legend>
<label for="caption">Caption</label>
<input name="caption" type="text" value="Front photo.">
<label for="image">Image</label>
<input name="image" type="file" value="photo1.png">
</fieldset>
</form>
الاستخدام في لغة بايثون
المستوى: ★ ★ ☆
الانتشار: ★ ★ ★
أمثلة الاستخدام: يكثر استخدام نمط المركَّب في لغة بايثون، إذ يُستخدم لتمثيل هرميات لأجزاء من واجهة المستخدم أو شيفرة تعمل مع الرسوم التوضيحية (Graphs).
يمكن ملاحظة نمط المركب من خلال الأساليب السلوكية التي تأخذ نسخة من نفس النوع التجريدي/التنفيذي إلى هيكل شجري.
مثال: مثال تصوري
يوضح هذا المثال بنية نمط الجسر، ويركز على إجابة الأسئلة التالية:
- ما الفئات التي يتكون منها؟
- ما الأدوار التي تلعبها هذه الفئات؟
- كيف ترتبط عناصر النمط ببعضها؟
main.py: مثال تصوري
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List
class Component(ABC):
"""
The base Component class declares common operations for both simple and
complex objects of a composition.
"""
@property
def parent(self) -> Component:
return self._parent
@parent.setter
def parent(self, parent: Component):
"""
Optionally, the base Component can declare an interface for setting
and accessing a parent of the component in a tree structure. It can
also provide some default implementation for these methods.
"""
self._parent = parent
"""
In some cases, it would be beneficial to define the child-management
operations right in the base Component class. This way, you won't need to
expose any concrete component classes to the client code, even during the
object tree assembly. The downside is that these methods will be empty
for the leaf-level components.
"""
def add(self, component: Component) -> None:
pass
def remove(self, component: Component) -> None:
pass
def is_composite(self) -> bool:
"""
You can provide a method that lets the client code figure out whether
a component can bear children.
"""
return False
@abstractmethod
def operation(self) -> str:
"""
The base Component may implement some default behavior or leave it to
concrete classes (by declaring the method containing the behavior as
"abstract").
"""
pass
class Leaf(Component):
"""
The Leaf class represents the end objects of a composition. A leaf can't
have any children.
Usually, it's the Leaf objects that do the actual work, whereas Composite
objects only delegate to their sub-components.
"""
def operation(self) -> str:
return "Leaf"
class Composite(Component):
"""
The Composite class represents the complex components that may have
children. Usually, the Composite objects delegate the actual work to
their children and then "sum-up" the result.
"""
def __init__(self) -> None:
self._children: List[Component] = []
"""
A composite object can add or remove other components (both simple or
complex) to or from its child list.
"""
def add(self, component: Component) -> None:
self._children.append(component)
component.parent = self
def remove(self, component: Component) -> None:
self._children.remove(component)
component.parent = None
def is_composite(self) -> bool:
return True
def operation(self) -> str:
"""
The Composite executes its primary logic in a particular way. It
traverses recursively through all its children, collecting and
summing their results. Since the composite's children pass these
calls to their children and so forth, the whole object tree is
traversed as a result.
"""
results = []
for child in self._children:
results.append(child.operation())
return f"Branch({'+'.join(results)})"
def client_code(component: Component) -> None:
"""
The client code works with all of the components via the base interface.
"""
print(f"RESULT: {component.operation()}", end="")
def client_code2(component1: Component, component2: Component) -> None:
"""
Thanks to the fact that the child-management operations are declared in
the base Component class, the client code can work with any component,
simple or complex, without depending on their concrete classes.
"""
if component1.is_composite():
component1.add(component2)
print(f"RESULT: {component1.operation()}", end="")
if __name__ == "__main__":
# This way the client code can support the simple leaf components...
simple = Leaf()
print("Client: I've got a simple component:")
client_code(simple)
print("\n")
# ...as well as the complex composites.
tree = Composite()
branch1 = Composite()
branch1.add(Leaf())
branch1.add(Leaf())
branch2 = Composite()
branch2.add(Leaf())
tree.add(branch1)
tree.add(branch2)
print("Client: Now I've got a composite tree:")
client_code(tree)
print("\n")
print("Client: I don't need to check the components classes even when managing the tree:")
client_code2(tree, simple)
Output.txt: المخرجات
Client: I've got a simple component:
RESULT: Leaf
Client: Now I've got a composite tree:
RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf))
Client: I don't need to check the components classes even when managing the tree:
RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf)+Leaf)