今天我要在这里介绍的是C#设计模式中的模版方法模式。
问题:现在有一家汽车生产商需要一个管理汽车生产的管理软件。其中在该系统中有一个管理组装汽车的子模块。该模块要能根据不同的汽车类型来组装相应的汽车。汽车的被组装部分分别是汽车的车盖,车地盘,后备箱,车胎。现在来试着实现下该过程。
根据上面的描述,我在这里选择里跑车和商务车来进行演示。跑车的车盖,车地盘,后备箱,车胎均不是标准的,而商务车的车盖,车地盘不是标准的。我将不同类型的汽车分别封装到不同的类中,将汽车组装过程交给汽车类进行管理。
我在这里展示我的不假思索的解决方案:
类图如下:
实现代码:
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
|
跑车类代码:
/// <summary>
/// 跑车
/// </summary>
public
class
SportsCar
{
/// <summary>
/// 组装汽车
/// </summary>
/// <param name="pTextContainer">显示组装信息的文本容器</param>
public
void
AssembleCar(
string
pTextContainer)
{
pTextContainer =
"开始组装汽车:\n"
;
pTextContainer +=
"\t 第一步"
+ Step1() +
"\n"
;
pTextContainer +=
"\t 第二步"
+ Step2() +
"\n"
;
pTextContainer +=
"\t 第三步"
+ Step3() +
"\n"
;
pTextContainer +=
"\t 第四步"
+ Step4() +
"\n"
;
}
/// <summary>
/// 组装步骤一
/// </summary>
/// <returns>组装步骤一信息</returns>
public
string
Step1()
{
return
"跑车地盘"
;
}
/// <summary>
/// 组装步骤二
/// </summary>
/// <returns>组装步骤二信息</returns>
public
string
Step2()
{
return
"敞篷车盖"
;
}
/// <summary>
/// 组装步骤三
/// </summary>
/// <returns>组装步骤三信息</returns>
public
string
Step3()
{
return
"敞篷跑车后备箱"
;
}
/// <summary>
/// 组装步骤四
/// </summary>
/// <returns>组装步骤四信息</returns>
public
string
Step4()
{
return
"跑车车胎"
;
}
}
商务车类代码:
/// <summary>
/// 跑车
/// </summary>
public
class
OfficeCar
{
/// <summary>
/// 组装汽车
/// </summary>
/// <param name="pTextContainer">显示组装信息的文本容器</param>
public
void
AssembleCar(
string
pTextContainer)
{
pTextContainer =
"开始组装汽车:\n"
;
pTextContainer +=
"\t 第一步"
+ Step1() +
"\n"
;
pTextContainer +=
"\t 第二步"
+ Step2() +
"\n"
;
pTextContainer +=
"\t 第三步"
+ Step3() +
"\n"
;
pTextContainer +=
"\t 第四步"
+ Step4() +
"\n"
;
}
/// <summary>
/// 组装步骤一
/// </summary>
/// <returns>组装步骤一信息</returns>
public
string
Step1()
{
return
"商务车地盘"
;
}
/// <summary>
/// 组装步骤二
/// </summary>
/// <returns>组装步骤二信息</returns>
public
string
Step2()
{
return
"商务车车盖"
;
}
/// <summary>
/// 组装步骤三
/// </summary>
/// <returns>组装步骤三信息</returns>
public
string
Step3()
{
return
"标准后备箱"
;
}
/// <summary>
/// 组装步骤四
/// </summary>
/// <returns>组装步骤四信息</returns>
public
string
Step4()
{
return
"标准车胎"
;
}
}
|
现在就让我们开探讨下这种方案中的缺陷。
先让我们比较下 类SportsCar和 类OfficeCar这两个类。从中我们可以看出这两个类的功能逻辑一样:
1. 方法AssembleCar()的逻辑过程一样;
2. 只是方法Step1(),Step2(),Step3(),Step4()中的逻辑不同。
这样分别实现Sports Car 和 Office Car就产生不必要的代码,同时程序员也多付出了不必要的劳动。在代码量大的情况下,这是很累人的,很不给力。
下面是我用模版方法模式来解决类继承带来的缺陷。
先让我们了解下模版方法模式的用意:
准备一个抽象类,将部分逻辑以具体方法以及具体构造子的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些 抽象方法,从而对剩余的逻辑有不同的实现。这就是模版方法模式的用意。(参考《C#设计模式》)
模版方法模式的结构图:
从上面的文字叙述和类图可以看出,在模版方法模式中,模版类提供功能逻辑(在方法TemplateMethod实现功能,在一系列方法PrimitiveMethod实现功能的具体细节),该功能逻辑可以根据需要被重写进行相应的调整,但是不能破坏,否则这就失去了模版方法模式的意义,变成了单纯的类继承。不过在一般情况,模板类的派生类都只重写全部或者一部分PrimitiveMethod,而不会重写模版方法TemplateMethod。
注:模版进程经常是被处理成抽象类,方法PrimitiveMethod在模版类中声明称抽象的,然后交给派生类进行实现。具体如何处理还得看实际情况。不过模版类的逻辑结构不能被破坏。
下面,我让我们来试试模版方法模式。
运用模版方法模式后的类图:
好的,现在让我们来看看模版方法模式是否解决了之前不假思索的不足。
从上面的类图中我们可以看出,模板提供类基本汽车的组装过程,派生子类_SportsCar和_OfficeCar 只是从父类中重写他们需要的成员。这就避免了不必要的冗余代码。
![](https://i-blog.csdnimg.cn/blog_migrate/81178cc93a2a3bb5048d90d76e7ec935.gif)
1 相关代码:
2 汽车模版:
3 /// <summary>
4 /// 汽车模版
5 /// </summary>
6 public abstract class _TemplateCar
7 {
8 /// <summary>
9 /// 组装汽车
10 /// </summary>
11 /// <param name="pTextContainer"> 显示组装信息的文本容器 </param>
12 public void AssembleCar( string pTextContainer)
13 {
14 pTextContainer = " 开始组装汽车:\n " ;
15 pTextContainer += " \t 第一步 " + Step1() + " \n " ;
16 pTextContainer += " \t 第二步 " + Step2() + " \n " ;
17 pTextContainer += " \t 第三步 " + Step3() + " \n " ;
18 pTextContainer += " \t 第四步 " + Step4() + " \n " ;
19 }
20
21 /// <summary>
22 /// 组装步骤一
23 /// </summary>
24 /// <returns> 组装步骤一信息 </returns>
25 public virtual string Step1()
26 {
27 return " 标准车地盘 " ;
28 }
29
30 /// <summary>
31 /// 组装步骤二
32 /// </summary>
33 /// <returns> 组装步骤二信息 </returns>
34 public virtual string Step2()
35 {
36 return " 标准车盖 " ;
37 }
38
39 /// <summary>
40 /// 组装步骤三
41 /// </summary>
42 /// <returns> 组装步骤三信息 </returns>
43 public virtual string Step3()
44 {
45 return " 标准后备箱 " ;
46 }
47
48 /// <summary>
49 /// 组装步骤四
50 /// </summary>
51 /// <returns> 组装步骤四信息 </returns>
52 public virtual string Step4()
53 {
54 return " 标准车胎 " ;
55 }
56 }
57 跑车子类:
58 public class _SportsCar : _TemplateCar
59 {
60 /// <summary>
61 /// 重写组装步骤一
62 /// </summary>
63 /// <returns> 重写后的组装步骤一信息 </returns>
64 public override string Step1()
65 {
66 return " 跑车地盘 " ;
67 }
68
69 /// <summary>
70 /// 重写组装步骤二
71 /// </summary>
72 /// <returns> 重写后的组装步骤二信息 </returns>
73 public override string Step2()
74 {
75 return " 敞篷车盖 " ;
76 }
77
78 /// <summary>
79 /// 重写组装步骤三
80 /// </summary>
81 /// <returns> 重写后的组装步骤三信息 </returns>
82 public override string Step3()
83 {
84 return " 敞篷跑车后备箱 " ;
85 }
86
87 /// <summary>
88 /// 重写组装步骤四
89 /// </summary>
90 /// <returns> 重写后的组装步骤四信息 </returns>
91 public override string Step4()
92 {
93 return " 跑车车胎 " ;
94 }
95 }
96
97 商务车子类:
98 public class _OfficeCar : _TemplateCar
99 {
100 /// <summary>
101 /// 重写组装步骤一
102 /// </summary>
103 /// <returns> 重写后的组装步骤一信息 </returns>
104 public override string Step1()
105 {
106 return " 商务车地盘 " ;
107 }
108
109 /// <summary>
110 /// 重写组装步骤二
111 /// </summary>
112 /// <returns> 重写后的组装步骤二信息 </returns>
113 public override string Step2()
114 {
115 return " 商务车车盖 " ;
116 }
117 }
118
由于模板模式主要体现的是利用类的继承特性来进行代码重构,因此在这里将《C# 设计模式》中提到的重构的原则在下面列出。我个人认为这些原则对学习者的帮助挺大的。
下面是《C# 设计模式》一书中提到的重构原则:
在对一个继承的等级结构做重构时,一个应当遵从的原则便是将行为尽量移动到结构的高端,而将状态尽量移动到结构的低端。
1995年,Auer曾在文献【AUER95】中指出:
- 应当根据行为而不是状态定义一个类。也就是说,一个类的实现首先建立在行为的基础之上,而不是建立在状态的基础之上。
- 在实现行为时,是用抽象状态而不是用具体状态。如果一个行为涉及到对象的状态时,使用间接的引用而不是直接的引用。换言之,应当使用取值方法而不是直接引用属性。
- 给操作划分层次。一个类的行为应当放到一个小组核心方法(Kernel Methods)里面,这些方法可以很方便地在子类中加以置换。
- 将状态属性的确认推迟到子类中。不要在抽象类中过早地声明属性变量,应将它们尽量地推迟到子类中去声明。在抽象超类中,如果需要状态属性的话,可以调用抽象的取值方法,而将抽象的取值方法的实现放到具体子类中。
如果能够遵从这样的原则,那么就可以在等级结构中将接口与实现分隔开来,将抽象与具体分割开来,从而保证代码可以最大限度地被复用。这个过程实际上是将设计师引导到模版方法模式上去。
参考资料:《C# 设计模式》 。