何时应该使用组合设计模式?

73

我不理解何时应该使用组合设计模式这种设计模式能给我带来哪些好处? 我访问了这个网站,但它只告诉我关于设计模式的结构,而不是它的使用场景。 我希望对像我这样正在学习设计模式的程序员有所裨益。


3
以下 Java 的真实世界示例可能会有用:https://dev59.com/vXI-5IYBdhLWcg3wy7sd - BalusC
谢谢!我会浏览链接的,你有C#的例子吗? - kevin
3
我不会用C#,但模式是与编程语言无关的。当涉及到API时,Java与C#之间并没有太大的差异。在C#中,你肯定也有使用组合模式来将另一个集合嵌入自身的集合API。 - BalusC
1
http://blog.ploeh.dk/2010/12/03/Towardsbetterabstractions/ - 一个稍微不同的方法。在这里,Mark Seemann说:“能够实现有意义的组合是一个健全接口的很好指标。” - Maciej Beimcik
11个回答

86

组合模式是一种在需要有选择性地将层次结构中的一组对象“视为相同”时很有用的模式,通常所用的示例是指对待叶子节点和节点相同,但该模式也可以扩展到异构列表。

例如,考虑看医生。当您去看医生时,通常会先见到护士或助手,他们会测量您的体温等。然后医生进行检查并做出诊断。然后医生可能进行一些治疗,但通常护士会回来完成剩下的工作。访问期间执行不同的活动,如体重和体温观察。但例如实验室将是一个不同的对象,因为它通常需要样本,该样本可能随后被送出并需要在以后记录结果。

因此,我们有了可以记录所有这些内容的软件,并且通常会创建某种层次结构,其中包含节点,例如:

就诊:
预检
检查
治疗

在每个节点下面,您都会有各种条目,例如诊断、观察、实验室程序、诊断、注射等。

这一切都很好,您最终获得了一个结构化的、虽然非常复杂的就诊层次结构记录。

现在假设您需要生成账单。突然,您面临一个非常不同的要求。您的医疗记录是必需的,以创建非常准确的就诊图像。但是在计费中,您并不关心谁做了什么或以何种顺序进行了操作,实际上您并不关心活动是什么,只关心一个可计费活动列表,即代码。

这些信息不仅嵌入在记录中,而且该记录也很难遍历,因为它包含大量不同的对象。它还具有可变的分层结构——如果您头上扎了一根钉子,他们可能会跳过任何形式的预检查,甚至检查,并直接进行治疗。如果您去拆线,可能没有预检查或检查。年度体检没有治疗等等。枚举此类对象图形非常困难。

一个组合模式可以解决所有这些问题。你需要为所有对象定义一个通用的接口或基类,我们称之为“CareEntry”。 CareEntry具有BillingCode属性。现在,您的Encounter看起来像是一个简单的容器,里面只有CareEntry对象。您的计费服务现在可以简单地枚举其中的所有内容,而不必担心某个对象是节点(PreExam,Exam)还是叶子(weight temperature),或者该对象位于哪个节点中(PreExam Exam等),以及对象的实际类型是什么(lab,injection等)。一切都是CareEntry并统一处理 - 您只需枚举Encounter中的所有CareEntry对象并收集每个具有非空计费代码的对象即可完成。就是这么简单。


6
是的,许多应用程序需要处理分层数据。使用组合模式可以统一地处理分层异构数据,而无需转换对象、评估其类型并执行条件判断来查看它们是否包含其他对象。 - Sisyphus

33

摘自《设计模式》

使用组合模式,当:

  • 您想要表示对象的整体-部分层次结构。
  • 您希望客户端能够忽略对象组合和单个对象之间的区别。客户端将统一地处理组合结构中的所有对象。

该模式通常用于书中的激励性示例——图形窗口显示系统,其中可以包含其他窗口和图形元素,例如图像、文本等。组合可以在运行时组合,客户端代码可以操纵所有元素,而不必考虑其类型,以进行诸如绘制等常见操作。


26

组合模式使客户端能够统一地处理单个对象和组合对象。


例如,双击文件夹应该打开该文件夹。双击文件时,它应该在相应的程序中打开。


操作相同,但行为基于是否为单个对象或组合对象。

单个对象和组合对象有共同的接口。

interface Data{
    public void doubleClick();
}

个体对象实现

class File implements Data {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public void doubleClick() {
        System.out.println(this.getName()+" file is Opened in a Program ");
    }
}

组合实现

class Folder implements Data {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    private List<Data> folder = new ArrayList<Data>();

    @Override
    public void doubleClick() {
        System.out.println(this.getName() + " folder is Opened");
        for(Data data : folder) {
            data.doubleClick();
        }
    }

    public void add(Data data) {
        folder.add(data);
    }

    public void remove(Data data) {
        folder.remove(data);
    }
}

客户端程序

public class CompositePattern {

    public static void main(String[] args) {

        Folder f1 = new Folder();f1.setName("Folder 1");
        Folder f2 = new Folder();f2.setName("Folder 2");
        Folder f3 = new Folder();f3.setName("Folder 3");

        File file1 = new File();file1.setName("File 1");
        File file2 = new File();file2.setName("File 2");
        File file3 = new File();file3.setName("File 3");
        File file4 = new File();file4.setName("File 4");

        f1.add(file1);
        f2.add(file2);

        f3.add(f2);
        f3.add(file3);
        f3.add(file4);

        f1.doubleClick();f2.doubleClick();f3.doubleClick();

    }

}

8
我不明白为什么文件夹类有一个循环和双击打开其中所有项目的功能。这意味着如果我单击一个文件夹,它会打开其中的每个文件。 - j2emanue

9
我经常使用组合设计模式来隐藏集合。在许多情况下,当集合中有许多元素和只有一个元素时,我们以完全相同的方式处理集合。
这就是问题所在,因为包含集合的类会充斥着基本上做相同事情的 foreach 循环 - 遍历所有元素并应用某些聚合函数。
为了解决这个问题,我引入了一个接口,该接口由单个元素以及隐藏这些元素集合的类实现。然后,组合类的目的是包含以前在客户端类中的所有聚合函数。
您可以在本文中找到一些有用的示例:使用集合 共同的事情是,组合不一定代表部分整体关系。可以引入组合元素只是为了将循环移出客户端。
以下是应用组合模式将零件隐藏在组合元素后面的标准示例:组合设计模式

7

最近,我研究和尝试了一下组合模式,发现其中一个强大的概念需要记在心里。

组合模式隐藏了集合中所涉及的复杂性,比如循环遍历、排序、筛选等,使你可以将其视为一个单一的整体。

假设你有一个狗在一个狗窝里,另外还有许多只狗。你想要喂食和打疫苗,但如果它们在一小时内进食或在过去的五个小时内已经接种了疫苗,你就不能再给它们喂食或打疫苗,或者在呕吐后为它们接种疫苗等。

更重要的是,有一些包装顺序规则,A品种的狗要先于B品种的狗吃饭,除非C品种的狗在旁边狂吠。

这很快就会达到一个地步,你只想关心一个问题——叫一个帮手来喂“所有的狗”。或者更好的办法是请三个帮手来负责喂食、疫苗、呕吐、狂吠和包装等所有其他令人惊奇的事情。

通过叫出帮手,你就依靠了组合模式。你只需要去“喂”每一个狗窝,无论里面有一只狗还是十只狗。你只想让狗窝自己解决如何喂食,因为在收银台上你手头上已经有太多事情要处理了。

所以,对于你来说,一只狗是IDog,它可以Feed()、Bark()、Vomit()和GetVaccine()。一个狗窝也是一只狗,你可以调用kennel.Feed()。你完成了。狗窝现在必须在内部确定该做什么。它可能有一个时间跟踪机制来跟踪每只狗的喂食和其他生理功能时间。这都被整洁地封装起来了。


2
答案应该是 -
将对象组合成树形结构以表示整体部分层次结构。组合模式使客户端能够统一地处理单个对象和对象组合。
- 递归组合 - “目录包含条目,每个条目都可以是目录。” - 1对多的“拥有”关系上升到“是一个”的层次结构
摘自论坛。

2

当你需要处理二叉树或其他复杂的数据结构,例如列表的列表的列表等时,你可能会发现实现1个接口对于你来说是必须的。这样,当每个元素(类)都实现了1个接口时,你可以在1个叶子节点上或整个组上执行相同的方法,例如复制、添加、删除、移动等,只要你正确地实现了它们。这非常有用且简单。


2

组合设计模式的实际例子,当我们有可能在或组件类型内拥有相同父类型的实例时。

例如:在外汇交易系统Ex1中

您可能会有一个交叉货币对(AUD/EUR)= (AUD/USD和 1/(EUR/USD)) 这里的关键是您的工具(Cross)可以有两个工具(Direct)内部。

在另一个例子中,有

一个工具(Cross)和工具(Direct)和工具(Cross),可以进一步分为两个工具(Direct)。SGD/CZK = USD/SGD (Direct) and USD/CZK (Cross) = USD/SGD (Direct) and (1/EUR/USD)(Direct) and EUR/CZK(Direct)

这里的重点是您不断分割,直到找不到所有直接货币对。

以上可以很容易地使用组合设计模式实现。


1
如果您想构建嵌套的相似对象,可以使用组合模式。例如:在实时环境中,如果您想根据层次结构显示办公室员工的树形结构。

1

顺便问一下,复合 UI 应用程序块是什么?谢谢。 - kevin
来自微软 P&P 团队的可重用代码,用于构建智能客户端应用程序 - http://msdn.microsoft.com/en-us/library/ff648747.aspx - Kumar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接