把书读薄 | 《设计模式之美》设计模式与范式(结构型-组合模式)

简介: 本文对应设计模式与范式:结构型(53),组合模式 (Composite Pattern),又称 部分整体模式,不要跟前面讲的类间的 组合关系 混淆!!!组合模式是用来 处理树形结构数据(对象集合) 的。数据必须要能表示成树形结构,导致日常开发中不怎么常用,但如果数据能满足树形结构,应用此模式有奇效(代码简洁)。

0x1、定义


将对象组合成 树形结构 以表示整个部分的层次结构,让用户可以 统一对待 对象和对象的组合。


云里雾里?写个简单例子帮助理解~


0x2、写个简单例子


还是奶茶店的例子(树形结构):


网络异常,图片无法展示
|


不难分解成两类,并假设有后面的需求:


  • 菜单:菜单名、描述信息、增删子菜单或饮品;


  • 饮品:名称、描述信息、价格、打印信息


下面我们用常规代码实现一波:


// 抽象出一个饮品的类(此处是为了偷懒减少方法重写)
public abstract class Drink {
    protected String name;
    protected String desc;
    protected int price;
    public Drink(String name, String desc, int price) {
        this.name = name;
        this.desc = desc;
        this.price = price;
    }
    protected String printMsg() {
        return "*【" + name + "】- " + desc + " - " + price + "元";
    }
}
// 对应各种子类饮品
public class FruitTea extends Drink {
    public FruitTea(String name, String desc, int price) { super(name, desc, price); }
}
public class IceCream extends Drink {
    public IceCream(String name, String desc, int price) { super(name, desc, price); }
}
public class MilkTea extends Drink {
    public MilkTea(String name, String desc, int price) { super(name, desc, price); }
}
// 菜单类(支持子菜单、和饮品增删,此处偷懒只实现添加方法~)
public class Menu {
    private String name;
    private String desc;
    private List<Menu> subMenus = new ArrayList<>();
    private List<FruitTea> fruitTeas = new ArrayList<>();
    private List<IceCream> iceCreams = new ArrayList<>();
    private List<MilkTea> milkTeas = new ArrayList<>();
    public Menu(String name, String desc) {
        this.name = name;
        this.desc = desc;
    }
    void addMenu(Menu menu) { subMenus.add(menu); }
    void addFruitTea(FruitTea tea) { fruitTeas.add(tea); }
    void addIceCream(IceCream iceCream) { iceCreams.add(iceCream); }
    void addMilkTea(MilkTea tea) { milkTeas.add(tea); }
    String printMsg() {
        StringBuilder sb = new StringBuilder("【菜单】 → " + name + ":" + desc);
        for(Menu menu: subMenus) {
            sb.append("\n【子菜单】 → ").append(menu.name).append(":").append(menu.desc).append("\n");
            for(FruitTea tea: menu.fruitTeas) { sb.append(tea.printMsg()).append("\n"); }
            for(IceCream iceCream: menu.iceCreams) { sb.append(iceCream.printMsg()).append("\n"); }
            for(MilkTea milkTea: menu.milkTeas) { sb.append(milkTea.printMsg()).append("\n"); }
        }
        return sb.toString();
    }
}
// 测试用例(就是把饮品加到菜单里,然后加入大菜单里~)
public class MenuTest {
    public static void main(String[] args) {
        Menu mainMenu = new Menu("大菜单", "包含子菜单和所有饮品");
        Menu fruitTeaMenu = new Menu("果茶", "真鲜果茶");
        Menu iceCreamMenu = new Menu("冰淇淋", "鲜冰淇淋");
        Menu milkTeaMenu = new Menu("奶茶", "现煮奶茶");
        FruitTea lemonWater = new FruitTea("冰鲜柠檬水", "本店好评第1名", 5);
        FruitTea hitOrange = new FruitTea("棒打鲜橙", "深圳橙子人气飙升第5名", 7);
        IceCream milkShake = new IceCream("草莓摇摇奶昔", "本店人气飙升第4名", 7);
        IceCream sundae = new IceCream("草莓雪王大圣代", "龙岗区草莓圣代人气飙升第4名", 7);
        MilkTea jelly = new MilkTea("外婆烧仙草(大杯)", "现熬嫩滑烧仙草", 10);
        MilkTea milkTea = new MilkTea("霸霸椰果奶茶", "Q弹椰果、融入香醇奶茶", 9);
        fruitTeaMenu.addFruitTea(lemonWater);
        fruitTeaMenu.addFruitTea(hitOrange);
        iceCreamMenu.addIceCream(milkShake);
        iceCreamMenu.addIceCream(sundae);
        milkTeaMenu.addMilkTea(jelly);
        milkTeaMenu.addMilkTea(milkTea);
        mainMenu.addMenu(fruitTeaMenu);
        mainMenu.addMenu(iceCreamMenu);
        mainMenu.addMenu(milkTeaMenu);
        System.out.println(mainMenu.printMsg());
    }
}


运行输出结果如下


网络异常,图片无法展示
|


上述这种实现方式,增删饮品或子菜单,原有代码要进行好几处修改,扩展性较差。对于这种 可以表示成树结构,业务需求通过递归遍历来实现的场景,可以直接套组合模式进行改造:


// 抽象组件(抽象出可以代表菜单、又可以代表饮品的类)
public abstract class AbstractComponent {
    abstract void add(AbstractComponent component);
    abstract AbstractComponent get(int index);
    abstract String printMsg();
}
// 叶子组件(这里是为了偷懒,不用重写那么那么多方法~)
public class AbstractDrink extends AbstractComponent {
    protected String name;
    protected String desc;
    protected int price;
    public AbstractDrink(String name, String desc, int price) {
        this.name = name;
        this.desc = desc;
        this.price = price;
    }
    @Override void add(AbstractComponent menu) { /* 饮品不需要使用这个 */ }
    @Override AbstractComponent get(int index) { return null; }
    @Override String printMsg() {
        return "*【" + name + "】- " + desc + " - " + price + "元";
    }
}
// 具体叶子结点
public class FruitTea extends AbstractDrink {
    public FruitTea(String name, String desc, int price) { super(name, desc, price); }
}
public class IceCream extends AbstractDrink {
    public IceCream(String name, String desc, int price) { super(name, desc, price); }
}
public class MilkTea extends AbstractDrink {
    public MilkTea(String name, String desc, int price) { super(name, desc, price); }
}
// 容器组件
public class AbstractMenu extends AbstractComponent {
    private String name;
    private String desc;
    private List<AbstractComponent> menus;
    public AbstractMenu(String name, String desc) {
        this.name = name;
        this.desc = desc;
        this.menus = new ArrayList<>();
    }
    @Override void add(AbstractComponent component) { menus.add(component); }
    @Override AbstractComponent get(int index) { return menus.get(index); }
    @Override String printMsg() {
        StringBuilder sb = new StringBuilder("【菜单】 → " + name + ":" + desc + "\n");
        for(AbstractComponent menu: menus) {
            sb.append(menu.printMsg()).append("\n");
        }
        return sb.toString();
    }
}
// 测试用例
public class MenuTest {
    public static void main(String[] args) {
        AbstractComponent mainMenu = new AbstractMenu("大菜单", "包含子菜单和所有饮品");
        AbstractComponent fruitTeaMenu = new AbstractMenu("果茶", "真鲜果茶");
        AbstractComponent iceCreamMenu = new AbstractMenu("冰淇淋", "鲜冰淇淋");
        AbstractComponent milkTeaMenu = new AbstractMenu("奶茶", "现煮奶茶");
        AbstractComponent lemonWater = new FruitTea("冰鲜柠檬水", "本店好评第1名", 5);
        AbstractComponent hitOrange = new FruitTea("棒打鲜橙", "深圳橙子人气飙升第5名", 7);
        AbstractComponent milkShake = new IceCream("草莓摇摇奶昔", "本店人气飙升第4名", 7);
        AbstractComponent sundae = new IceCream("草莓雪王大圣代", "龙岗区草莓圣代人气飙升第4名", 7);
        AbstractComponent jelly = new MilkTea("外婆烧仙草(大杯)", "现熬嫩滑烧仙草", 10);
        AbstractComponent milkTea = new MilkTea("霸霸椰果奶茶", "Q弹椰果、融入香醇奶茶", 9);
        // 直接add,不需要具体区分,强调了统一处理~
        fruitTeaMenu.add(lemonWater);
        fruitTeaMenu.add(hitOrange);
        iceCreamMenu.add(milkShake);
        iceCreamMenu.add(sundae);
        milkTeaMenu.add(jelly);
        milkTeaMenu.add(milkTea);
        mainMenu.add(fruitTeaMenu);
        mainMenu.add(iceCreamMenu);
        mainMenu.add(milkTeaMenu);
        System.out.println(mainMenu.printMsg());
    }
}


运行输出结果如下


网络异常,图片无法展示
|


代码看着多,其实很简单,完美呼应组合模式的两个关键:树形结构 + 统一对待。顺带画出UML类图和角色介绍:


网络异常,图片无法展示
|


  • Component (抽象组件) → 为组合中的对象声明接口,客户端通过这个接口访问和管理整个对象结构;


  • Composite (容器组件) → 继承抽象组件,包含多个结点的复合对象,它下面还可以有其他容器组件或叶子组件;


  • Leaf (叶子组件) → 继承抽象组件,定义实现叶子对象的行为,原子对象,它下面不会有其他组件了。


另外,还根据 抽象组件是否声明了用于管理成员的方法,划分为:透明组合模式安全组合模式,上面的例子就是透明组合模式(标准形式),好处是可以确保所有组件类都有相同的接口,缺点是 不够安全,叶子组件和容器组件在本质上是有区别的,编译器叶子结点调容器组件方法不会报错,但运行阶段调用可能会出错(没提供响应的错误处理代码的话~)


优点:


客户端无需操心面对的是组合对象还是叶结点对象,不需要写一大堆if语句来保证对正确的对象调用了正确的方法,通常只需要对整个结构调用一个方法并执行操作即可。


缺点:


新增组件会带来一些问题,如很难限制组合中的组件类型,在检测组件类型时不能依靠编译器类型来束完成,必须在运行时动态检测。


组合模式的一些经典应用例子:


  • java.awt
  • Java集合
  • Mybatis SqlNode


限于篇幅就不展开了,例子详细讲解可见:设计模式 | 组合模式及典型应用


相关文章
|
1天前
|
设计模式 存储 缓存
第三篇 结构型设计模式 - 简化复杂系统的结构
第三篇 结构型设计模式 - 简化复杂系统的结构
|
2天前
|
设计模式 Java
【设计模式】文件目录管理是组合模式吗?
【设计模式】文件目录管理是组合模式吗?
7 0
|
2天前
|
设计模式 存储 Java
[设计模式Java实现附plantuml源码~结构型]实现对象的复用——享元模式
[设计模式Java实现附plantuml源码~结构型]实现对象的复用——享元模式
|
2天前
|
设计模式 JavaScript Java
[设计模式Java实现附plantuml源码~结构型]处理多维度变化——桥接模式
[设计模式Java实现附plantuml源码~结构型]处理多维度变化——桥接模式
|
2天前
|
设计模式 Java Go
[设计模式Java实现附plantuml源码~结构型]不兼容结构的协调——适配器模式
[设计模式Java实现附plantuml源码~结构型]不兼容结构的协调——适配器模式
|
2天前
|
设计模式 JavaScript Java
[设计模式Java实现附plantuml源码~结构型]对象的间接访问——代理模式
[设计模式Java实现附plantuml源码~结构型]对象的间接访问——代理模式
|
2天前
|
设计模式 安全 Java
[设计模式Java实现附plantuml源码~结构型]树形结构的处理——组合模式
[设计模式Java实现附plantuml源码~结构型]树形结构的处理——组合模式
|
2天前
|
设计模式 JavaScript Java
[设计模式Java实现附plantuml源码~结构型] 扩展系统功能——装饰模式
[设计模式Java实现附plantuml源码~结构型] 扩展系统功能——装饰模式
|
2天前
|
设计模式 JavaScript 前端开发
[设计模式Java实现附plantuml源码~结构型] 提供统一入口——外观模式
[设计模式Java实现附plantuml源码~结构型] 提供统一入口——外观模式
|
2天前
|
设计模式 Go 网络安全
[设计模式 Go实现] 结构型~代理模式
[设计模式 Go实现] 结构型~代理模式