大话设计模式之简单工厂模式

想起之前学 Android 开发和 Java 开发的时候发现框架的出现能大大提高开发效率。如今学习 Web 前端开发的时候发现前端框架更加丰富。那研究各式各样的框架就成为了挺重要的学习部分,我一位学计算机的高中同学介绍说“框架是设计模式的一次实战应用”。所以这又扯到了设计模式,那什么是设计模式,后面我们将慢慢介绍各种各样有趣的设计模式


大话设计模式人物及背景

小菜:准码农一枚,正求职找工作

大鸟:小菜表哥,大学毕业后长期从事软件开发和管理工作,与小菜同住一起

什么是设计模式

码农翻身作者刘欣曾在他的公众号推送给设计模式说句公道话说过

设计模式是什么东西? 简单来讲就是久经考验的、在面向对象设计领域的成功经验的总结

因为自从上世纪70年代高级语言发展到面向对象阶段,这中间接近六十年的时间你可以想象有多少应用会被开发出来,这中间开发人员得积累了多少的经验教训。而设计模式就是这半个多世纪那些开发大牛对血与泪教训的经验总结。我同时认为,这些经验总结不单单只是程序开发经验的总结,而是取自于现实社会生活,但同时也能反哺现实生活的一种经验性知识。正因为是取自于现实社会生活,才有了如工厂模式、观察者模式、中介者模式、代理模式等如此生活化的命名方式。而 OO 语言的指导思想就是希望能够以现实人类求解日常问题的思维方法去求解软件开发的问题,那么开发出来的软件不仅容易被人理解,而且易于维护和修改,从而会保证软件的可靠性和可维护性,并能提高公共问题域中的软件模块和模块重用的可靠性。所以学习并学会合理使用设计模式就显得尤其重要了!好,下面开讲第一课!

面试受挫

小菜今年计算机专业大四了,学了不少软件开发的知识,对自己的技术能力也信心满满。这不,他自信地在各大招聘网站投下自己的简历,希望能谋得一份不错的开发工作。投递结果也如小菜预计的一样,各大公司、单位陆陆续续向他发送了面试通知。到了人家单位,前台小姐姐给了他一份题目,上面写着:“请用 C++、Java、C# 或 VB.NET 任意一种面向对象语言实现一个计算器控制台程序,要求输入两个数和运算符号,得到结果。”小菜心想:“这还不简单!”,三下五除二。不到一会儿就写完了,觉得也没错误,就交了卷。交卷时前台小姐姐面带微笑地说道“请回去等待进行下一轮面试的通知吧”。于是小菜只得耐心等待,可是半个月过去了,什么消息也没有,小菜很苦闷,便向他已经从事软件开发工作七年的表哥大鸟讨论可能的原因。大鸟问了题目和了解了小菜代码的细节以后,撇起嘴角,说道“小菜呀,你是真不明白吗?人家单位出题的意思,你完全都没明白,当然不会再联系你了。”这时小菜更加郁闷了,觉得自己代码应该能完美运行,越想越想不明白的小菜郁闷的看着大鸟。大鸟苦笑的指着他写下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Program{
static void Main(string[] args){
Console.Write("请输入数字A:");
string A = Console.ReadLine();
Console.Write("请选择运算符号(+、-、*、、):");
string B = Console.ReadLine();
Console.Write("请输入数字B:");
string C = Console.ReadLine();
string D = "";

if (B == "+")
D = Convert.ToString(Convert.ToDouble(A) + Convert.ToDouble(C));
if (B == "-")
D = Convert.ToString(Convert.ToDouble(A) - Convert.ToDouble(C));
if (B == "*")
D = Convert.ToString(Convert.ToDouble(A) * Convert.ToDouble(C));
if (B == "/")
D = Convert.ToString(Convert.ToDouble(A) / Convert.ToDouble(C));

Console.WriteLine("结果是:" + D);
}
}

大鸟一一的揪出了代码中不如意的地方。比如命名不规范、没有对用户输入情况做判断、判断分支的写法效率不够高。“哦,说的没错,这个我以前听老师说过,可是从来没有在意过,我马上改,改完你再看看”小菜说道。

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
class Program{
static void Main(string[] args){
try{
Console.Write("请输入数字A:");
string strNumA = Console.ReadLine();
Console.Write("请选择运算符号(+、-、*、、):");
string strOperation = Console.ReadLine();
Console.Write("请输入数字B:");
string strNumB = Console.ReadLine();
string strResult = "";

switch (strOperation){
case "+":
strResult = Convert.ToString(Convert.ToDouble(A) + Convert.ToDouble(C));
break;
case "-":
strResult = Convert.ToString(Convert.ToDouble(A) - Convert.ToDouble(C));
break;
case "*":
strResult = Convert.ToString(Convert.ToDouble(A) * Convert.ToDouble(C));
break;
case "/":
if (strNumB != "0"){
strResult = Convert.ToString(Convert.ToDouble(A) / Convert.ToDouble(C));
}else{
strResult = "除数不能为0";
}
break;
}
Console.WriteLine("结果是:" + strResult);
Console.ReadLine();
}catch(Exception ex){
Console.WriteLine("您的输入有错:" + ex.Message);
}
}
}

大鸟接过小菜写下的代码,摇摇头着说道“目前这代码实现计算器没有问题,但这样写出的代码是否符合出题人的意思呢?”

面向对象

活字印刷,面向对象

大鸟沉寂了一会,突然拍着小菜的肩膀说“你还记得我们古代四大发明之一的活字印刷术吗?”小菜菜点点头。“那你还知道活字印刷术发明之前民间常用的雕版印刷术吗?”这是小菜摇摇头。大鸟说道“雕版印刷术就是印书的时候,先用一把刷子蘸了墨,在雕好的板上刷一下,接着,用白纸覆在板上,另外拿一把干净的刷子在纸背上轻轻刷一下,把纸拿下来,这样一页书就印好了。那如果万一这时发现文章写的不对,需要改,那就只能为了一处的错误而去重新雕刻整块印刷板。而活字印刷术的出现就刚刚好解决了这个问题,只需把文章中的字体一个个分别雕刻出来,等后面印刷成页的时候进行排版就行了,如果需要更改,就只需要在排版的时候更改顺序。同时雕刻的单个字体还可以重复使用,还可以根据情况对文章增删内容。这样岂不是比之前方便多了!”

活字印刷术

雕版印刷术

大鸟接着说道“开发程序也应该如此——可维护、可复用、可扩展。而在活字印刷术出现之前,要修改,必须重刻,要加字,必须重刻,要重新排列,必须重刻,印完一页后,此版已无任何可再利用价值。其实雕版印刷术的问题就在于所有的字都刻在同一版面上,按计算机专业的概念来理解,就是耦合度太高。而利用封装、继承、多态把程序的耦合度降低,将使程序更加灵活,容易修改,并且易于复用。”

业务的封装

大鸟:“讲了那么多,你觉得之前那道题目你有什么想法吗?”

小菜:“我好像懂了一点,你刚才说的面向对象目的就是降低耦合度。刚才我写的代码的确还是有不合理的地方。比如代码逻辑中,既有计算逻辑也有控制台显示逻辑。这样就不利于代码的复用,维护,扩展。“

大鸟:“不错,那你试试按你的刚才的想法去修改一下的你的代码”

小菜:“那我试试!”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//Operation 运算类
public class Operation{
public static double GetResult(double numberA,double numberB,string operation){
double result = 0d;
switch (operation){
case "+":
result = numberA + numberB;
break;
case "-":
result = numberA - numberB;
break;
case "*":
result = numberA * numberB;
break;
case "/":
result = numberA / numberB;
break;
}
return result;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//控制台显示类
static void Main(string[] args){
try{
Console.Write("请输入数字A:");
string strNumA = Console.ReadLine();
Console.Write("请选择运算符号(+、-、*、、):");
string strOperation = Console.ReadLine();
Console.Write("请输入数字B:");
string strNumB = Console.ReadLine();
string strResult = "";
strResult = Convert.ToString(Operation.GetResult(Convert.ToDouble(strNumA),
Convert.ToDouble(strNumB),strOperation));
Console.WriteLine("结果是:"+strResult);
Console.ReadLine();
}catch(Exception ex){
Console.WriteLine("您的输入有错:"+ex.Message);
}
}

小菜:“写好了,你看看!”

大鸟:“不错,这样就完全把业务和界面分离了。你现在在其他平台上写计算器的应用,就可以复用这个运算类了。但你现在只用了面向对象三大特性中的一个,还有两个没用呢?”

小菜:“面向对象三大特性不就是封装、继承和多态吗,这里我用到的应该是封装。我觉得一个个小小的计算器程序不至于用到继承和多态吧。”

大鸟:“你看来 too young too naive!”

继承和多态

第二天。

小菜问道:“你说计算器这样的小程序还可以用到面向对象三大特性?继承和多态怎么可能用得上,我实在理解不了。我已经把业务和界面分离了,这不是很灵活了吗?”

大鸟:”那我问你,现在如果希望增加一个开根(sqrt)运算,你如何改?“

小菜:”那只需要改 Operation 运算类就行了,在 switch 中加一个分支就行了。”

大鸟:“那万一你不小心改错了呢。这么说吧,如果公司把某个算法类交个你去做修改,结果一不小心改错了,结果还可能影响到了其他原有的代码。这样本来只是让你增加一个功能,却使得原有的运行良好的功能代码产生了变化,这个风险太大了 。你明白吗?“

小菜:“哦,你的意思是我应该把加减乘除运算分离,修改其中一个不影响另外的几个,增加运算算法也不影响其他代码。”

大鸟:“对,如何用继承和多态,你应该有感觉了。”

小菜:“OK,我马上去写。”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Operation 运算类
public class Operation{
private double _numberA = 0;
private double _numberB = 0;

public double NumberA{
get {return _numberA;}
set {_numberA = value;}
}
public double NumberB{
get {return _numberB;}
set {_numberB = value;}
}
public virtual double GetResult(){
double result = 0;
return result;
}

加减乘除类

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
class Add : Operation{
public override double GetResult(){
double result = 0;
result = NumberA + NumberB;
return result;
}
}

class Sub : Operation{
public override double GetResult(){
double result = 0;
result = NumberA - NumberB;
return result;
}
}

class Multi : Operation{
public override double GetResult(){
double result = 0;
result = NumberA * NumberB;
return result;
}
}

class Div : Operation{
public override double GetResult(){
double result = 0;
if (NumberB==0){
throw new Exception("除数不能为0");
}
result = NumberA / NumberB;
return result;
}
}

大鸟:“写的不错,但是问题来了你如何让计算器知道我是希望用哪一个算法呢?”

小菜:“是哦,我还没考虑到。”

简单工厂模式

大鸟:“你现在的问题其实就是如何根据用户去实例化运算类的问题。解决这个问题可以用’简单工厂模式‘,也就是说到底要实例化谁,将来会不会增加实例化的对象,比如上面提到的开根运算,这是很容易变化的地方,应该考虑用一个单独的类来做这个创造运算实例的过程,这就像我们生活当中会的工厂,共同点都是生产某一样特定的东西。那么这个类就叫做工厂类!”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//简单运算工厂类
public class OperationFactory{
public static Operation createOperation(string operation){
Operation oper = null;
switch (operation){
case "+":
oper = new Add();
break;
case "-":
oper = new Sub();
break;
case "*":
oper = new Multi();
break;
case "/":
oper = new Div();
break;
}
return oper;
}
}

大鸟:“看看代码,这样的话你只需要输入运算符号,工厂实例化出合适的对象,通过多态,返回父类的方式实现了计算器的结果。”

1
2
3
4
5
6
//实现加法运算过程
Operation oper;
oper = OperationFactory.createOperation("+");
oper.NumberA = 1;
oper.NumberB = 2;
double result = oper.GetResult();

大鸟:“现在就是比较完整的计算器程序了。复用性,可维护性,可扩展性都不错。如果某一天需要更改加法运算,我们只需要更改 Add 类。增加其他各种运算,比如取对数,正弦余弦,就只要增加相对应的运算类即可。同时还要记得去工厂做一下”备注“-在工厂类 switch 增加对应的分支。是不是很不错啊。“

小菜:”厉害了我的哥,我服。看来我还是 too young too naive!“

大鸟:“就你嘴能,来我们最后看看这几个类的结构图总结一下。”

简单工厂模式 计算器

本文作者:刘志宇

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!

Donate comment here