思考并回答以下问题:
- 学习设计模式从客户端代码开始怎么理解?
- 整体与部分可以被一致对待是什么意思?组合模式使得用户对单个对象和组合对象的使用具有一致性。怎么理解?
- 透明方式与安全方式的区别是什么?
- 当发现需求中是体现部分与整体层次的结构时,以及希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时,就应该考虑用组合模式了。怎么理解?
案例介绍
现在需要为一家在全国许多城市都有分销机构的大公司做办公管理系统,总部有人力资源、财务、运营等部门。
这家在北京有总部,在全国几大城市设有分公司,比如上海设有华东区分部,然后在一些省会城市还设有办事处,比如南京办事处、杭州办事处。此外,总公司的人力资源部、财务部等办公管理功能在所有的分公司或办事处都需要有。
因为简单复制是最糟糕的设计,所以共享功能到各个分公司,也就是让总部、分公司、办事处用同一套代码,只是根据ID的不同来区分。
但是总部、分部和办事处是成树状结构的,也就是有组织结构的,不可以简单的平行管理。这下就比较痛苦了,因为实际开发时就得一个一个的判断它是总部,还是分公司的财务,然后再执行其相应的方法。
类似的这种部分与整体情况很多见,例如卖电脑的商家,可以卖单独配件也可以卖组装整机,又如复制文件,可以一个一个文件复制粘贴还可以整个文件夹进行复制,再比如文本编辑,可以给单个字加粗、变色、改字体,当然也可以给整段文字做同样的操作。其本质都是同样的问题。
分公司或办事处与总公司的关系,就是部分与整体的关系。希望总公司的组织结构,比如人力资源部、财务部的管理功能可以复用于分公司。这其实就是整体与部分可以被一致对待的问题。
对于Word文档里的文字,对单个字的处理和对多个字、甚至整个文档的处理,其实是一样的,用户希望一致对待,程序开发者也希望一致处理。但具体怎么做呢?
首先,来分析一下这个项目,如果把北京总公司当做一棵大树的根部的话,它的下属分公司其实就是这棵树的分枝,至于各办事处是更小的分支,而它们的相关的职能部门由于没有分枝了,所以可以理解为树叶。
尽管天下没有两片相同的树叶,但同一棵树上长出来的树叶样子也不会相差到哪去。也就是说,所希望的总部的财务部管理功能也最好是能复用到子公司,那么最好的办法就是,在处理总公司的财务管理功能和处理子公司的财务管理功能的方法都是一样的。
组合模式
组合模式(Composite)的定义:1
将对象组合成树形结构以表示“部分——整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
组合模式结构图
Component为“组合中的对象”声明接口,在适当情况下,实现所有类共有接口的默认行为。声明一个接口用于访问和管理Component的子部件。1
2
3
4
5
6
7
8
9
10
11
12
13
14abstract class Component
{
protected string name;
public Component(string name)
{
this.name = name;
}
// 通常都用Add和Remove方法来提供增加或移除树叶或树枝的功能
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
23class Leaf : Component
{
public Leaf(string name) : base(name)
{}
// 由于叶子没有再增加分枝和树叶,所以Add和Remove方法实现它没有意义
// 但这样做可以消除叶节点和枝节点对象在抽象层次的区别,它们具有完全一致的接口
public override void Add(Component c)
{
Console.WriteLine("Cannot add to a leaf");
}
public override void Remove(Component c)
{
Console.WriteLine("Cannot remove from a leaf");
}
// 叶节点的具体方法,此处是显示其名称和级别
public override void Display(int depth)
{
Console.WriteLine(new String('-', depth) + 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
29class Composite : Component
{
// 一个子对象集合用来存储其下属的枝节点和叶节点
private List<Component> children = new List<Component>();
public Composite(string name) : base(name)
{}
public override void Add(Component c)
{
children.Add(c);
}
public override void Remove(Component c)
{
children.Remove(c);
}
public override void Display(int depth)
{
// 显示其枝节点名称,并对其下级进行遍历
Console.WriteLine(new String('-', depth) + name);
foreach(Component component in children)
{
component.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
25
26
27
28class Program
{
static void Main(string[] args)
{
Composite root = new Composite("root"); // 生成树根root,根上长出两叶LeafA和LeafB
root.Add(new Leaf("Leaf A"));
root.Add(new Leaf("Leaf B"));
Composite comp = new Composite("Composite X"); // 根上长出分枝CompositeX,分枝上也有两叶LeafXA和LeafXB
comp.Add(new Leaf("Leaf XA"));
comp.Add(new Leaf("Leaf XB"));
root.Add(comp);
Composite comp2 = new Composite("Composite XY"); // 在CompositeX再长出分枝CompositeXY,分枝上也有两叶LeafXYA和LeafXYB
comp2.Add(new Leaf("Leaf XYA"));
comp2.Add(new Leaf("Leaf XYB"));
comp.Add(comp2);
root.Add(new Leaf("Leaf C")); // 根部又长出两叶LeafC和LeafD,可惜LeafD没长牢,被风吹走了
Leaf leaf = new Leaf("Leaf D");
root.Add(leaf);
root.Remove(leaf);
root.Display(1); // 显示大树的样子
Console.Read();
}
}
结果显示1
2
3
4
5
6
7
8
9
10-root
---Leaf A
---Leaf B
---Composite X
-----Leaf XA
-----Leaf XB
-----Composite XY
-------Leaf XYA
-------Leaf XYB
---Leaf C
透明方式与安全方式
树可能有无数的分枝,但只需要反复用Composite就可以实现树状结构了。
为什么Leaf类当中也有Add和Remove,树叶不是不可以再长分枝吗?
这种方式叫做透明方式,也就是说在Component中声明所有用来管理子对象的方法,其中包括Add、Remove等。这样实现Component接口的所有子类都具备了Add和Remove。这样做的好处就是叶节点和枝节点对于外界没有区别,它们具备完全一致的行为接口。但问题也很明显,因为Leaf类本身不具备Add()、Remove()方法的功能,所以实现它是没有意义的。
如果Leaf类当中不用Add和Remove方法,那么就需要使用安全方式,也就是在Component接口中不去声明Add和Remove方法,那么子类的Leaf也就不需要去实现它,而是在Composite声明所有用来管理子类对象的方法,这样做就不会出现刚才提到的问题,不过由于不够透明,所以树叶和树枝类将不具有相同的接口,客户端的调用需要做相应的判断,带来了不便。两者各有好处,视情况而定。
何时使用组合模式
什么地方用组合模式比较好呢?
当发现需求中是体现部分与整体层次的结构时,以及希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时,就应该考虑用组合模式了。
公司管理系统
代码结构图
公司类 抽象类或接口1
2
3
4
5
6
7
8
9
10
11
12
13
14abstract 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
34
35
36
37class ConcreteCompany : Company
{
private List<Company> children = new List<Company>();
public ConcreteCompany(string name)
: base(name)
{ }
public override void Add(Company c)
{
children.Add(c);
}
public override void Remove(Company c)
{
children.Remove(c);
}
public override void Display(int depth)
{
Console.WriteLine(new String('-', depth) + name);
foreach(Company component in children)
{
component.Display(depth + 2);
}
}
// 履行职责
public override void LineOfDuty()
{
foreach(Company component in children)
{
component.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// 人力资源部
class HRDepartment : Company
{
public HRDepartment(string name) : base(name)
{ }
public override void Add(Company c)
{ }
public override void Remove(Company c)
{ }
public override void Display(int depth)
{
Console.WriteLine(new String('-', depth) + name);
}
public override void LineOfDuty()
{
Console.WriteLine("{0} 员工招聘培训管理", name);
}
}
// 财务部
class FinanceDepartment : Company
{
public FinanceDepartment(string name) : base(name)
{ }
public override void Add(Company c)
{ }
public override void Remove(Company c)
{ }
public override void Display(int depth)
{
Console.WriteLine(new String('-', depth) + name);
}
public override void LineOfDuty()
{
Console.WriteLine("{0} 公司财务收支管理", 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
27
28
29static 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("杭州办事处");
comp2.Add(new HRDepartment("杭州办事处人力资源部"));
comp2.Add(new FinanceDepartment("杭州办事处财务部"));
comp.Add(comp2);
Console.WriteLine("\n结构图:");
root.Display(1);
Console.WriteLine("\n职责:");
root.LineOfDuty();
Console.Read();
}
结果显示
结构图:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22-北京总公司
---总公司人力资源部
---总公司财务部
---上海华东分公司
-----华东分公司人力资源部
-----华东分公司财务部
-----南京办事处
-------南京办事处人力资源部
-------南京办事处财务部
-----杭州办事处
-------杭州办事处人力资源部
-------杭州办事处财务部
职责:
总公司人力资源部 员工招聘培训管理
总公司财务部 公司财务收支管理
华东分公司人力资源部 员工招聘培训管理
华东分公司财务部 公司财务收支管理
南京办事处人力资源部 员工招聘培训管理
南京办事处财务部 公司财务收支管理
杭州办事处人力资源部 员工招聘培训管理
杭州办事处财务部 公司财务收支管理
组合模式好处
组合模式这样就定义了包含人力资源部和财务部这些基本对象和分公司、办事处等组合对象的类层次结构。基本对象可以被组合成更复杂的组合对象,而这个组合对象又可以被组合,这样不断地递归下去,客户代码中,任何用到基本对象的地方都可以使用组合对象了。
用户是不用关心到底是处理一个叶节点还是处理一个组合组件,也就用不着为定义组合而写一些选择判断语句了。
简单点说,就是组合模式让客户可以一致地使用组合结构和单个对象。
这也就是说,那家公司开多少个以及多少级办事处都没问题了。