نمط المركَّب

من موسوعة حسوب
مراجعة 05:26، 19 فبراير 2019 بواسطة أسامه-دمراني (نقاش | مساهمات) (3.1 تمام التعديل على الصور)

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

المشكلة

(ش.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)

انظر أيضًا

مصادر