有些知识总是过段时间就会忘掉,怎么办呢?身为笨人的我只能采用笨办法,再学一遍。
前言
工厂模式总是有很多名字,什么简单工厂,工厂方法,静态工厂,抽象工厂等等。在学习工厂模式的时候我倒是想到了很多东西。比如面向对象里的抽象、多态。于是我想着能不能换个方式,从其他方面来谈一谈工厂模式,或者说谈一谈我学习工厂模式的时候产生的思考。
当然,我水平有限,也不知道理解的对不对,只能想到什么就说什么。
本篇博客相关代码放在该仓库里:https://gitee.com/siumu/blog_code.git
简单与复杂,不同与相同
一切事物都是从简单到复杂,慢慢发展而来的。就像学数学一样,刚开始学的正整数、自然数,然后是小数、负数、实数。到后来,数学已经不能单单用数字来表示了。比如我们学过的离散数学里有个概念就是命题。数学不断地升华,不断地抽象成更高一层。不断地从简单到复杂,从不同中寻找相同。
又比如,我们熟悉的编程从面向过程到面向对象,不断地寻找事物的相同部分,并抽象出来。
接下来就聊聊这个工厂模式,或许上文与工厂模式关系不是很大,但是我确实在学习中想到了它们,感觉我对这些概念比以前又多了一点点认识。
场景问题
我们来写一个简易的计算器吧,能进行加减乘除就可以。我写的很简单,就是在控制台输入数字与运算符,然后输出运算结果。如图所示:
没错,就是这么简单的一个计算器。如此简单的运算器,岂不是几行代码就搞定,还有什么可说的?接下来我们就说道说道。这个计算器的代码如下:
public static void main(String[] args) {
System.out.println("请输入第一个数字:");
Scanner input = new Scanner(System.in);
double firstNumber = input.nextDouble();
System.out.println("请输入运算符:");
String operator = input.next();
char oper = operator.charAt(0);
System.out.println("请输入第二个数字:");
double secondNumber = input.nextDouble();
input.close();
System.out.println("运算结果为:");
switch (oper){
case '+':
System.out.println(firstNumber + "+" + secondNumber+"="+(firstNumber+secondNumber));
break;
case '-':
System.out.println(firstNumber + "-" + secondNumber+"="+(firstNumber-secondNumber));
break;
case '*':
System.out.println(firstNumber + "*" + secondNumber+"="+(firstNumber*secondNumber));
break;
case '/':
if (secondNumber==0){
System.out.println("输入不合法");
break;
}
System.out.println(firstNumber + "/" + secondNumber+"="+(firstNumber/secondNumber));
break;
}
}
需求实现了,那我们是不是就要考虑维护与扩展的问题了,这个代码虽然简单,但是我们从宏观上考虑,我们所写的这个方法里有两个职责,输入参数与计算结果。
假如现在有个新需求,要求写一个窗体,在窗口里输入参数。那么我们普遍会怎么办呢?自然是重写,把输入参数的部分改写成一个窗体,然后把计算的那部分代码复制过去。
复制与复用
如此一来,代码就成了复制,而不是复用。怎么才能复用呢?自然是解耦,将计算业务分离出来,输入参数或者说界面部分可以变化,负责计算的业务代码不用改变。改造完之后的计算业务代码如下:
public class Operator {
public static double calculate(double firstNumber,double secondNumber,char oper){
double result = 0;
switch (oper){
case '+':
result = firstNumber+secondNumber;
break;
case '-':
result = firstNumber-secondNumber;
break;
case '*':
result = firstNumber*secondNumber;
break;
case '/':
if (secondNumber==0){
System.out.println("输入不合法");
break;
}
result = firstNumber/secondNumber;
break;
}
return result;
}
}
这样,计算业务分离了出来,不管输入参数那部分怎么变化,计算业务是不需要改变的,只用一行代码来调用即可:
//计算业务
double result = Operator.calculate(firstNumber, secondNumber, oper);
内聚与耦合
我们再来看这个计算业务,它真的不会发生变化吗?如果我想加一个新的计算,比如求余,开方,平方,阶乘等等。那这样一来,每次加一个运算,代码都要修改一遍。修改的风险代价很大,因为是人就会出错,把正确的代码改错的后果是灾难性的。比如把加法改成了减法之类的。
那么就需要一种办法,使得运算独立出来,每次增加新的运算时,不会对原有的运算代码产生修改风险,那么我们来将计算的部分进一步的抽象出来。让代码更加内聚,无关的代码解耦。
那么计算部分的共同特点就是,两个需要计算的数字,获得计算结果。代码如下:
public class Calculator {
//需要计算的数字
private double firstNumber;
private double secondNumber;
//计算结果
public String getResult(){
return "";
}
public double getFirstNumber() {
return firstNumber;
}
public void setFirstNumber(double firstNumber) {
this.firstNumber = firstNumber;
}
public double getSecondNumber() {
return secondNumber;
}
public void setSecondNumber(double secondNumber) {
this.secondNumber = secondNumber;
}
public Calculator(double firstNumber, double secondNumber) {
this.firstNumber = firstNumber;
this.secondNumber = secondNumber;
}
public Calculator() {
}
}
接下来,就是每个加减乘除类都要继承这个计算类,并重写计算结果方法。如加法类代码如下:
public class OperatorAdd extends Calculator{
@Override
public String getResult() {
return super.getFirstNumber()+super.getSecondNumber()+"";
}
}
如此一来,如果要增加新的方法只需要继承Operator类,重写计算方法即可,完全不用担心会将其他运算的代码不小心修改掉。
面向对象领域中有一个原则就是开闭原则,对修改关闭,对扩展开放,那么我们对计算业务的代码进一步抽象,在一定程度上符合了开闭原则。
然而,我们还需要一个类,用来创建对象。一个运算工厂,根据运算符号来决定创建哪个计算对象。具体代码如下:
public class OperatorFactory {
public static Calculator createOperator(char oper){
Calculator calculator = null;
switch (oper){
case '+':
calculator = new OperatorAdd();
break;
case '-':
calculator = new OperatorSub();
break;
case '*':
calculator = new OperatorMul();
break;
case '/':
calculator = new OperatorDiv();
break;
}
return calculator;
}
}
这里是不是就用到了我们曾经学过的多态。
那我们在调用计算业务的时候只需要这几行代码就可以实现计算的过程。
Calculator calculator = OperatorFactory.createOperator(oper);
calculator.setFirstNumber(firstNumber);
calculator.setSecondNumber(secondNumber);
String result = calculator.getResult();
但是这样还是没有彻底的贯彻开闭原则,我们要扩展的时候,运算方法工厂类OperatorFactory 还是要修改代码。那么有没有办法可以在扩展的时候不修改代码呢?
那自然是有办法的,继续抽象,将工厂抽象,抽象成一个运算总工厂。各种运算工厂,比如加法工厂,减法工厂都属于这个总工厂。然后这些运算工厂再生产具体的加法类或者减法类。具体代码如下:
//总工厂
public interface OperatorFactory {
//由子工厂实现该方法,生产具体的运算类。
Calculator createCalculator();
}
//加法工厂,实现总工厂
public class AddOperatorFactory implements OperatorFactory{
//生产具体运算类
@Override
public Calculator createCalculator() {
return new OperatorAdd();
}
}
那么这样一来,再增加新的方法怎么办呢?这个我只写了加法,那再增加一个减法,就是写一个减法工厂类实现总工厂,然后生产具体减法类:
public class SubOperatorFactory implements OperatorFactory{
@Override
public Calculator createCalculator() {
return new OperatorSub();
}
}
那么如何调用这些类呢?看代码:
//实现加法运算,实例化加法工厂
OperatorFactory addOperator = new AddOperatorFactory();
//生产具体加法类
Calculator calculator = addOperator.createCalculator();
calculator.setFirstNumber(firstNumber);
calculator.setSecondNumber(secondNumber);
String result = calculator.getResult();
这样一来,扩展功能只需要按照规则增加相应的类就可以。那这里我们又有疑问了,判断运算符的方法去了哪里?如果把输入参数或者说界面叫做客户端,那么这个逻辑就到了客户端这里,那这样一来,不是依旧要修改客户端代码?
这里怎么说呢?在我的理解中,是因为我们没有对客户端代码进行抽象处理。按照这个思路来,如果我们对客户端代码进行抽象处理,也让它符合开闭原则,理论上是可以实现的。但是这里并不是要把它写的多么完善,重点是理解它的思想。
个人总结
在工厂模式的学习中,从头到尾我的脑海中就两个字,抽象,不断地抽象。