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