设计模式
组合模式
在现实生活着,“整体-部分” 的这种关系是很常见的。例如,卖电脑的商家,可以卖单独配件也可以卖组装整机,又如复制文件,可以一个一个文件复制粘贴还可以整个文件夹进行复制,再比如文本编辑,可以给单个字加粗、变色、改字体,当然也可以给整段文字做同样的操作。这些其本质都是同样的问题,对于这些简单对象与复合对象的处理,如果使用组合模式来实现将会很方便。
定义与特点
组合模式有时又叫 ”整体-部分“ 模式,它是一种将对象组合成树状层次结构的模式,用来表示 “整体-部分” 的关系,使用户对单个对象和组合对象具有一致性的访问,是一种结构型设计模式。
组合模式一般用来描述整体与部分的关系,它将对象组织到一个树状结构中,顶层的节点为根节点,根节点下面可以包含树枝节点和叶子节点,树枝节点下面也可以包含树枝节点和叶子节点,而叶子节点下面不可以包含其它节点,其结构图如下:
根节点和树枝节点本质上属于同一种数据结构【都可以包含子节点】,可以作为容器使用;而叶子节点与树枝节点在语义上不属于用一种类型。但是在组合模式中,我们把树枝节点和叶子节点看作属于同一种数据类型(继承或实现统一的抽象类或接口),让它们具备一致行为。这样,在组合模式中,整个树形结构中的对象都属于同一种类型,带来的好处就是用户不需要辨别是树枝节点还是叶子节点,可以直接进行操作,给用户的使用带来极大的便利。
组合模式的优点:
- 组合模式使得客户端代码可以一致的处理单个对象和组合对象,无需关心自己处理的是单个对象还是组合对象,方便了客户端的调用。
- 由于组合模式的对象是同一种类型,所以组合模式可以很方便的增加新的对象,客户端不需要因为加入了新对象而修改源代码,符合 “开闭原则”。
缺点:
- 设计较为复杂,客户端需要花更多时间理清类之间的层次关系。
- 很难限制容器中的组件,很难用继承的方法增加组件的新功能
模式的结构
组合模式主要包含以下角色:
- 抽象组件角色(Component):为树枝组件和树叶组件声明公共的接口,并实现它们默认的行为。在透明式的组合模式中,抽象组件还声明了访问和管理子类【树枝组件和树叶组件】的接口;在安全式的组合模式中,不声明访问和管理子类的接口,管理工作交给树枝组件完成。
- 树枝组件角色(Composite):是组合中的分支节点对象,继承或实现了抽象组件,存在子节点。它的主要作用是存储和管理子组件【包含树枝组件和树叶组件】,通常包含 add、remove、child 等方法。
- 树叶组件角色(Leaf):是组合中的叶子节点,继承或实现了抽象组件,不存在子节点。
其 UML 图如下:
模式的分类
组合模式分为:透明式组合模式 与 安全式组合模式
一、透明式组合模式
该组合模式中,抽象组件声明了所有子类中的全部方法,客户端无需区别树叶对象和树枝对象,对客户端来说是透明的。也就是说在 Component 中声明所有用来管理子对象的方法,其中,包括Add、Remove等。这样实现 Component 接口的所有子类都具备了 Add 和 Remove。这样做的好处就是叶节点和枝节点对于外界没有区别,它们具备完全一致的行为接口。但问题也很明显,因为 Leaf 类本身不具备 Add、 Remove 方法的功能,所以实现它是没有意义的【空实现或抛异常】。
其结构图如下所示:
二、安全式组合模式
该组合模式中,将管理子组件的方法迁移到了树枝组件中,抽象组件和树叶组件没有子组件的管理方法。也就是在 Component 接口中不去声明 Add 和 Remove 方法,那么子类的 Leaf 也就不需要去实现它,而是在 Composite 声明所有用来管理子类对象的方法,这样做就不会出现刚才提到的问题,不过由于不够透明,所以树叶和树枝类具有不同的相同的接口,客户端的调用需要做相应的判断,带来了不便。
模式的使用
这里我们以 “公司-部门” 来介绍组合模式的使用【透明式组合模式】。
- 抽象组件角色:公司抽象类 Company 为抽象组件,其中包含了子组件的所有方法。
- 树枝组件角色:总公司及其分公司为树枝组件,该实例包括:深圳总公司、上海华东分公司、南京办事处、杭州办事处。
- 树叶组件角色:公司下的各部门为树叶组件,该实例包括:人力资源部、财务部、市场部。各个部门分别有不同的职责,人力资源部负责员工的招聘和管理、财务部负责公司财政收支的管理、市场部负责市场相关事务的处理。
/**
* 抽象组件角色 - 公司抽象类
*/
public abstract class Company {
protected String name;
public Company(String name) {
this.name = name;
}
/**
* 增加
* @param company
*/
public abstract void add(Company company);
/**
* 删除
* @param company
*/
public abstract void remove(Company company);
/**
* 显示
* @param depth
*/
public abstract void displayChild(int depth);
/**
* 履行职责
*/
public abstract void lineOfDuty();
}
/**
* 树枝组件角色 - 具体公司类
*/
public class ConcreteCompany extends Company {
private List<Company> children = new CopyOnWriteArrayList<>();
public ConcreteCompany(String name) {
super(name);
}
@Override
public void add(Company company) {
children.add(company);
}
@Override
public void remove(Company company) {
children.remove(company);
}
@Override
public void displayChild(int depth) {
String prefix = new String(new char[depth]).replace('\0', '-');
System.out.println(prefix + name);
for (Company company : children) {
company.displayChild(depth + 2);
}
}
@Override
public void lineOfDuty() {
for (Company company : children) {
company.lineOfDuty();
}
}
}
/**
* 树叶组件角色 - 人事部门
*/
public class HRDepartment extends Company{
public HRDepartment(String name) {
super(name);
}
@Override
public void add(Company company) {
}
@Override
public void remove(Company company) {
}
@Override
public void displayChild(int depth) {
String prefix = new String(new char[depth]).replace('\0', '-');
System.out.println(prefix + name);
}
@Override
public void lineOfDuty() {
System.out.println(name + " 负责员工招聘培训管理");
}
}
/**
* 树叶组件角色 - 财务部门
*/
public class FinanceDepartment extends Company{
public FinanceDepartment(String name) {
super(name);
}
@Override
public void add(Company company) {
}
@Override
public void remove(Company company) {
}
@Override
public void displayChild(int depth) {
String prefix = new String(new char[depth]).replace('\0', '-');
System.out.println(prefix + name);
}
@Override
public void lineOfDuty() {
System.out.println(name + " 负责公司财务收支管理");
}
}
/**
* 树叶组件角色 - 市场部门
*/
public class MarketingDepartment extends Company{
public MarketingDepartment(String name) {
super(name);
}
@Override
public void add(Company company) {
}
@Override
public void remove(Company company) {
}
@Override
public void displayChild(int depth) {
String prefix = new String(new char[depth]).replace('\0', '-');
System.out.println(prefix + name);
}
@Override
public void lineOfDuty() {
System.out.println(name + " 负责公司市场扩展、产品的战略规划、产品的售前售后");
}
}
/**
* 组合模式测试
*/
public class CompositeTest {
public static void main(String[] args) {
ConcreteCompany root = new ConcreteCompany("深圳总公司");
root.add(new HRDepartment("总公司人力资源部"));
root.add(new FinanceDepartment("总公司财务部"));
root.add(new MarketingDepartment("总公司市场部"));
ConcreteCompany huadong = new ConcreteCompany("上海华东分公司");
huadong.add(new HRDepartment("华东分公司人力资源部"));
huadong.add(new FinanceDepartment("华东分公司财务部"));
huadong.add(new MarketingDepartment("华东分公司市场部"));
root.add(huadong);
ConcreteCompany nanjing = new ConcreteCompany("南京办事处");
nanjing.add(new HRDepartment("南京办事处人力资源部"));
nanjing.add(new FinanceDepartment("南京办事处人力资源部"));
huadong.add(nanjing);
ConcreteCompany hangzhou = new ConcreteCompany("杭州办事处");
hangzhou.add(new HRDepartment("杭州办事处人力资源部"));
hangzhou.add(new FinanceDepartment("杭州办事处人力资源部"));
huadong.add(hangzhou);
System.out.println("公司结构图:");
root.displayChild(1);
System.out.println("\n\n");
System.out.println("公司职责:");
root.lineOfDuty();
}
}
运行程序,结果如下:打印显示了公司的结构图 以及 公司旗下的各部门的职责。
若使用安全式组合模式,主要修改如下:
- 抽象组件
Company
移除管理组件的 add、remove 方法。- 树枝组件
ConcreteCompany
提供管理组件的 add、remove 方法。- 树叶组件
HRDepartment
、FinanceDepartment
、MarketingDepartment
移除管理组件的 add、remove 方法【这里只给出HRDepartment
的示例,其它一样操作】。- 组合模式测试类保持不变
运行程序,结果和上面一样。
public abstract class Company {
protected String name;
public Company(String name) {
this.name = name;
}
/**
* 显示
* @param depth
*/
public abstract void displayChild(int depth);
/**
* 履行职责
*/
public abstract void lineOfDuty();
}
public class ConcreteCompany extends Company {
private List<Company> children = new CopyOnWriteArrayList<>();
public ConcreteCompany(String name) {
super(name);
}
public void add(Company company) {
children.add(company);
}
public void remove(Company company) {
children.remove(company);
}
@Override
public void displayChild(int depth) {
String prefix = new String(new char[depth]).replace('\0', '-');
System.out.println(prefix + name);
for (Company company : children) {
company.displayChild(depth + 2);
}
}
@Override
public void lineOfDuty() {
for (Company company : children) {
company.lineOfDuty();
}
}
}
public class HRDepartment extends Company{
public HRDepartment(String name) {
super(name);
}
@Override
public void displayChild(int depth) {
String prefix = new String(new char[depth]).replace('\0', '-');
System.out.println(prefix + name);
}
@Override
public void lineOfDuty() {
System.out.println(name + " 负责员工招聘培训管理");
}
}
总结:组合模式这样就定义了包含人力资源部和财务部这些基本对象和分公司、办事处等组合对象的类层次结构。基本对象可以被组合成更复杂的组合对象,而这个组合对象又可以被组合,这样不断地递归下去,客户代码中,任何用到基本对象的地方都可以使用组合对象了。用户是不用关心到底是处理一个叶节点还是处理一个组合组件,也就用不着为定义组合写一些选择判断语句了。简单点说,就是组合模式让客户可以一致地使用组合结构和单个对象。
模式的应用场景
当你发现需求中是体现部分与整体层次的结构时,以及你希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时,就应该考虑用组合模式了。