本系列的文章,来介绍编程中的设计模式,介绍的内容主要为《大话设计模式》的读书笔记,并改用C++语言来实现(书中使用的是.NET中的C#),本篇来学习第一章,介绍的设计模式是——简单工厂模式。
1 面向对象编程
设计模式依赖与面向对象编程密不可分,因此在开始学习设计模式之前,先简单介绍下面向对象编程。
先来看一个小故事:
话说三国时期,曹操在赤壁带领百万大军,眼看就要灭掉东吴,统一天下,非常高性,于是大宴文武。
在酒席间,不觉吟到:“喝酒唱歌,人生真爽,…”,众文武齐呼:“丞相好诗!”,
于是一臣子速速命令印刷工匠进行刻版印刷,以便流传天下。
印刷工匠刻好样张,拿出来给曹操一看,曹操感觉不妥,
说道:“喝与唱,此话过俗,应该改为对酒当歌较好!”,
于是臣子就命令工匠重新来过,工匠眼看连夜刻版之功,彻底白费,心中叫苦不迭,只得照办。
印刷工匠再次刻好样张,拿出来给曹操过目,曹操细细一品,觉得还是不好,
说:“人生真爽太过直接,应该改为问句才够意境,因此应改为对酒当歌,人生几何”,
当臣子再次转告工匠之时,工匠晕倒…
那,问题出在哪里呢?
大概是三国时期还没有活字印刷术吧,所以要改字的时候,就必须整个刻板全部重新雕刻。
如果有了活字印刷术,其实只需要更改四个字即可,其余工作都未白做。
我们联想编程,从这个小故事中,来体会一下编程中的一些思想:
- 可维护:要改字,只需更改需要变动的字即可
- 可复用:这些字并不是只是这次有用,后续如果在其它印刷中需要用,可重复使用
- 可扩展:如果诗中需要加字,只需另外单独刻字即可
- 灵活性:字的排列可以横排,也可以竖排
面向对象编程,通过封装、继承和多态,把程序的耦合度降低。
传统印刷术的问题就在于把所有字都刻在同一个版面上的耦合度太高。
使用设计模式可以使程序更加灵活,容易修改,并易于复用。
2 计算器实例
下面以一个计算器的代码实例,来体会封装的思想,以及简单工厂模式的使用。
题目:设计一个计算器控制台程序,输入为两个数和运算符,输出结果
功能比较简单,先来看第一个版本的实现。
2.1 版本一:面向过程
第一个版本采用面向过程的思想,从接收用户输入,到数据运算,以及最后的输出,都是按顺序在一个代码块中实现的:
int main()
{
float numA = 0;
float numB = 0;
float result = 0;
char operate;
bool bSuccess = true;
printf("please input a num A:\n");
scanf("%f", &numA);
printf("please input a operate(+ - * \\):\n");
std::cin >> operate;
printf("please input a num B:\n");
scanf("%f", &numB);
switch(operate)
{
case '+':
{
result = numA + numB;
break;
}
case '-':
{
result = numA - numB;
break;
}
case '*':
{
result = numA * numB;
break;
}
case '/':
{
if (numB == 0)
{
bSuccess = false;
printf("divisor cannot be 0!\n");
break;
}
result = numA / numB;
break;
}
default:
{
bSuccess = false;
break;
}
}
if (bSuccess)
{
printf("%f %c %f = %f\n", numA, operate, numB, result);
}
else
{
printf("[%f %c %f] calc fail!\n", numA, operate, numB);
}
return 0;
}
该程序的运行效果如下图所示:
上述代码实现本身没有什么问题,但是,如果现在要再实现一个带有UI界面的计算器,代码能不能复用呢?很显然不行,代码都是在一起的。
因此,为了便于代码复用,可以将计算部分的代码和显示部分的代码分开,降低它们之间的耦合度。
2.2 版本二:对业务封装
版本二则是对计算部分的业务代码和显示部分的控制台输入输出代码分开。
计算部分的业务代码,设计一个Operation运算类,通过其成员函数GetResult来实现加减乘除运算。
2.2.1 业务代码
class Operation
{
public:
bool GetResult(float numA, float numB, char operate, float &result)
{
bool bSuccess = true;
switch(operate)
{
case '+':
{
result = numA + numB;
break;
}
case '-':
{
result = numA - numB;
break;
}
case '*':
{
result = numA * numB;
break;
}
case '/':
{
if (numB == 0)
{
bSuccess = false;
printf("divisor cannot be 0!\n");
break;
}
result = numA / numB;
break;
}
default:
{
bSuccess = false;
break;
}
}
return bSuccess;
}
};
2.2.2 控制台界面代码
显示部分的控制台输入输出代码,还在main函数中。
int main()
{
float numA = 0;
float numB = 0;
float result = 0;
char operate;
printf("please input a num A:\n");
scanf("%f", &numA);
printf("please input a operate(+ - * \\):\n");
std::cin >> operate;
printf("please input a num B:\n");
scanf("%f", &numB);
Operation Op1;
bool bSuccess = Op1.GetResult(numA, numB, operate, result);
if (bSuccess)
{
printf("%f %c %f = %f\n", numA, operate, numB, result);
}
else
{
printf("[%f %c %f] calc fail!\n", numA, operate, numB);
}
return 0;
}
版本二的运行效果演示如下:
上述的版本二的代码实现,就用到了面向对象三大特性中的封装。
那,上述代码,是否可以做到灵活扩展?
比如,如果希望增加一个开根号的运算,如果改?
按照现有逻辑,需要修改Operation运算类,在switch中增加一个分支。但这样,会需要加减乘除的逻辑再次参与编译,另外,如果在修改开根号的代码时,不小心改动了加减乘除的逻辑,影响就大了。
因此,可以使用面向对象中继承和多态的思想,来实现各个运算类的分离。
2.3 版本三:简单工厂
版本三用到了封装、继承、多态,以及通过简单工厂来实例化出合适的对象。
2.3.1 Operation运算类(父类)
Operation运算类为一个抽象类,是加减乘除类的父类。
该类包含numA和numB两个成员变量,以及一个虚函数GetResult用于计算运算结果,各个子类中对其进行具体的实现。
// 操作类(父类)
class Operation
{
public:
float numA = 0;
float numB = 0;
public:
virtual float GetResult()
{
return 0;
};
};
2.3.2 加减乘除类(子类)
加减乘除子类通过公有继承Operation类,可以访问其共有成员变量numA和numB,并对GetResult方法进行具体的实现:
// 加法类(子类)
class OperationAdd : public Operation
{
public:
float GetResult()
{
return numA + numB;
}
};
// 减法类(子类)
class OperationSub : public Operation
{
public:
float GetResult()
{
return numA - numB;
}
};
// 乘法类(子类)
class OperationMul : public Operation
{
public:
float GetResult()
{
return numA * numB;
}
};
// 除法类(子类)
class OperationDiv : public Operation
{
public:
float GetResult()
{
if (numB == 0)
{
printf("divisor cannot be 0!\n");
return 0;
}
return numA / numB;
}
};
2.3.3 简单运算工厂类
为了能方便地实例化加减乘除类,考虑使用一个单独的类来做这个创造实例的过程,这个就是工厂。
设计一个OperationFactory类来实现,这样,只要输入运算的符号,就能实例化出合适的对象。
// 简单工厂模式
class OperationFactory
{
public:
Operation *createOperation(char operation)
{
Operation *oper = nullptr;
switch(operation)
{
case '+':
{
oper = (Operation *)(new OperationAdd());
break;
}
case '-':
{
oper = (Operation *)(new OperationSub());
break;
}
case '*':
{
oper = (Operation *)(new OperationMul());
break;
}
case '/':
{
oper = (Operation *)(new OperationDiv());
break;
}
default:
{
break;
}
}
return oper;
}
};
使用版本三,如果后续需要修改加法运算,只需要修改OperationAdd类中的内容即可,不会影响到其它计算类。
2.3.4 控制台界面代码
显示部分的控制台输入输出代码,还在main函数中。
通过多态,返回父类的方式,实现对应运算的计算结果。
{
float numA = 0;
float numB = 0;
float result = 0;
char operate;
printf("please input a num A:\n");
scanf("%f", &numA);
printf("please input a operate(+ - * \\):\n");
std::cin >> operate;
printf("please input a num B:\n");
scanf("%f", &numB);
OperationFactory opFac;
Operation *oper = nullptr;
oper = opFac.createOperation(operate);
if (oper != nullptr)
{
oper->numA = numA;
oper->numB = numB;
result = oper->GetResult();
printf("%f %c %f = %f\n", numA, operate, numB, result);
delete oper;
}
else
{
printf("[%f %c %f] calc fail!\n", numA, operate, numB);
}
return 0;
}
版本三的运行效果演示如下:
版本三中,各个类之间的关系如下图所示:
- 运算类是一个抽象类(类名用斜体表示),具有两个float类型的公有的(共有用**+号**)成员变量numA和numB以及一个GetResult公有方法
- 四个计算类继承(继承用空心三角+实线表示)运算类,并实现对应的GetResult方法
- 简单工厂类依赖于(依赖用箭头+虚线表示)运算类,通过createOperation方法实现运算类的实例化
3 总结
本篇主要介绍设计模式中的简单工厂模式,首先通过一个活字印刷的小故事来体会程序设计中的可维护、可复用、可扩展、灵活性的思想,并引入面向对象设计模式中的三大基本思想:封装、继承、多态,然后通过一个计算器的代码实现的例子,通过C++实现了三个版本的代码,由浅到深地理解面向对象的设计思想以及简单工厂模式的使用。