故事引入
小菜今年计算机专业大四毕业,在找工作面试的时候,遇到一道题目👇
- “请用C++、JAV、C# 或 VB.NET 任意一种面向对象语言实现一个计算机控制台程序,要求输入两个数和运算符,得到结果”
小菜做完题目交卷后,石沉大海,小菜实现计算器的代码:
代码问题
- 代码规范问题:明明不规范、三次无效判断、没有考虑特殊情况
- 只是简单实现计算器功能,没有面向对象设计,维护和二次开发都不方便,要实现容易维护,容易扩展,又容易复用的代码!
栗子说明问题
- 三国时期,曹操诗性大发“喝酒唱歌,人生真爽”,众文臣齐呼“丞相好诗!”于是一个臣子立马命印刷工匠刻板印刷这首诗!
- 样张出来后给曹操一看,觉得不妥,说把“喝”与“唱”太俗气,应改成“对酒当歌”较好!于是这个臣子又工匠重新印刷,工匠眼看连夜刻板之工,彻底白费,心中叫苦,但只能照办出第二版: -样张再次出来后请曹操过目,曹操细细一品觉得还是不够好,说“人生真爽过于直接,应改成问语才够意境,因为这里应该改成 “对酒当歌,人生几何?” …”当臣子转告工匠时,工匠晕倒!!
- 其中原因:三国时期,活字印刷术还未发明,改字的时候要整个刻板全部重新雕刻!
- 解决办法:有活字印刷后,只需要改四个字即可,不会白费其他工作,岂不妙哉!!
- 要修改时只需要修改更改的字,此为可维护
- 这些字做出来之后并不是没用了,完全可以在后来的印刷中重复使用,此乃可复用
- 若要加字,只需要另刻新字加入即可,这是可扩展
- 字的排序其实可能是竖排或者横排,此时只需要将活字移动就可以满足需求,这叫灵活性好
- 在活字印刷术出来之前,这四种特性都无法满足,修改、加字、排序、必须重刻,印完一本书后,整个版子就没有价值
面向对象的好处
客户要求改需求时,因为我们的程序不宜维护,灵活性差,不容易扩展,更谈不上复用,因此面对需求分析,只能加班处理!一般都要对程序大作修改!但如果用面向对象的分析设计编程思想,考虑通过封装、继承、多态把程序的耦合度降低,传统印刷术的问题就在于所有字都可在同一板面上造成耦合度太高导致,用设计模式使程序更加灵活、容易修改、并且易于复用!
设计原则
1. 复用代码封装业务
用尽可能的办法去避免重复,业务逻辑与界面逻辑分开,降低耦合度,分离才能更容易维护扩展
这样的代码如果要添加一个新的运算,需要修改Operation类,在switch新+一个分支!这样需要让加减乘除都参与编译,可能会修改错别的代码!
举个栗子
公司财务要求做运维软件,目前有技术人员(月薪),销售人员(底薪+提成),经理(年薪+股份)三种算法,现在要求加入兼职人员(时薪)算法,按照上面的算法,要修改全部计算工资算法的代码,如果除了增加一个类算法还在别处做了修改,每一处都会让公司财务发生不必要的麻烦!!整个风险太大!
正确操作要把各个运算分离,修改新增删除一个不影响其他类型!
3. 松耦合
Operation
运算类
运算类有两个Number变量,用于计算输入,GetResult()在各个具体类重写的方法,这样修改任何一个算法,都会返回的是其运算的值,也不会影响到其他算法代码!但是需要告诉计算器使用的是哪一个算法!
简单工厂模式
实例化对象: 用一个单独的类来做创造实例的过程
简单运算工厂类
OperationFactory.cs
:
class OperationFactory
{
public static Operation CreateOperate(string operate)
{
Operation operation = null;
switch (operate)
{
case "+":
operation = new OperationAdd();
break;
case "-":
operation = new OperationSub();
break;
case "*":
operation = new OperationMul();
break;
case "/":
operation = new OperationDiv();
break;
}
return operation;
}
}
运算基类
Operation.cs
:
class Operation
{
private double num1 = 0;
private double num2 = 0;
public double Num1 { get => num1; set => num1 = value; }
public double Num2 { get => num2; set => num2 = value; }
public virtual double GetResult()
{
double result = 0;
return result;
}
}
加法类
OperationAdd.cs
:
class OperationAdd : Operation
{
public override double GetResult()
{
double result = 0;
result = Num1 + Num2;
Console.Write("OperationAdd [简单工厂模式加法运算]:" + Num1 + " + " + Num2 + " = " + result + "\n");
return result;
}
}
减法类
OperationSub .cs
:
class OperationSub : Operation
{
public override double GetResult()
{
double result = 0;
result = Num1 + Num2;
Console.Write("OperationSub [简单工厂模式减法运算]:" + Num1 + " + " + Num2 + " = " + result + "\n");
return result;
}
}
除法类
OperationDiv .cs
class OperationDiv : Operation
{
public override double GetResult()
{
if (Num2 == 0)
{
throw new ArgumentException();
}
double result = Num1 / Num2;
Console.Write("OperationDiv [简单工厂模式除法运算]:" + Num1 + " / " + Num2 + " = " + result + "\n");
return result;
}
}
乘法类
OperationMul .cs
class OperationMul : Operation
{
public override double GetResult()
{
double result;
result = Num1 * Num2;
Console.Write("OperationMul [简单工厂模式乘法运算]:" + Num1 + " × " + Num2 + " = " + result + "\n");
return base.GetResult();
}
}
客户端主程序
Program.cs
class Program
{
static void Main(string[] args)
{
int num1 = 10;
int num2 = 20;
Operation addOperation = OperationFactory.CreateOperate("+");
addOperation.Num1 = num1;
addOperation.Num2 = num2;
double addResult = addOperation.GetResult();
Console.Write("OperationFactory 简单工厂加法 \n\n");
// 简单工厂 -
Operation subOperation = OperationFactory.CreateOperate("-");
subOperation.Num1 = num1;
subOperation.Num2 = num2;
double subResult = subOperation.GetResult();
Console.Write("OperationFactory 简单工厂减法 \n\n");
// 简单工厂 *
Operation mulOperation = OperationFactory.CreateOperate("*");
mulOperation.Num1 = num1;
mulOperation.Num2 = num2;
double mulResult = mulOperation.GetResult();
Console.Write("OperationFactory 简单工厂乘法 \n\n");
// 简单工厂 *
Operation divOperation = OperationFactory.CreateOperate("/");
divOperation.Num1 = num1;
divOperation.Num2 = num2;
double divResult = divOperation.GetResult();
Console.Write("OperationFactory 简单工厂除法 \n\n");
Console.ReadKey();
}
}
运行结果:
OperationAdd [简单工厂模式加法运算]:10 + 20 = 30
OperationFactory 简单工厂加法
OperationSub [简单工厂模式减法运算]:10 + 20 = 30
OperationFactory 简单工厂减法
OperationMul [简单工厂模式乘法运算]:10 × 20 = 200
OperationFactory 简单工厂乘法
OperationDiv [简单工厂模式除法运算]:10 / 20 = 0.5
OperationFactory 简单工厂除法
不规范的类图:
- 要增加新运算比如开方,直接添加一个开方类,并且修改工厂类即可,若要修改加法运算,直接修改加法类中代码,不会影响其他运算!
工厂方法模式
- 简单工厂添加新类时需要修改工厂里面的
分支条件!修改原有的类说明不旦开放扩展,对修改也开放了,这样违背了开放-封闭原则Case
工厂方法模式(Factory Method),定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
把工厂类抽象出一个接口,这个接口只有一个方法,就是创建抽象产品的工厂方法,然后所有的要生产具体类的工厂就去实现这个接口,这样一个简单工厂模式的工厂类变成一个工厂抽象接口和多个具体生成对象的工厂!需要增加新的运算功能时,不需要改变原有的工厂类,只需要增加此功能的运算类和相应的工厂类就行!
- 这样一来整个工厂和产品体系其实没有发生什么修改,只是扩展的变化,这就完全符合开放-封闭的原则!!
- 工厂方法模式实现时,客户端需要决定实例化哪一个工厂来实现运算类,选择判断的问题还是存在的,也就是说,工厂方法把简单工厂的内部逻辑判断移到客户端代码来进行,如果说想要增加功能,本来是要改工厂类,现在变成在修改客户端!
工厂方法模式应用
- 栗子:薛磊风是一个大学生,以学习雷锋的名义去帮助老人做事,这里可以设计成一个了雷锋类,拥有扫地、洗米、买米等方法。
LeiFeng.cs
// 雷锋类
class LeiFeng
{
public void Sweep()
{
Console.WriteLine("扫地");
}
public void Wash()
{
Console.WriteLine("洗衣");
}
public void BuyRice()
{
Console.WriteLine("买米");
}
}
‘学习雷锋的大学生’类要继承‘雷锋’
Undergraduate.cs
public Undergraduate()
{
Name = "学习雷锋的大学生";
}
客户端实现代码,大学生做雷锋:
LeiFeng student = new Undergraduate();
student.BuyRice();
student.Wash();
student.Sweep();
如果说有三个大学生要去做事,就要实例化三个大学生,于是👇
由此可得创建一个社区志愿者类
Volunteer .cs
class Volunteer : LeiFeng
{
public Volunteer()
{
Name = "学习雷锋的社区志愿者";
}
}
简单学雷锋工厂
SimpleFactory.cs
:
class SimpleFactory
{
public static LeiFeng CreateLeiFeng(string type)
{
LeiFeng result = null;
switch (type)
{
case "大学生":
result = new Undergraduate();
break;
case "志愿者":
result = new Volunteer();
break;
}
return result;
}
}
客户端代码
Program.cs
:
LeiFeng studentA = SimpleFactory.CreateLeiFeng("大学生");
studentA.BuyRice();
LeiFeng studentB = SimpleFactory.CreateLeiFeng("大学生");
studentB.Sweep();
LeiFeng studentC = SimpleFactory.CreateLeiFeng("大学生");
studentC.Wash();
在实例化时都需要写出这个工厂的代码,这是重复的地方!!
// 雷锋工厂
interface IFactory
{
LeiFeng CreateLeiFeng();
}
// 学习雷锋的大学生工厂
class UndergraduateFactory : IFactory
{
public LeiFeng CreateLeiFeng()
{
//throw new NotImplementedException();
return new Undergraduate();
}
}
// 志愿者工厂
class VolunteerFactory : IFactory
{
LeiFeng IFactory.CreateLeiFeng()
{
//throw new NotImplementedException();
return new Volunteer();
}
}
客户端写法
// 工厂方法
IFactory studentFactory = new UndergraduateFactory();
LeiFeng student = studentFactory.CreateLeiFeng();
student.BuyRice();
student.Sweep();
student.Wash();
IFactory volunteerFactory = new VolunteerFactory();
LeiFeng volunteer = volunteerFactory.CreateLeiFeng();
volunteer.BuyRice();
volunteer.Sweep();
volunteer.Wash();
输出:
学习雷锋的大学生 买米
学习雷锋的大学生 扫地
学习雷锋的大学生 洗衣
学习雷锋的社区志愿者 买米
学习雷锋的社区志愿者 扫地
学习雷锋的社区志愿者 洗衣
工厂方法和简单工厂对比
- 工厂方法客服了简单工厂违背 “ 开放-封闭原则 ” 的缺点,又保持了封装对象创建过程的优点。
- 工厂方法模式是简单工厂模式的进一步抽象和推广,由于使用了多态性,工厂方法模式保持了简单工厂模式的优点并克服它的缺点。
- 缺点:由于每增加一个产品都需要增加一个产品工厂类,增加了额外的开发量
- 同:工厂模式都是集中封装了对象的创建,使要更换对象时,不需要做大的改动就可以实现,降低了客户程序与产品对象的耦合!
工厂模式
意图
主要解决接口选择的问题。
使用场景
我们明确知道不同条件下要创建不同实例时
如何实现
让工厂子类实现工厂接口,创建具体类型产品返回,创建过程在子类执行完成
应用实例
- 吃一个面包,不需要关心怎么做出来的,只要知道价格味道
- 买一辆车直接从店里提货,不用管车怎么制造的
- 使用API不需要关心底层创建逻辑,可以直接用这个类的方法
优点
- 一个调用者想要创建一个对象,只需要知道其名字就可以
- 扩展性高,如果想增加一个产品,扩展一个工厂类+一个产品类即可
- 屏蔽产品的具体实现,调用者只关心产品的接口
缺点
每次增加一个产品就需要增加一个具体类和对象具体实现类,导致系统中类的数量成倍增长,增加系统代码复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
应用场景
- 日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。
- 数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。
- 设计一个连接服务器的框架,需要三个协议,“POP3”、“IMAP”、“HTTP”,可以把这三个作为产品类,共同实现一个接口。
- 在任何需要生成复杂对象的地方都可以使用工厂方法模式,但是简单对象只需要new 出来的就不需要用工厂,如果用了就会引入工厂类,只会增加系统复杂度。
参考资料
《大话数据结构》
菜鸟教程 | 工厂模式