19.分公司=一部门 - 组合模式(Composite Pattern)

19.1 分公司不就是一部门吗?

  • 时间: 5月10日19点   地点: 小菜大鸟住所的客厅   人物: 小菜, 大鸟

     “大鸟, 请教你一个问题? 快点帮帮我.”

     “今天轮到你做饭, 你可别忘了.”
     “做饭? 好说好说! 先帮我解决问题吧. 再弄不出来, 我就要失业了.”
     “有这么严重吗! 什么问题呀.”
     “我们公司最近接手了一个项目, 是为一家在全国许多城市都有分销机构的大公司做办公管理系统, 总部由人力资源, 财务, 运营等部门.”
     “这是很常见的OA 系统, 需求分析好的话, 应该不难开发的.”
     “是呀, 我开始也这么想, 这家公司试用了我们开发的系统后感觉不错, 他们希望可以在他们的全部分公司推广, 一起使用. 他们在北京有总部, 在全国几大城市设有分公司, 比如在上海设有华东区分部, 然后在一些省会城市还设有办事处, 比如南京办事处, 杭州办事处. 现在有个问题是, 总公司的人力资源部, 财务部等办公管理功能在所有的分公司或办事处都要有. 你说怎么办.”
     “你打算怎么办呢?” 大鸟不答反问道.
     “因为你之前讲过的简单的复制是最糟糕的设计, 所以我的想法是共享功能到各个分公司, 也就是让总部, 分公司 办事处用同一套代码, 只是根据 ID 不同来区分.”
     “要遭了.”
     “你怎么知道, 的确是不行, 因为他们的要求, 总部, 分部和办事处是树状结构, 也就是有组织结构的, 不可以简单的平行管理. 这下我就比较痛苦了, 因为实际开发时就得一个一个的判断他是总部, 还是分公司, 然后在执行其相应的方法.”



     “你有没有发现, 类似的这种部分由总体的情况很多件, 例如买电脑的商家, 可以卖单独配件也可以卖组装整机, 又如复制文件, 可以一个一个复制粘贴还可以整个文件夹进行复制, 再比如文本编辑, 可以给单个字体加粗, 变色, 改字体, 当然也可以给整段文字做同样的操作. 其本质都是同样的问题.”
     “你的意思是, 分公司或办事处与总公司的关系, 就是部分与整体的关系?”
     “对的, 你希望总公司的组织结构, 比如人力资源部, 财务部的管理功能可以复用于分公司. 这其实就是整体与部分可以被一致对待的问题.”
     “哈, 我明白了, 就像你举的例子, 对于 Word 文档里的文字, 对单个字的处理核对多个字, 甚至整个文档的处理, 其实是一样的, 用户希望一致对待, 程序开发者也希望一致处理. 但具体怎么做呢?”
     “首先, 我们来分析一下你刚才讲到的这个项目, 如果把北京总部公司当做一个大树的树根的话, 他的下属分公司其实就是这个数的什么?”
     “是树的分支, 哦, 至于各个办事处是更小的分支, 而他们的相关职能部门由于没有分支了, 可以理解为树叶.”
     “小菜理解的很快, 尽管天下没有两片相同的树叶, 但同一棵树上长出的树叶也差别不到哪里去. 也就是说, 你所希望的总部的财务部门管理功能最好也能服用到子公司, 那么最好的办法就是, 我们在处理总公司的财务和处理子公司的财务的财务管理功能的方法都是一样的.”
     “有点晕了, 别绕弯子了, 你是不是想讲一个新的设计模式给我.”

19.2 组合模式

     “哈, 小菜够直接. 这个模式叫做 ‘组合模式’.”

组合模式(Composite), 将对象组合成树结构以表示, ‘部分—整体’ 的结构层次. 组合模式使得用户对单个对象和对组合对象的使用具有一致性.[DP]



     Component 为组合模式中的对象声明的接口, 在适当情况下, 实现所有类共有接口的默认行为. 声明一个接口用于访问和管理 Component 的子部件.

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class Component {
protected String name;
public Component(String name) {
this.name = name;
}
public abstract void add(Component c);
public abstract void remove(Component c);
public abstract void display(int depth);
}

     Leaf 在组合中表示叶节点对象, 叶节点没有子节点.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Leaf extends Component {
public Leaf(String name) {
super(name);
}
// 接口一致性, 这里可以抛出一个异常.
@Override
public void add(Component c) {
System.out.println("Cannot add to a leaf");
}
@Override
public void remove(Component c) {
System.out.println("Cannot remove from a leaf");
}
@Override
public void display(int depth) {
char[] level = new char[depth];
for (int i = 0; i< level.length; i++) {
level[i] ='-';
}
System.out.println(level.toString() + name);
}
}

     Composite 定义有节点行为, 用来存储子部件, 在 Component 接口中是想与子部件有关的操作, 比如增加 add 和 删除 remove.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Composite extends Component {
// 用一个对象集合来存储下属的枝节点与叶子节点
private List<Component> kids = new LinkedList<>();
public Composite(String name) {
super(name);
}
@Override
public void add(Component c) {
kids.add(c);
}
@Override
public void remove(Component c) {
kids.remove(c);
}
@Override
public void display(int depth) {
char[] level = new char[depth];
for (int i = 0; i < level.length; i++) {
level[i] = '-';
}
System.out.println(level.toString() + name);
// 遍历每一个节点
for (Component kid : kids) {
kid.display(depth + 2);
}
}
}

     客户端代码, 能通过 Component 接口操作组合部件的对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) {
Composite root = new Composite("root");
root.add(new Leaf("leaf A"));
root.add(new Leaf("leaf B"));
Composite comp = new Composite("Composite X");
comp.add(new Leaf("leafX A"));
comp.add(new Leaf("leafX B"));
root.add(comp);
Composite comp2 = new Composite("Composite XY");
comp2.add(new Leaf("leaf XYA"));
comp2.add(new Leaf("leaf XYB"));
comp.add(comp2);
root.add(new Leaf("leaf C"));
Leaf leaf = new Leaf("leaf D");
root.add(leaf);
root.remove(leaf);
root.display(1); // 显示树根

     结果显示

1
2
3
4
5
6
7
8
9
10
-root
---leaf A
---leaf B
---Composite X
-----leafX A
-----leafX B
-----Composite XY
-------leaf XYA
-------leaf XYB
---leaf C

19.3 透明方式与安全方式

     “虽然可能有无数的分支, 但只需要反复使用 Composite 就可以实现树状结构了. 小菜感觉如何呢?”
     “有点懂, 但是还有疑问, 为什么leaf 类当中也有add()remove() 方法, 树叶不是不能长分支吗?”
     “是的, 这种方式叫做透明方式, 也就是说在 Component 中声明原有所采用的管理子对象的方法, 其中包括 add, remove等. 这样实现 Component 接口的所有子类都具备了 add 和 remove. 这样做的好处是, 叶节点和直接点对外界来说没有区别, 他们完全具备一致性的接口. 但问题也很明显, 因问题也很明显, 因为 leaf 类本身不具备 add, remove 方法的功能, 所以实现他们也是没有意义的.
     “哦, 那么如果我们不希望做这样的无用功呢? 也就是 leaf 类当中不用add 和 remove 方法, 可以吗?”
     “当然是可以的, 那么就需要安全方式, 也就是在Component 接口中不去声明 add 和 remove 方法, 那么子类的 leaf 也就不需要去实现它, 而是在 Composite 声明所有用来管理子类对象的方法, 这样做就不会出现刚才的问题, 不过由于不够透明, 所以树叶和树枝类将不具有相同的接口, 客户端的调用要做相应的判断, 带来了不便.
     “那我喜欢透明方式, 那样就不用做任何判断了.”
     “开发怎么能随便有倾向性? 两者各有好处, 视情况而定吧.”

19.4 何时使用组合模式

     “什么地方用组合模式比较好呢?”
     “当你发现需求中是体现部分与整体层次的结构时, 以及你希望用户可以忽略组合对象与单个对象的不同, 统一的实用组合结构中的所有对象时, 就应该考虑使用组合模式了.”
     “哦, 我想起来了. 以前曾经用过的 ASP.Net 的 TreeView 控件就是典型的组合模式的应用.”
     “又何止是这个, 你应该写过自定义控件吧, 也就是把一些基本的空间组合起来, 通过编写成一个定制的控件, 比如用两个文本框和一个按钮就可以写一个自定义的登陆控件, 实际上, 所有的Web 控件的基类都是system.Web.UI.Control, 而Control 基类中就有add 和 remove 方法, 这就是典型的组合模式的应用.”
     “哦, 对的对的, 这就是部分与整体的关系.”
     “好了, 你是不是可以把你提到的公司管理系统的例子练习一下了?”
     “OK, 现在感觉不是很困难了.”

19.5 公司管理系统

     半小时后, 小菜写出了代码.



     公司类, 抽象类或接口.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Company {
protected String name;
public Company(String name) {
this.name = name;
}
public abstract void add(Company c);
public abstract void remove(Company c);
public abstract void display(int depth);
public abstract void lineOfDuty();
}

     具体公司类 实现接口 树枝节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ConcreteCompany extends Company{
private List<Company> kids = new LinkedList<>();
public ConcreteCompany(String name) {
super(name);
}
@Override
public void add(Company c) { kids.add(c); }
@Override
public void remove(Company c) { kids.remove(c); }
@Override
public void display(int depth) {
char[] level = new char[depth];
for (int i = 0; i<level.length; i++) {
level[i] = '-';
}
System.out.println(new String(level) + name);
for (Company c : kids) {
c.display(depth + 2);
}
}
// 履行相应的职责
@Override
public void lineOfDuty() {
for (Company c : kids) {
c.lineOfDuty();
}
}
}

     人力资源与财务部类 树叶节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 人力资源部
public class HRDepartment extends Company {
public HRDepartment(String name) {
super(name);
}
@Override
public void add(Company c) { }
@Override
public void remove(Company c) { }
@Override
public void display(int depth) {
char[] level = new char[depth];
for (int i = 0; i<level.length; i++) {
level[i] = '-';
}
System.out.println(new String(level) + 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 c) { }
@Override
public void remove(Company c) { }
@Override
public void display(int depth) {
char[] level = new char[depth];
for (int i = 0; i<level.length; i++) {
level[i] = '-';
}
System.out.println(new String(level) + name);
}
@Override
public void lineOfDuty() {
System.out.println(name + "公司财务收支管理");
}
}

     客户端代码调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String[] args) {
ConcreteCompany root = new ConcreteCompany("北京公司总部");
root.add(new HRDepartment("总公司人力资源总部"));
root.add(new FinanceDepartment("总公司财务部"));
ConcreteCompany comp = new ConcreteCompany("上海华东分公司");
comp.add(new HRDepartment("华东公司人力资源部"));
comp.add(new FinanceDepartment("华东公司财务部"));
root.add(comp);
ConcreteCompany comp1 = new ConcreteCompany("南京办事处");
comp1.add(new HRDepartment("南京办事处人力资源部"));
comp1.add(new FinanceDepartment("南京办事处财务部"));
comp.add(comp1);
ConcreteCompany comp2 = new ConcreteCompany("杭州办事处");
comp1.add(new HRDepartment("杭州办事处人力资源部"));
comp1.add(new FinanceDepartment("杭州办事处财务部"));
comp.add(comp2);
System.out.println("结构图:");
root.display(1);
System.out.println("职责:");
root.lineOfDuty();
}

     结果显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
结构图:
-北京公司总部
---总公司人力资源总部
---总公司财务部
---上海华东分公司
-----华东公司人力资源部
-----华东公司财务部
-----南京办事处
-------南京办事处人力资源部
-------南京办事处财务部
-------杭州办事处人力资源部
-------杭州办事处财务部
-----杭州办事处
职责:
总公司人力资源总部员工招聘培训管理
总公司财务部公司财务收支管理
华东公司人力资源部员工招聘培训管理
华东公司财务部公司财务收支管理
南京办事处人力资源部员工招聘培训管理
南京办事处财务部公司财务收支管理
杭州办事处人力资源部员工招聘培训管理
杭州办事处财务部公司财务收支管理

19.6 组合模式的好处

     “小菜写的不错, 你想想看, 这样写的好处有哪些?”
     “组合模式这样就定义了包含人力资源部和财务部这些基本对象和分公司, 办事处等组合对象的类层次结构. 基本对象可以被组合成更复杂的组合对象, 而这个组合对象又可以被组合, 这样不断的递归下去, 客户代码中, 任何用到基本对象的地方都可以使用组合对象了
     “非常好, 还有没有.”
     “我感觉用户是不用关心到底是处理一个节点还是处理一个组合组件, 也就是用不着为了定义组合而写一些选择判断语句了.
     “简单点儿说, 就是组合模式让客户可以一致的实用组合结构和单个对象.
     “这也就是说, 那家公司开多少个以及多少级办事处都没有问题了.” 小才开始兴奋起来.
     “喂, 发什么神经.” 大鸟提醒道. “刚才还为项目设计不好而犯愁叫失业, 现在可好, 得意地恨不得全国挨家挨户用你那套软件, 瞧你那副德行.”

~感谢捧场,您的支持将鼓励我继续创作~