نمط المركَّب

من موسوعة حسوب
مراجعة 02:44، 21 سبتمبر 2019 بواسطة أسامه-دمراني (نقاش | مساهمات) (مراجعة: إضافة أمثلة لغات py-rb-swift-ts، تعديل مثال لغة #C، إضافة تصانيف.)

نمط المركَّب هو نمط تصميم هيكلي يسمح لك بتركيب كائنات في هياكل شجرية لتعاملها على أنها وحدة واحدة أو كائن واحد.

المشكلة

(ش.1) قد يتكون الطلب الواحد من عدة منتجات مختلفة مغلفة في صناديق وغير مغلفة، وتغلف كلها في صناديق أكبر منها تحتويها، وهكذا ليبدو الشكل الكلي كشجرة مقلوبة.

يستخدم نمط المركَّب في حالة واحدة فقط، وهي حين يمكن تمثيل النموذج الذي يقوم عليه تطبيقك في هيئة شجرية، كأن يكون لديك نوعين من الكائنات هما المنتجات Products والصناديق Boxes. ويستطيع الأخير استيعاب منتجات بداخله وصناديق أخرى كذلك أصغر منه في الحجم، وتلك الصناديق الجديدة يمكنها احتواء صناديق أخرى بداخلها، وهكذا دواليك. ولنقل أنك تريد إنشاء نظام طلبات يستخدم تلك الفئات، تحتوي فيه الطلبات المختلفة على منتجات بسيطة بدون أي تغليف إضافة إلى صناديق ملأى بالمنتجات والصناديق الأخرى الأصغر منها، وتريد الآن أن تحسب الثمن الكلي لطلب به صناديق بداخلها صناديق أخرى ومنتجات بدون تغليف، كيف تفعل ذلك؟

إن سلكنا الطريق المباشرة فهذا يعني فتح الصناديق جميعها وحساب ثمن كل منتج بسيط ثم جمع تلك الأثمان لنحصل على الثمن الكلي للطلب كاملًا، لكن هذا الأسلوب وإن كان يصلح أحيانًا في الأمثلة التي نراها في الحياة اليومية، لكنها لا تصلح في البرمجة، إذ -وفقًا لهذا المنظور- يجب أن تعرف فئات كل المنتجات Products والصناديق Boxes التي ستفحصها، ومستوى تداخل الصناديق وأمور أخرى غير ذلك، وذلك يجعل المنظور المباشر غير عملي وقريبًا من المستحيل لحل مشكلة كهذه.

الحل

(ش.2) يسمح لك نمط المركَّب بتشغيل أسلوب ما بشكل تكراري في عناصر شجرة من الكائنات.

يقترح نمط المركَّب أن تعمل مع Boxes و Products من خلال واجهة مشتركة تصرح عن أسلوب لحساب الثمن الكلي للطلب، وسيفحص ذلك الأسلوب كل عنصر في الصندوق ويطلب ثمنه ثم يعيد إجمالي الصندوق، وإن كان أحد عناصر الصندوق صندوقًا أصغر منه فإن الأسلوب يكرر نفس الخطوات في فحص كل عنصر فيه وطلب ثمنه، وهكذا يعامل كل صندوق فرعي معاملةَ الصندوق الأول حتى يصل إلى آخر تلك السلسلة ويحسب أثمان كل المنتجات الفرعية، بل قد يحسب أثمان الصناديق أنفسها أيضًا في بعض الحالات التي تُحسب فيها قيمة الصناديق من تكلفة التغليف.

وأعظم فائدة لهذا الأسلوب في الحل أنك لا تحتاج إلى النظر في الفئات الحقيقية للكائنات التي تتكون منها الشجرة، فلا تحتاج إلى معرفة ما إن كان الكائن منتجًا بسيطًا أو صندوقًا معقدًا إذ يمكنك معالجتها جميعًا بنفس الأسلوب من خلال الواجهة المشتركة، ذلك أن الكائنات أنفسها تمرر الطلب إلى ما تحتها في كامل الشجرة كلما استدعيت أسلوبًا ما.

مثال واقعي

تُشكَّل جيوش أغلب الدول بصورة هرمية يتكون فيها الجيش من أقسام رئيسية، في كل قسم مجموعة ألوية، في كل لواء منها مجموعة كتائب، ثم كل كتيبة بها مجموعة سرايا، وكل سرية فيها مجموعات من الجنود الذين تُمرر إليهم الأوامر من القيادات العليا للجيش لتعرف المهام التي عليها تنفيذها.

البُنية

(ش.4) البُنية
  1. تصف واجهةُ العنصر (Component) العمليات المشتركةَ في كل من العناصر البسيطة والمعقدة في الشجرة.
  2. الورقة (Leaf) هي عنصر بسيط في الشجرة ليس له عناصر فرعية، وعادة ما تنفذ الورقة أغلب المهام الحقيقية بما أنها ليس لها عناصر تحتها تفوض إليه تلك المهام.
  3. الحاوية (المركَّب أيضًا) هي عنصر في الشجرة تحته عناصر فرعية سواء كانت حاويات أخرى أو أوراقًا، ولا تعرف الحاوية الفئات الحقيقية لما تحتها من فروع، وهي تعمل مع كل العناصر الفرعية من خلال واجهة العنصر فقط، وتفوض العمل إلى عناصرها الفرعية ثم تعالج النتائج الأولية وتعيد النتيجة النهائية للعميل.
  4. يعمل العميل مع كل العناصر من خلال واجهة العنصر، ويستطيع العمل نتيجة لهذا بنفس الطريقة مع العناصر البسيطة والمعقدة من الشجرة.

مثال توضيحي

(ش.5) مثال محرر الأشكال الهندسية.

يسمح لك نمط المركَّب في هذا المثال بتنفيذ تكديسات لأشكال هندسية (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) المعقدة، وقد تتكون الحاوية من الأوراق والحاويات الأخرى كذلك، ويسمح لك هذا بإنشاء هيكلة تكرارية متداخلة تشبه الشجرة.

  • استخدم نمط المركَّب حين تريد لشيفرة العميل أن تعامل العناصر البسيطة والمعقدة بنفس الأسلوب.

تشترك كل العناصر المعرَّفة من قبل نمط المركَّب في واجهة واحدة، ولا يحتاج العميل أن يعرف الفئة الحقيقية للكائن الذي يتعامل معه.

كيفية الاستخدام

  1. تأكد أن النموذج الأساسي لتطبيقك يمكن تمثيله بهيكل شجري، ثم جزّء ذلك إلى عناصر بسيطة وحاويات. تذكر أن الحاويات يجب أن تكون قادرة على احتواء العناصر البسيطة إضافة إلى الحاويات الأخرى كذلك.
  2. صرِّح عن واجهة العنصر مع قائمة أساليب تصلح للعناصر البسيطة والحاويات المعقدة على حد سواء.
  3. أنشئ فئة ورقة (Leaf) لتمثل العناصر البسيطة، قد يحتوي البرنامج فئات مختلفة للأوراق.
  4. أنشئ فئة حاوية (Container) لتمثل العناصر المعقدة في تلك الفئة، وأضف حقل مصفوفة لتخزين المراجع إلى العناصر الفرعية، يجب أن تكون المصفوفة قادرة على تخزين الأوراق والحاويات، لذا تأكد أنها مصرح عنها مع نوع واجهة العنصر. تذكر أثناء تنفيذ أساليب واجهة العنصر أن الحاوية يفترض بها تفويض أغلب مهامها إلى العناصر الفرعية لها.
  5. أخيرًا، عرّف أساليب إضافة وحذف العناصر الفرعية في الحاوية.

لاحظ أن هذه العمليات يمكن التصريح عنها في واجهة العنصر (Component Interface)، وذاك قد يخرق مبدأ عزل الواجهة (Interface Segregation Principle) لأن الأساليب ستكون فارغة في فئة الورقة، لكن العميل سيتمكن من معاملة كل العناصر بالتساوي حتى عند تركيب (Composing) الشجرة.

المزايا والعيوب

المزايا

  • تستطيع العمل مع هياكل شجرية معقدة بشكل أسهل: استخدم تعدد الأشكال (polymorphism) والتكرار المستمر (recursion) لصالحك.
  • مبدأ المفتوح/المغلق. تستطيع إدخال أنواعًا جديدة من العناصر في التطبيق دون تعطيل الشيفرة الحالية، والتي تعمل الآن مع شجرة كائنات.

العيوب

  • قد يكون من الصعب توفير واجهة مشتركة للفئات التي تختلف وظائفها عن بعضها بشكل كبير، فستحتاج في حالات معينة إلى المبالغة في تعميم واجهة العنصر إلى الحد الذي سيجعلها تستعصي على الفهم.

العلاقات مع الأنماط الأخرى

  • يمكنك استخدام نمط الباني عند إنشاء أشجار مركبات معقدة لأنك تستطيع برمجة خطوات هيكلتها لتعمل بتكرارية.
  • تُستخدم سلسلة المسؤوليات عادة بالتزامن مع المركَّب، وفي تلك الحالة تمرر الورقة الطلب الذي تحصل عليه خلال العناصر التي تعلوها في السلسلة الهرمية حتى تصل إلى جذر شجرة الكائنات.
  • يمكنك استخدام المكررات لاجتياز أشجار المركبات.
  • يمكنك استخدام نمط الزائر لتنفيذ عملية على شجرة مركب بكاملها.
  • يمكنك تنفيذ عُقد ورقية مشتركة (Shared Leaf Nodes) لشجرة المركب لتكوّن أنماطًا من نوع وزن الذبابة، من أجل توفير الذاكرة العشوائية RAM.
  • لنمطيْ المركب والمزخرِف هياكل متشابهة بما أنهما يعتمدان على التركيب التكراري لتنظيم عدد مفتوح النهاية (open-ended) من الكائنات.
  • المزخرف مثل المركب لكنه لا يملك إلا عنصرًا فرعيًا واحدًا، ويضيف المزخرف مسؤوليات إضافية إلى الكائن المُغلَّف، بينما ما يقوم به المركَّب هو جمع نتائج الفروع فقط. لكن بأي حال فإن هذه الأنماط تتعاون فيما بينها، فتستطيع استخدام المزخرِف لتوسيع سلوك كائن معين في شجرة مركَّب ما.
  • التصميمات التي تستخدم المركب والمزخرف بكثرة يمكنها الاستفادة من نمط النموذج الأولي، إذ استخدامه يسمح باستنساخ هياكل معقدة بدلًا من إعادة بنائهم من الصفر.

الاستخدام في لغة جافا

المستوى: ★ ★ ☆

الانتشار:  ★ ★ ★

أمثلة الاستخدام: يكثر استخدام نمط المركَّب في لغة بايثون، إذ يُستخدم لتمثيل هرميات لعناصر من واجهة المستخدم أو شيفرة تعمل مع الرسوم التوضيحية (Graphs). إليك بعض الأمثلة على نمط المركب من مكتبات جافا قياسية:

يمكن ملاحظة نمط المركب من خلال الأساليب السلوكية التي تأخذ نسخة من نفس النوع التجريدي/التنفيذي إلى هيكل شجري.

مثال: أشكال مرئية بسيطة ومجمَّعة

أشكال

 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: نتائج التنفيذ

الاستخدام في لغة #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;

/**
 * تصرح فئة العنصر الأساسي عن العمليات المشتركة للكائنات البسيطة والمعقدة لمركب ما.
 */
abstract class Component
{
    /**
     * @var Component
     */
    protected $parent;

    /**
     * لفئة العنصر الأساسي أن تصرح عن واجهة لإعداد أحد الأصول لعنصر ما والوصول إليه في هيكل شجري
     * كما تضيف بعض الاستخدامات الافتراضية لهذه الأساليب.
     */
    public function setParent(Component $parent)
    {
        $this->parent = $parent;
    }

    public function getParent(): Component
    {
        return $this->parent;
    }

    /**
     * قد يفيد في بعض الحالات أن تعرّف عمليات إدارة الفروع في فئة العنصر الأساسي مباشرة، وهكذا لن تحتاج
     * إلى كشف الفئات الحقيقية للعناصر إلى شيفرة العميل حتى أثناء تجميع الشجرة. لكن المشكلة هنا هي أن هذه
     * الأساليب ستكون فارغة للعناصر التي في مستوى الورقة.
     */
    public function add(Component $component): void { }

    public function remove(Component $component): void { }

    /**
     * تستطيع إضافة أسلوب يسمح لشيفرة العميل أن تعرف إن كان عنصر ما يستطيع استيعاب فروع له
     */
    public function isComposite(): bool
    {
        return false;
    }

    /**
     * قد ينفذ العنصر الأساسي بعض السلوك الافتراضي أو لا ينفذه للفئات الحقيقية من خلال التصريح عن الأسلوب
     * الحاوي للسلوك على أنه تجريدي.
     */
    abstract public function operation(): string;
}

/**
 * تمثل فئة الورقةُ الكائناتِ النهائيةَ لمركب ما، فلا يمكن لكائن الورقة 
 * أن يحتوي على كائنات فرعية
 *
 * عادة ما تنفذ كائنات الأوراق المهام التي توكل إليها بينما تفوض
 * الكائنات المركبة تلك المهام إلى ما دونها.
 */
class Leaf extends Component
{
    public function operation(): string
    {
        return "Leaf";
    }
}

/**
 * تمثل فئة المركب العناصر المعقدة التي قد تحتوي على فروع، وعادة ما تفوض
 * كائنات المركب الأعمال الحقيقية لما دونها ثم تجمع النتيجة النهائية.
 */
class Composite extends Component
{
    /**
     * @var \SplObjectStorage
     */
    protected $children;
    
    public function __construct()
    {
        $this->children = new \SplObjectStorage;
    }

    /**
     * يستطيع الكائن المركب أن يضيف أو يحذف عناصرً أخرى -بسيطة أو معقدة- من قائمة الفروع.
     */
    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;
    }

    /**
     * ينفذ المركب منطقه الأساسي بأن يمر على فروعه كلها بشكل تكراري جامعًا نتائجها، وبما أن فروع 
     * المركب تمرر استدعاءاتها لما دونها فإن الشجرة بكاملها تُفحص نتيجة لهذا.
*/
    public function operation(): string
    {
        $results = [];
        foreach ($this->children as $child) {
            $results[] = $child->operation();
        }

        return "Branch(" . implode("+", $results) . ")";
    }
}

/**
 * تعمل شيفرة العميل مع كل العناصر من خلال الواجهة الأساسية.
 */
function clientCode(Component $component)
{
    // ...

    echo "RESULT: " . $component->operation();

    // ...
}

/**
 * تستطيع شيفرة العميل بهذه الطريقة أن تدعم عناصر الورقة البسيطة.
 */
$simple = new Leaf;
echo "Client: I've got a simple component:\n";
clientCode($simple);
echo "\n\n";

/**
 * ... إضافة إلى المركبات المعقدة.
 */
$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";

/**
 * تستطيع شيفرة العميل أن تعمل مع أي عنصر بسيط أو معقد دون الاعتماد على الفئات الحقيقية
 * للعناصر، وذلك بفضل كون التصريح عن عمليات إدارة الفروع في فئة العنصر الأساسي.
 */
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;

/**
 * تصرح فئة العنصر الأساسي عن واجهة لكل العناصر الحقيقية سواء البسيطة أو المعقدة.
 * DOM لعناصر (Rendering Behaviour) وسنركز في مثالنا على السلوك الإخراجي 
 */
abstract class FormElement
{
    /**
     * تتطلب هذه الحقول الثلاثة DOM نستطيع توقع أن كل عناصر 
     */
    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;
    }

    /**
     * حقيقي بتنفيذه الإخراجي، لكن يمكننا الافتراض DOM يجب أن يزودنا كل عنصر 
     * (Strings) أنها جميعًا ستعيد مقاطع نصية
     */
    abstract public function render(): string;
}

/**
 * هذا عنصر من نوع الورقة، وككل عناصر الأوراق فإنه لا يملك أي فروع له.
 */
class Input extends FormElement
{
    private $type;

    public function __construct(string $name, string $title, string $type)
    {
        parent::__construct($name, $title);
        $this->type = $type;
    }

    /**
     * بما أن الأوراق ليس لها فروع تفوض إليها المهام فإنها تنفذ المهام التي توكل إليها بأنفسها
     * في نمط المركب.
     */
    public function render(): string
    {
        return "<label for=\"{$this->name}\">{$this->title}</label>\n" .
            "<input name=\"{$this->name}\" type=\"{$this->type}\" value=\"{$this->data}\">\n";
    }
}

/**
 * تنفذ الفئةُ الأساسيةُ للمركب البنيةَ التحتيةَ لإدارة الكائنات الفرعية التي يعاد استخدامها
 * بواسطة كل المركبات الحقيقية.
 * 
 */
abstract class FieldComposite extends FormElement
{
    /**
     * @var FormElement[]
     */
    protected $fields = [];

    /**
     * أساليب إضافة أو حذف الكائنات الفرعية.
     */
    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;
        });
    }

    /**
     * يجب أن يضع أسلوب المركب كائناته الفرعية في حساباته في حين أن أسلوب الورقة عادة ما ينفذ
     * المهام بنفسه، ويستطيع المركب في تلك الحالة أن يقبل البيانات المهيكلة.
     * @param array $data
     */
    public function setData($data): void
    {
        foreach ($this->fields as $name => $field) {
            if (isset($data[$name])) {
                $field->setData($data[$name]);
            }
        }
    }

    /**
     * الذي يعيد البيانات المهيكلة من المركب نفسه إن وجد (Getter) ينطبق نفس المنطق على المستخلِص
     * وكل بيانات الفروع.
     */
    public function getData(): array
    {
        $data = [];
        
        foreach ($this->fields as $name => $field) {
            $data[$name] = $field->getData();
        }
        
        return $data;
    }

    /**
     * التنفيذ الأساسي لإخراج المركب يجمع نتائج كل فروعه، وستكون المركبات الحقيقية قادرة على إعادة
     * استخدام هذا التنفيذ في استخدامات حقيقية.
     */
    public function render(): string
    {
        $output = "";
        
        foreach ($this->fields as $name => $field) {
            $output .= $field->render();
        }
        
        return $output;
    }
}

/**
 * هو مركب حقيقي fieldset عنصر.
 */
class Fieldset extends FieldComposite
{
    public function render(): string
    {
        // fieldset لاحظ كيف تُدمج نتيجة الإخراج المُجمع للفروع في وسم
        $output = parent::render();
        
        return "<fieldset><legend>{$this->title}</legend>\n$output</fieldset>\n";
    }
}

/**
 * form وكذا عنصر .
 */
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";
    }
}

/**
 * تحصل شيفرة العميل على واجهة مريحة لبناء هياكل شجرية معقدة.
 */
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;
}

/**
 * يمكن ملء هياكل الاستمارات ببيانات من مصادر مختلفة، وليس على العميل أن يفحص كل حقول الاستمارات
 * لتعيين بيانات إلى الحقول المختلفة بما أن الاستمارة نفسها يمكنها معالجة ذلك.
 */
function loadProductData(FormElement $form)
{
    $data = [
        'name' => 'Apple MacBook',
        'description' => 'A decent laptop.',
        'photo' => [
            'caption' => 'Front photo.',
            'image' => 'photo1.png',
        ],
    ];

    $form->setData($data);
}

/**
 * تستطيع شيفرة العميل أن تعمل مع عناصر الاستمارات باستخدام الواجهة المجردة، وهكذا لا يهم
 * إن كان العميل يعمل مع عناصر بسيطة أو شجرة مركب معقدة.
 */
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):
    """
    تصرح فئة العنصر الأساسي عن العمليات المشتركة للكائنات البسيطة والمعقدة لمركب ما.
    """

    @property
    def parent(self) -> Component:
        return self._parent

    @parent.setter
    def parent(self, parent: Component):
        """
        لفئة العنصر الأساسي أن تصرح عن واجهة لإعداد أحد الأصول لعنصر ما والوصول إليه في هيكل شجري
        كما تضيف بعض الاستخدامات الافتراضية لهذه الأساليب.
        """

        self._parent = parent

    """
    قد يفيد في بعض الحالات أن تعرّف عمليات إدارة الفروع في فئة العنصر الأساسي مباشرة، وهكذا لن تحتاج
    إلى كشف الفئات الحقيقية للعناصر إلى شيفرة العميل حتى أثناء تجميع الشجرة. لكن المشكلة هنا هي أن 
    هذه الأساليب ستكون فارغة للعناصر التي في مستوى الورقة.
    """

    def add(self, component: Component) -> None:
        pass

    def remove(self, component: Component) -> None:
        pass

    def is_composite(self) -> bool:
        """
        تستطيع إضافة أسلوب يسمح لشيفرة العميل أن تعرف إن كان عنصر ما يستطيع استيعاب فروع له
        """

        return False

    @abstractmethod
    def operation(self) -> str:
        """
        قد ينفذ العنصر الأساسي بعض السلوك الافتراضي أو لا ينفذه للفئات الحقيقية من خلال التصريح عن 
        الأسلوب الحاوي له على أنه تجريدي.
        """

        pass


class Leaf(Component):
    """
    تمثل فئة الورقةُ الكائناتِ النهائيةَ لمركب ما، فلا يمكن لكائن الورقة أن يحتوي على كائنات فرعية
    عادة ما تنفذ كائنات الأوراق المهام التي توكل إليها بينما تفوض الكائنات المركبة تلك المهام إلى
    ما دونها.
    """

    def operation(self) -> str:
        return "Leaf"


class Composite(Component):
    """
     تمثل فئة المركب العناصر المعقدة التي قد تحتوي على فروع، وعادة ما تفوض
    كائنات المركب الأعمال الحقيقية لما دونها ثم تجمع النتيجة النهائية.
    """

    def __init__(self) -> None:
        self._children: List[Component] = []

    """
    يستطيع الكائن المركب أن يضيف أو يحذف عناصرً أخرى -بسيطة أو معقدة- من قائمة الفروع.
    """

    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:
        """
        ينفذ المركب منطقه الأساسي بأن يمر على فروعه كلها بشكل تكراري جامعًا نتائجها، وبما أن فروع 
        المركب تمرر استدعاءاتها لما دونها فإن الشجرة بكاملها تُفحص نتيجة لهذا.
        """

        results = []
        for child in self._children:
            results.append(child.operation())
        return f"Branch({'+'.join(results)})"


def client_code(component: Component) -> None:
    """
    تعمل شيفرة العميل مع كل العناصر من خلال الواجهة الأساسية
    """

    print(f"RESULT: {component.operation()}", end="")


def client_code2(component1: Component, component2: Component) -> None:
    """
    تستطيع شيفرة العميل أن تعمل مع أي عنصر بسيط أو معقد دون الاعتماد على الفئات الحقيقية
    للعناصر، وذلك بفضل كون التصريح عن عمليات إدارة الفروع في فئة العنصر الأساسي.
    """

    if component1.is_composite():
        component1.add(component2)

    print(f"RESULT: {component1.operation()}", end="")


if __name__ == "__main__":
    # تستطيع شيفرة العميل بهذه الطريقة أن تدعم عناصر الورقة البسيطة.
    simple = Leaf()
    print("Client: I've got a simple component:")
    client_code(simple)
    print("\n")

    #  ... إضافة إلى المركبات المعقدة.
    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)

الاستخدام في لغة روبي

المستوى: ★ ★ ☆

الانتشار:  ★ ★ ☆

أمثلة الاستخدام: يكثر استخدام نمط المركَّب في لغة بايثون، إذ يُستخدم لتمثيل هرميات لعناصر من واجهة المستخدم أو شيفرة تعمل مع الرسوم التوضيحية (Graphs).

يمكن ملاحظة نمط المركب من خلال الأساليب السلوكية التي تأخذ نسخة من نفس النوع التجريدي/التنفيذي إلى هيكل شجري.

مثال: مثال تصوري

يوضح هذا المثال بنية نمط الجسر، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟
# تصرح فئة العنصر الأساسي عن العمليات المشتركة للكائنات
# البسيطة والمعقدة لمركب ما.

class Component
  # @return [Component]
  def parent
    @parent
  end

  # لفئة العنصر الأساسي أن تصرح عن واجهة لإعداد أحد الأصول لعنصر ما 
  # والوصول إليه في هيكل شجري كما تضيف بعض الاستخدامات 
  # الافتراضية لهذه الأساليب.

  def parent=(parent)
    @parent = parent
  end

  # قد يفيد في بعض الحالات أن تعرّف عمليات إدارة الفروع في فئة العنصر الأساسي
  # مباشرة، وهكذا لن تحتاج إلى كشف الفئات الحقيقية للعناصر إلى شيفرة العميل 
  # حتى أثناء تجميع الشجرة. لكن المشكلة هنا هي أن هذه الأساليب ستكون فارغة 
  # للعناصر التي في مستوى الورقة.

  def add(component)
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end

  # @abstract
  #
  # @param [Component] component
  def remove(component)
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end

  # تستطيع إضافة أسلوب يسمح لشيفرة العميل أن تعرف إن كان 
  # عنصر ما يستطيع استيعاب فروع له
  def composite?
    false
  end

  # قد يطبق العنصر الأساسي بعض السلوك الافتراضي أو لا يطبقه للفئات
  # الحقيقية من خلال التصريح عن الأسلوب الحاوي له على أنه تجريدي.
  def operation
    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
  end
end

# تمثل فئة الورقةُ الكائناتِ النهائيةَ لمركب ما، فلا يمكن 
# لكائن الورقة أن يحتوي على كائنات فرعية
#
# عادة ما تنفذ كائنات الأوراق المهام التي توكل إليها 
# بينما تفوض الكائنات المركبة تلك المهام إلى ما دونها.
class Leaf < Component
  # return [String]
  def operation
    'Leaf'
  end
end

# تمثل فئة المركب العناصر المعقدة التي قد تحتوي على فروع، وعادة ما تفوض
# كائنات المركب الأعمال الحقيقية لما دونها ثم تجمع النتيجة النهائية.
class Composite < Component
  def initialize
    @children = []
  end

  # يستطيع الكائن المركب أن يضيف أو يحذف عناصرً 
  # أخرى -بسيطة أو معقدة- من قائمة الفروع.


  # @param [Component] component
  def add(component)
    @children.append(component)
    component.parent = self
  end

  # @param [Component] component
  def remove(component)
    @children.remove(component)
    component.parent = nil
  end

  # @return [Boolean]
  def composite?
    true
  end

  # ينفذ المركب منطقه الأساسي بأن يمر على فروعه كلها بشكل تكراري جامعًا 
  # نتائجها، وبما أن فروع المركب تمرر استدعاءاتها لما دونها
  # فإن الشجرة بكاملها تُفحص نتيجة لهذا.

  def operation
    results = []
    @children.each { |child| results.append(child.operation) }
    "Branch(#{results.join('+')})"
  end
end

# تعمل شيفرة العميل مع كل العناصر من خلال الواجهة الأساسية.
def client_code(component)
  puts "RESULT: #{component.operation}"
end

# تستطيع شيفرة العميل أن تعمل مع أي عنصر بسيط أو معقد 
# دون الاعتماد على الفئات الحقيقية للعناصر، وذلك بفضل وجود التصريح عن
# عمليات إدارة الفروع في فئة العنصر الأساسي
def client_code2(component1, component2)
  component1.add(component2) if component1.composite?

  print "RESULT: #{component1.operation}"
end

# تستطيع شيفرة العميل بهذه الطريقة أن تدعم عناصر الورقة البسيطة....
simple = Leaf.new
puts 'Client: I\'ve got a simple component:'
client_code(simple)
puts "\n"

# ...إضافة إلى المركبات المعقدة.
tree = Composite.new

branch1 = Composite.new
branch1.add(Leaf.new)
branch1.add(Leaf.new)

branch2 = Composite.new
branch2.add(Leaf.new)

tree.add(branch1)
tree.add(branch2)

puts 'Client: Now I\'ve got a composite tree:'
client_code(tree)
puts "\n"

puts '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)

الاستخدام في لغة Swift

المستوى: ★ ★ ☆

الانتشار:  ★ ★ ★

أمثلة الاستخدام: يكثر استخدام نمط المركَّب في لغة Swift، إذ يُستخدم لتمثيل هرميات لعناصر من واجهة المستخدم أو شيفرة تعمل مع الرسوم التوضيحية (Graphs).

يمكن ملاحظة نمط المركب من خلال الأساليب السلوكية التي تأخذ نسخة من نفس النوع التجريدي/التنفيذي إلى هيكل شجري.

مثال: مثال تصوري

يوضح هذا المثال بنية نمط الجسر، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

بعد تعلم بنية النمط سيكون من السهل عليك استيعاب المثال التالي المبني على حالة واقعية في لغة Swift.

 Example.swift: مثال تصوري

import XCTest

/// تصرح فئة العنصر الأساسي عن العمليات المشتركة للكائنات 
/// البسيطة والمعقدة لمركب ما.
protocol Component {

    /// لفئة العنصر الأساسي أن تصرح عن واجهة لإعداد أحد الأصول لعنصر ما والوصول
    /// إليه في هيكل شجري كما تضيف بعض الاستخدامات الافتراضية لهذه الأساليب.
    var parent: Component? { get set }

    /// قد يفيد في بعض الحالات أن تعرّف عمليات إدارة الفروع في فئة 
    /// العنصر الأساسي مباشرة، وهكذا لن تحتاج إلى كشف الفئات الحقيقية
    /// للعناصر إلى شيفرة العميل حتى أثناء تجميع الشجرة. لكن المشكلة هنا 
    /// هي أن هذه الأساليب ستكون فارغة للعناصر التي في مستوى الورقة.

    func add(component: Component)
    func remove(component: Component)

    /// تستطيع إضافة أسلوب يسمح لشيفرة العميل أن تعرف إن كان عنصر
    /// ما يستطيع استيعاب فروع له
    func isComposite() -> Bool

    /// قد يطبق العنصر الأساسي بعض السلوك الافتراضي أو يتركه للفئات الحقيقية.
    func operation() -> String
}

extension Component {

    func add(component: Component) {}
    func remove(component: Component) {}
    func isComposite() -> Bool {
        return false
    }
}

/// تمثل فئة الورقةُ الكائناتِ النهائيةَ لمركب ما، فلا 
/// يمكن لكائن الورقة أن يحتوي على كائنات فرعية.
///
/// عادة ما تنفذ كائنات الأوراق المهام التي توكل إليها 
/// بينما تفوض الكائنات المركبة تلك المهام إلى ما دونها.
class Leaf: Component {

    var parent: Component?

    func operation() -> String {
        return "Leaf"
    }
}

/// تمثل فئة المركب العناصر المعقدة التي قد تحتوي على فروع، 
/// وعادة ما تفوض كائنات المركب الأعمال الحقيقية لما دونها
/// ثم تجمع النتيجة النهائية.
class Composite: Component {

    var parent: Component?

    /// يحتوي هذا الحقل على الشجرة الفرعية للعناصر.
    private var children = [Component]()

    /// يستطيع الكائن المركب أن يضيف أو يحذف عناصرً أخرى -بسيطة أو معقدة-
    /// من قائمة الفروع.
    func add(component: Component) {
        var item = component
        item.parent = self
        children.append(item)
    }

    func remove(component: Component) {
        // ...
    }

    func isComposite() -> Bool {
        return true
    }

    /// ينفذ المركب منطقه الأساسي بأن يمر على فروعه كلها بشكل 
    /// تكراري جامعًا نتائجها، وبما أن فروع المركب تمرر استدعاءاتها لما 
    /// دونها فإن الشجرة بكاملها تُفحص نتيجة لهذا.
    func operation() -> String {
        let result = children.map({ $0.operation() })
        return "Branch(" + result.joined(separator: " ") + ")"
    }
}

class Client {

    /// تعمل شيفرة العميل مع كل العناصر من خلال الواجهة الأساسية.
    static func someClientCode(component: Component) {
        print("Result: " + component.operation())
    }

    /// تستطيع شيفرة العميل أن تعمل مع أي عنصر بسيط أو معقد
    /// دون الاعتماد على الفئات الحقيقية، وذلك بفضل وجود التصريح عن
    /// عمليات إدارة الفروع في فئة العنصر الأساسي.
    static func moreComplexClientCode(leftComponent: Component, rightComponent: Component) {
        if leftComponent.isComposite() {
            leftComponent.add(component: rightComponent)
        }
        print("Result: " + leftComponent.operation())
    }
}

/// لنرى كيف سيعمل كل ذلك...
class CompositeConceptual: XCTestCase {

    func testCompositeConceptual() {

        /// تستطيع شيفرة العميل بهذه الطريقة أن تدعم عناصر الورقة البسيطة....
        print("Client: I've got a simple component:")
        Client.someClientCode(component: Leaf())

        /// ...إضافة إلى المركبات المعقدة.
        let tree = Composite()

        let branch1 = Composite()
        branch1.add(component: Leaf())
        branch1.add(component: Leaf())

        let branch2 = Composite()
        branch2.add(component: Leaf())
        branch2.add(component: Leaf())

        tree.add(component: branch1)
        tree.add(component: branch2)

        print("\nClient: Now I've got a composite tree:")
        Client.someClientCode(component: tree)

        print("\nClient: I don't need to check the components classes even when managing the tree:")
        Client.moreComplexClientCode(leftComponent: tree, rightComponent: Leaf())
    }
}

 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 Leaf))

Client: I don't need to check the components classes even when managing the tree:
Result: Branch(Branch(Leaf Leaf) Branch(Leaf Leaf) Leaf)

مثال واقعي

Example.swift: مثال واقعي

import UIKit
import XCTest

protocol Component {

    func accept<T: Theme>(theme: T)
}

extension Component where Self: UIViewController {

    func accept<T: Theme>(theme: T) {
        view.accept(theme: theme)
        view.subviews.forEach({ $0.accept(theme: theme) })
    }
}

extension UIView: Component {}
extension UIViewController: Component {}

extension Component where Self: UIView {

    func accept<T: Theme>(theme: T) {

        print("\t\(description): has applied \(theme.description)")

        backgroundColor = theme.backgroundColor
    }
}

extension Component where Self: UILabel {

    func accept<T: LabelTheme>(theme: T) {

        print("\t\(description): has applied \(theme.description)")

        backgroundColor = theme.backgroundColor
        textColor = theme.textColor
    }
}

extension Component where Self: UIButton {

    func accept<T: ButtonTheme>(theme: T) {

        print("\t\(description): has applied \(theme.description)")

        backgroundColor = theme.backgroundColor
        setTitleColor(theme.textColor, for: .normal)
        setTitleColor(theme.highlightedColor, for: .highlighted)
    }
}


protocol Theme: CustomStringConvertible {

    var backgroundColor: UIColor { get }
}

protocol ButtonTheme: Theme {

    var textColor: UIColor { get }

    var highlightedColor: UIColor { get }

    /// خصائص أخرى.
}

protocol LabelTheme: Theme {

    var textColor: UIColor { get }

    /// خصائص أخرى.
}

/// سمات الأزرار.

struct DefaultButtonTheme: ButtonTheme {

    var textColor = UIColor.red

    var highlightedColor = UIColor.white

    var backgroundColor = UIColor.orange

    var description: String { return "Default Buttom Theme" }
}

struct NightButtonTheme: ButtonTheme {

    var textColor = UIColor.white

    var highlightedColor = UIColor.red

    var backgroundColor = UIColor.black

    var description: String { return "Night Buttom Theme" }
}

/// Label themes سمات العناوين.

struct DefaultLabelTheme: LabelTheme {

    var textColor = UIColor.red

    var backgroundColor = UIColor.black

    var description: String { return "Default Label Theme" }
}

struct NightLabelTheme: LabelTheme {

    var textColor = UIColor.white

    var backgroundColor = UIColor.black

    var description: String { return "Night Label Theme" }
}



class CompositeRealWorld: XCTestCase {

    func testCompositeRealWorld() {

        print("\nClient: Applying 'default' theme for 'UIButton'")
        apply(theme: DefaultButtonTheme(), for: UIButton())

        print("\nClient: Applying 'night' theme for 'UIButton'")
        apply(theme: NightButtonTheme(), for: UIButton())


        print("\nClient: Let's use View Controller as a composite!")

        /// السمة الليلية.
        print("\nClient: Applying 'night button' theme for 'WelcomeViewController'...")
        apply(theme: NightButtonTheme(), for: WelcomeViewController())
        print()

        print("\nClient: Applying 'night label' theme for 'WelcomeViewController'...")
        apply(theme: NightLabelTheme(), for: WelcomeViewController())
        print()

        /// السمة الافتراضية.
        print("\nClient: Applying 'default button' theme for 'WelcomeViewController'...")
        apply(theme: DefaultButtonTheme(), for: WelcomeViewController())
        print()

        print("\nClient: Applying 'default label' theme for 'WelcomeViewController'...")
        apply(theme: DefaultLabelTheme(), for: WelcomeViewController())
        print()
    }

    func apply<T: Theme>(theme: T, for component: Component) {
        component.accept(theme: theme)
    }
}

class WelcomeViewController: UIViewController {

    class ContentView: UIView {

        var titleLabel = UILabel()
        var actionButton = UIButton()

        override init(frame: CGRect) {
            super.init(frame: frame)
            setup()
        }

        required init?(coder decoder: NSCoder) {
            super.init(coder: decoder)
            setup()
        }

        func setup() {
            addSubview(titleLabel)
            addSubview(actionButton)
        }
    }

    override func loadView() {
        view = ContentView()
    }
}

/// من أجل مخرجات أفضل description property غير خصائص الوصف .

extension WelcomeViewController {

    open override var description: String { return "WelcomeViewController" }
}

extension WelcomeViewController.ContentView {

    override var description: String { return "ContentView" }
}

extension UIButton {

    open override var description: String { return "UIButton" }
}

extension UILabel {

    open override var description: String { return "UILabel" }
}

 Output.txt: نتائج التنفيذ

Client: Applying 'default' theme for 'UIButton'
UIButton: has applied Default Buttom Theme

Client: Applying 'night' theme for 'UIButton'
UIButton: has applied Night Buttom Theme

Client: Let's use View Controller as a composite!

Client: Applying 'night button' theme for 'WelcomeViewController'...
ContentView: has applied Night Buttom Theme
UILabel: has applied Night Buttom Theme
UIButton: has applied Night Buttom Theme

الاستخدام في لغة TypeScript

المستوى: ★ ★ ☆

الانتشار:  ★ ★ ★

أمثلة الاستخدام: يكثر استخدام نمط المركَّب في لغة TypeScript، إذ يُستخدم لتمثيل هرميات لعناصر من واجهة المستخدم أو شيفرة تعمل مع الرسوم التوضيحية (Graphs).

يمكن ملاحظة نمط المركب من خلال الأساليب السلوكية التي تأخذ نسخة من نفس النوع التجريدي/التنفيذي إلى هيكل شجري.

مثال: مثال تصوري

يوضح هذا المثال بنية نمط الجسر، ويركز على إجابة الأسئلة التالية:

  • ما الفئات التي يتكون منها؟
  • ما الأدوار التي تلعبها هذه الفئات؟
  • كيف ترتبط عناصر النمط ببعضها؟

 index.ts: مثال تصوري

/**
 * تصرح فئة العنصر الأساسي عن العمليات المشتركة للكائنات 
 * البسيطة والمعقدة لمركب ما.
 */
abstract class Component {
    protected parent: Component;

    /**
     * لفئة العنصر الأساسي أن تصرح عن واجهة لإعداد أحد الأصول لعنصر ما والوصول
     * إليه في هيكل شجري كما تضيف بعض الاستخدامات الافتراضية لهذه الأساليب.
     */
    public setParent(parent: Component) {
        this.parent = parent;
    }

    public getParent(): Component {
        return this.parent;
    }

    /**
     * قد يفيد في بعض الحالات أن تعرّف عمليات إدارة الفروع في فئة 
     * العنصر الأساسي مباشرة، وهكذا لن تحتاج إلى كشف الفئات الحقيقية
     * للعناصر إلى شيفرة العميل حتى أثناء تجميع الشجرة. لكن المشكلة هنا
     * هي أن هذه الأساليب ستكون فارغة للعناصر التي في مستوى الورقة.
     */
    public add(component: Component): void { }

    public remove(component: Component): void { }

    /**
     * تستطيع إضافة أسلوب يسمح لشيفرة العميل أن تعرف إن كان عنصر
     * ما يستطيع استيعاب فروع له
     */
    public isComposite(): boolean {
        return false;
    }

    /**
     * قد يطبق العنصر الأساسي بعض السلوك الافتراضي أو يتركه للفئات الحقيقية.
     * عبر التصريح عن الأسلوب الحاوي للسلوك على أنه مجرد.
     */
    public abstract operation(): string;
}

/**
 * تمثل فئة الورقةُ الكائناتِ النهائيةَ لمركب ما، فلا
 * يمكن لكائن الورقة أن يحتوي على كائنات فرعية.
 *
 * عادة ما تنفذ كائنات الأوراق المهام التي توكل إليها 
 * بينما تفوض الكائنات المركبة تلك المهام إلى ما دونها.
 */
class Leaf extends Component {
    public operation(): string {
        return 'Leaf';
    }
}

/**
 * تمثل فئة المركب العناصر المعقدة التي قد تحتوي على فروع، 
 * وعادة ما تفوض كائنات المركب الأعمال الحقيقية لما دونها
 * ثم تجمع النتيجة النهائية.
 */
class Composite extends Component {
    protected children: Component[] = [];

    /**
     * يستطيع الكائن المركب أن يضيف أو يحذف عناصرً أخرى -بسيطة أو معقدة-
     * من قائمة الفروع.
     */
    public add(component: Component): void {
        this.children.push(component);
        component.setParent(this);
    }

    public remove(component: Component): void {
        const componentIndex = this.children.indexOf(component);
        this.children.splice(componentIndex, 1);

        component.setParent(null);
    }

    public isComposite(): boolean {
        return true;
    }

    /**
     * ينفذ المركب منطقه الأساسي بأن يمر على فروعه كلها بشكل 
     * تكراري جامعًا نتائجها، وبما أن فروع المركب تمرر استدعاءاتها لما 
     * دونها فإن الشجرة بكاملها تُفحص نتيجة لهذا.
     */
    public operation(): string {
        const results = [];
        for (const child of this.children) {
            results.push(child.operation());
        }

        return `Branch(${results.join('+')})`;
    }
}

/**
 * تعمل شيفرة العميل مع كل العناصر من خلال الواجهة الأساسية.
 */
function clientCode(component: Component) {
    // ...

    console.log(`RESULT: ${component.operation()}`);

    // ...
}

/**
 * تستطيع شيفرة العميل بهذه الطريقة أن تدعم عناصر الورقة البسيطة....
 */
const simple = new Leaf();
console.log('Client: I\'ve got a simple component:');
clientCode(simple);
console.log('');

/**
 * ...إضافة إلى المركبات المعقدة.
 */
const tree = new Composite();
const branch1 = new Composite();
branch1.add(new Leaf());
branch1.add(new Leaf());
const branch2 = new Composite();
branch2.add(new Leaf());
tree.add(branch1);
tree.add(branch2);
console.log('Client: Now I\'ve got a composite tree:');
clientCode(tree);
console.log('');

/**
 * تستطيع شيفرة العميل أن تعمل مع أي عنصر بسيط أو معقد.
 * دون الاعتماد على الفئات الحقيقية، وذلك بفضل وجود التصريح عن
 * عمليات إدارة الفروع في فئة العنصر الأساسي.
 */
function clientCode2(component1: Component, component2: Component) {
    // ...

    if (component1.isComposite()) {
        component1.add(component2);
    }
    console.log(`RESULT: ${component1.operation()}`);

    // ...
}

console.log('Client: I don\'t need to check the components classes even when managing the tree:');
clientCode2(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)

انظر أيضًا

مصادر