组合模式

组合模式

案例

我们想开发一个界面控件库,界面控件分为两大类,一类是单元控件,例如按钮、文本框等,一类是容器控件,例如面板。面板界面内可以放入单元控件和其他面板。这样最终得到一个类似窗体的样子。下面就用代码模拟这一过程。

1.首先定义一个面板类:

/**
 * 面板内,可以添加按钮、文本框和其他的面板
 */
public class Panel {
    private String name;
    // 存放面板的容器
    private List<Panel> panelList = new ArrayList<>();
    // 存放按钮的容器
    private List<Button> buttonList = new ArrayList<>();
    // 存放文本框的容器
    private List<TextBox> textBoxList = new ArrayList<>();

    public Panel(String name) {
        this.name = name;
    }

    // 三个添加面板、按钮和文本框的方法
    public void addPanel(Panel panel) {
        panelList.add(panel);
    }

    public void addButton(Button button) {
        buttonList.add(button);
    }

    public void addTextBox(TextBox textBox) {
        textBoxList.add(textBox);
    }

    // 分别调用展示面板、按钮和文本框的方法
    public void show(String prefix) {
        System.out.println(prefix + "展示面板[" + this.name + "]");
        for (Button button : buttonList) {
            button.show(prefix + "--");
        }
        for (TextBox textBox : textBoxList) {
            textBox.show(prefix + "--");
        }
        for (Panel panel : panelList) {
            panel.show(prefix + "--");
        }
    }
}

2.定义按钮组件

/**
 * 按钮组件
 */
public class Button {
    private String name;

    public Button(String name) {
        this.name = name;
    }

    public void show(String prefix) {
        System.out.println(prefix + "展示按钮[" + this.name + "]");
    }
}

3.定义文本框组件

/**
 * 文本框组件
 */
public class TextBox {
    private String name;

    public TextBox(String name) {
        this.name = name;
    }

    public void show(String prefix) {
        System.out.println(prefix + "展示文本框[" + this.name + "]");
    }
}

4.测试在面板上添加按钮、文本框和其他面板:

public class Main {
    public static void main(String[] args) {
        // 面板 A
        Panel panelA = new Panel("A");
        // 面板 A 放入了一个按钮
        panelA.addButton(new Button("A-1"));
        // 面板 A 放入了一个文本框
        panelA.addTextBox(new TextBox("A-2"));
        // 面板 A 放入了另一个面板 B
        Panel panelB = new Panel("A-B");
        // 面板 B 放入了另一个按钮
        panelB.addButton(new Button("A-B-1"));
        // 面板 B 放入了另一个文本框
        panelB.addTextBox(new TextBox("A-B-2"));
        panelA.addPanel(panelB);
        // 展示面板 A 的内容
        panelA.show("");
    }
}

5.测试结果:

展示面板[A]
--展示按钮[A-1]
--展示文本框[A-2]
--展示面板[A-B]
----展示按钮[A-B-1]
----展示文本框[A-B-2]

以上代码,就结果的结构来看与上面的要求是满足的。但是这一编码设计不够灵活,可扩展性也很差。比如我们新增一个密码框组件,我们除了需要新增一个类以外,还需要修改现有的代码,在Panel类中增加对其类的列表维护,还要修改show()方法中的内容。而且Panel类的设计由于需要定义多个集合存储不同的类型的成员并对其成员,本身就比较复杂了。下面就通过使用组合模式对这写问题进行改善。

模式介绍

组合模式(结构型模式),将对象组合成树形结构以表示“部分-整体”的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性。掌握组合模式的重点是要理解清楚 “部分/整体” 还有 ”单个对象“ 与 "组合对象" 的含义。

从上面的定义中可以看出,组合模式区分出单个对象与组合对象来表示部分与整体的关系。从我们的案例来说,其中 Panel就可以看作是组合对象,而ButtonTextBox类就可以看作是单个对象。

角色构成:

  • Component(抽象构件):它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问及管理它的子构件的方法,如增加子构件、删除子构件、获取子构件等。
  • Leaf(叶子构件):它在组合结构中表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过异常等方式进行处理。
  • Composite(容器构件):它在组合结构中表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。

从角色构成上可以看出组合模式,通过引入抽象构件类Component,同时使用容器构件类Composite和叶子构件类Leaf,使得客户端只需针对Component类进行编码。

UML类图:

composite

从图中我们可以看出,组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,使得客户端可以对其进行统一处理。同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器。

那么在最开始的案例中,我们的Panel类就可以看作是一个容器类,而ButtonTextBox类就可以看作是叶子节点。在这之前还需要引入一个Component抽象构件类,下面就根据这一思路对代码进行改造。

代码改造

1.首先引入Component抽象构件类:

/**
 * 抽象构件类角色
 */
public abstract class Component {
    // 添加成员
    public abstract void add(Component c);
    // 不同的实现类实现不同的展示方式
    public abstract void show(String prefix);
}

2.容器构件类Panel

/**
 * 容器构件类角色
 */
public class Panel extends Component {
    private String name;
    private List<Component> list = new ArrayList<>();

    public Panel(String name) {
        this.name = name;
    }

    @Override
    public void add(Component c) {
        list.add(c);
    }

    @Override
    public void show(String prefix) {
        System.out.println(prefix + "展示面板[" + this.name + "]");
        for (Component component : list) {
            component.show(prefix + "--");
        }
    }
}

3.两个叶子构建类:

按钮组件:

/**
 * 叶子构件类:按钮组件
 */
public class Button extends Component {
    private String name;

    public Button(String name) {
        this.name = name;
    }

    @Override
    public void add(Component c) {
        // 这里通过抛异常的方式,拒绝添加子构件
        throw new UnsupportedOperationException();
    }

    public void show(String prefix) {
        System.out.println(prefix + "展示按钮[" + this.name + "]");
    }
}

文本框组件:

/**
 * 叶子构件类:文本框组件
 */
public class TextBox extends Component {
    private String name;

    public TextBox(String name) {
        this.name = name;
    }

    @Override
    public void add(Component c) {
        // 这里通过抛异常的方式,拒绝添加子构件
        throw new UnsupportedOperationException();
    }

    public void show(String prefix) {
        System.out.println(prefix + "展示文本框[" + this.name + "]");
    }
}

4.测试类:

public class Main {
    // 这里我们只用针对抽象类 Component 编程
    public static void main(String[] args) {
        // 面板 A
        Component panelA = new Panel("A");
        // 面板 A 放入了一个按钮
        panelA.add(new Button("A-1"));
        // 面板 A 放入了一个文本框
        panelA.add(new TextBox("A-2"));
        // 面板 A 放入了另一个面板 B
        Component panelB = new Panel("A-B");
        // 面板 B 放入了另一个按钮
        panelB.add(new Button("A-B-1"));
        // 面板 B 放入了另一个文本框
        panelB.add(new TextBox("A-B-2"));
        panelA.add(panelB);
        // 展示面板 A 的内容
        panelA.show("");
    }
}

5.测试结果:

展示面板[A]
--展示按钮[A-1]
--展示文本框[A-2]
--展示面板[A-B]
----展示按钮[A-B-1]
----展示文本框[A-B-2]

测试结果与上面最开始的测试结果是一模一样的,但是我们通过引入了Component抽象类,使得客户端只用针对Component类进行编程。同时我们在添加新的叶子构件,如一个密码框时,只需要继承Component类就可以达到扩展的目的,符合“开闭原则”。

模式应用

在我们使用 Java 来开发界面应用时使用到的java.swing.*包下面的类中就存在组合模式的应用。先来看一段简单的创建窗口的代码。

​ 1.创建一个窗体:

public class Main {
    public static void main(String[] args) {
        // 创建 JFrame 实例
        JFrame jf = new JFrame();
        // 设置宽高
        jf.setSize(200, 100);
        // 设置在窗口中间打开
        jf.setLocationRelativeTo(null);
        // 设置默认关闭操作
        jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        // 创建面板,类似于 html 中的 div
        JPanel panel = new JPanel();

        // 创建一个输入框
        JTextField textField = new JTextField(8);
        // 添加到面板中
        panel.add(textField);

        // 创建一个按钮
        JButton btn = new JButton("提交");
        // 添加到面板中
        panel.add(btn);

        // 添加面板到 JFrame 中
        jf.add(panel);
        // 设置界面可见
        jf.setVisible(true);
    }
}

2.运行结果:

composite-JFrame

在窗体上,先创建了一个JPanel面板,然后创建并添加了一个JTextField输入框和一个JButton按钮,最后把面板放入到JFrame中。为什么说这里用到了组合模式呢?下面它们之间的UML类图。

composite-swing

从类中可以看到抽象类Component就是组合模式中的抽象构件,JFrameJPanel类作为容器构件角色,而JButtonJTextField类作为叶子构件。这样的使用时容器构件中可以容纳其他容器构件,如代码中的jf.add(panel);。同时也可以在容器构件中添加叶子构件如panel.add(textField);panel.add(btn);

总结

1.主要优点

  • 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
  • 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
  • 在组合模式中增加新的容器构件和叶子构件都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
  • 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子对象和容器对象的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。

2.主要缺点

  • 在增加新构件时很难对容器中的构件类型进行限制。有时候我们希望一个容器中只能有某些特定类型的对象,例如在某个文件夹中只能包含文本文件,使用组合模式时,不能依赖类型系统来施加这些约束,因为它们都来自于相同的抽象层,在这种情况下,必须通过在运行时进行类型检查来实现,这个实现过程较为复杂。

3.适用场景

  • 在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们。
  • 在一个使用面向对象语言开发的系统中需要处理一个树形结构。
  • 在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型。

参考资料

本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/composite
转载请说明出处,本篇博客地址:https://www.jianshu.com/p/cc5a931b8771

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342