设计模式之工厂方法

工厂方法模式

先看问题:

业务场景:需要实现一个复杂的计算器,计算器包含3个菜单:标准计算器模式,科学计算器模式,程序员计算器模式。

其中标准计算器模式能够进行: 加法,减法,乘法运算

科学计算器模式能够进行: 根号,对数,幂运算

程序员计算器能够进行: 左移 ,右移,位与运算

其他要求:需要有用户界面

image

分析问题:

第一:需要支持模式操作

第二:根据不同的模式,需要3个操作符按钮需要能够进行不同的运算

首先,按照正常的代码来写:我们一般会先判断当前选择的模式,再根据不同的模式给操作符1,操作符2,操作符3赋予不同的功能:

代码如下:

public class OperationUtils {
    //总方法,根据传入的mode选择具体的方法
    public static double getResult(String mode, String opera, double op1, double op2) {

        switch (mode) {
            case "标准":
                return getStandardResult(opera, op1, op2);
            case "科学计算器":
                return getScientificResult(opera, op1, op2);
            case "程序员":
                return getComputerResult(opera, op1, op2);
            default:
                throw new UnsupportedOperationException("不支持的模式");
        }

    }

    //标准计算器方法,可以执行+ - *
    private static double getStandardResult(String opera, double op1, double op2) {
        switch (opera) {
            case "+":
                return op1 + op2;
            case "-":
                return op1 - op2;
            case "*":
                return op1 * op2;
            default:
                throw new UnsupportedOperationException("不支持的操作符");
        }
    }

    //科学计算器方法,可以执行对数,开方,幂运算
    private static double getScientificResult(String opera, double op1, double op2) {
        switch (opera) {
            case "√":
                return Math.pow(op1, 1 / op2);
            case "^":
                return Math.pow(op1, op2);
            case "log":
                return Math.log(op1) / Math.log(op2);
            default:
                throw new UnsupportedOperationException("不支持的操作符");

        }
    }
    //计算机计算器方法,可以执行左移,右移,与等位运算
    private static double getComputerResult(String opera, double op1, double op2) {
        int intOp1 = (int) op1;
        int intOp2 = (int) op2;
        switch (opera) {
            case "<<":
                return intOp1 << intOp2;
            case ">>":
                return intOp1 >> intOp2;
            case "&":
                return intOp1 & intOp2;
            default:
                throw new UnsupportedOperationException("不支持的操作符");
        }
    }

}

调用方法就很简单:

public class Main {
    public static void main(String[] args) {
        double op1=7;
        double op2=9;
        String model="程序员";
        OperationUtils.getResult(model,"<<",op1,op2);
    }
}

其中model可以在用户点击标准模式科学模式程序员模式的时候进行赋值。op1op2可以根据用户点击的具体数字进行赋值。

首先,上面的代码是典型的面向过程代码,同理,如同在介绍简单工厂设计模式中提到的一样,计算器类作为核心逻辑,所有的逻辑都被暴露在外面,容易引起bug。

接下来根据设计模式原则分析一下上面的代码的问题:

  • SRP:上面的代码中,当需要增加一个计算模式的时候,需要改变OperationUtils,当某个计算模式中需要具体的增加某个方法的时候,也需要改变OperationUtils,这违背的单一职责原则。
  • OCP:同样,当需要扩展/修改某个计算模式/计算符的时候,需要修改OperationUtils

对于其他的原则,由于这不是一个面向对象的类,所以不存在其他原则。

上面代码的唯一优点只在于:每个方法之间的分层比较清晰。


我们似乎发现问题和简单工厂比较像?

于是我们同样使用上篇文章学到的简单工厂模式重构上面的代码:

首先需要将核心业务使用面向对象包装起来:

定义一个Operation

public abstract class Operation {
    //操作数1
    protected double op1;
    //操作数2
    protected double op2;
    //给操作数1赋值
    public void setOp1(double op1){
        this.op1=op1;
    }
    //给操作数2赋值
    public void setOp2(double op2) {
        this.op2 = op2;
    }
    //虚方法,由子类决定具体的操作
    public abstract double getResult();
}

接下来为每个具体的运算符实现具体的操作:

//加法
public class AddOperation extends Operation {
    @Override
    public double getResult() {
        return op1+op2;
    }
}
//乘法
public class MulOperation extends Operation {
    @Override
    public double getResult() {
        return op1*op2;
    }
}
//...
//...

实现了9个操作后,我们使用OperaFactory将他们包装起来:

public class OperationFactory {
    //获取操作对象
    public static Operation getOperation(String model, String opera, double op1, double op2) {
        Operation operation;
        switch (model) {
            case "标准":
                operation = getStandardResult(opera);
                break;
            case "科学计算器":
                operation = getScientificResult(opera);
                break;
            case "程序员":
                operation = getComputerResult(opera);
                break;
            default:
                throw new UnsupportedOperationException("不支持的操作符");
        }
        //为对象赋值
        operation.setOp1(op1);
        operation.setOp2(op2);
        return operation;
    }
    //标准计算器
    private static Operation getStandardResult(String opera) {
        switch (opera) {
            case "+":
                return new AddOperation();
            case "-":
                return new SubOperation();
            case "*":
                return new MulOperation();
            default:
                throw new UnsupportedOperationException("不支持的操作符");
        }
    }

    //科学计算器
    private static Operation getScientificResult(String opera) {
        switch (opera) {
            case "√":
                return new RootOperation();
            case "^":
                return new PowerOperation();
            case "log":
                return new LogarithmOperation();
            default:
                throw new UnsupportedOperationException("不支持的操作符");

        }
    }
    //计算机计算器
    private static Operation getComputerResult(String opera) {

        switch (opera) {
            case "<<":
                return new MoveLeftOperation();
            case ">>":
                return new MoveRightOperation();
            case "&":
                return new AndOperation();
            default:
                throw new UnsupportedOperationException("不支持的操作符");
        }
    }
}

使用方法如下:

public class Main {
    public static void main(String[] args) {
        double op1 = 7;
        double op2 = 9;
        String model = "程序员";
        Operation operation=OperationFactory.getOperation(model,"<<",op1,op2);
        operation.getResult();
    }
}

同样op1op2以及model可以在用户触发的Click事件的时候赋值。

继续分析上面的代码:

  • 已经符合面向对象编程,将核心功能使用对象包装起来
  • SRP:依然违背单一职责原则,不论是扩展计算模式,还是扩展运算符,都需要改变OperationFactory,也就是说OperationFactory担任的职责过多
  • OCP:同SRP,扩展功能的时候需要修改OperationFactory

也就是说上面的代码,除了是将核心功能使用面向对象封装起来后,其他的问题依然存在。


第三次修改:

进过仔细分析OperationFactory我们可以发现,最主要的问题在于OperationFactory需要进行两次switch判断,第一次判断计算器模式,第二次判断用户点击的操作符 ,这样便导致了OperationFactory一直违背SRPOCP,因此我们可以将两次switch判断分离开来、

发现问题后,我们再次重构:

//9个运算符类不变

//新建一个模式工厂父类类

public abstract class OperationFactory {
    //统一管理获取到运算对象后的操作
    //比如赋值,打日志等等
    //但是我们并不知道获取的对象是什么,获取对象的方法作为抽象方法交由子类实现
    public Operation getOperation(String opera, double op1, double op2) {
        Operation operation = createOperation(opera);
        operation.setOp1(op1);
        operation.setOp2(op2);
        return operation;
    }

    protected abstract Operation createOperation(String opera);
}

//每种模式自己实现自己的模式类

//标准计算器
public class StandardOperFactory extends OperationFactory {
    @Override
    public Operation createOperation(String opera) {
        switch (opera) {
            case "-":
                return new SubOperation();
            case "*":
                return new MulOperation();
            case "+":
                return new AddOperation();
            default:
                throw new UnsupportedOperationException("不支持的操作符");
        }
    }
}

//科学计算器
public class ScientificOperFactory extends OperationFactory {
    @Override
    public Operation createOperation(String opera) {
        switch (opera) {
            case "^":
                return new PowerOperation();
            case "log":
                return new LogarithmOperation();
            case "√":
                return new RootOperation();
            default:
                throw new UnsupportedOperationException("不支持的操作符");
        }
    }
}

//计算机计算器
public class ComputerOperFactory extends OperationFactory {
    @Override
    public Operation createOperation(String opera) {
        switch (opera) {
            case "<<":
                return new MoveLeftOperation();
            case "&":
                return new AndOperation();
            case ">>":
                return new MoveRightOperation();
            default:
                throw new UnsupportedOperationException("不支持的操作符");
        }

    }
}

使用方式如下:

public class Main {

    public static void main(String[] args) {

        double op1 = 7;
        double op2 = 9;
        //具体的模式对象factory可以在用户触发`Click`事件的时候赋值/切换
        OperationFactory factory =new StandardOperFactory();
        Operation operation=factory.getOperation("+",op1,op2);
        System.out.println(operation.getResult());
    }
}

继续分析上面的代码:

  • 符合面向对象
  • SRP:没有了具体OperationFactory类,而由每个模式自己管理自己的符号类,当需要增加某个模式下的某个符号时,只需要修改对应的模式即可,当需要增加计算模式的时候,完全不用修改源代码。
  • OCP:只符合一般的OCP,当需要增加新的计算模式的时候,不用修改源代码。当需要增加某个计算模式下的某个符号的时候,需要修改对应的计算模式。
  • OperationFactory作为总的控制中心,可以统一管理产生的对象的生命周期以及产生对象前后的方法调用。

以上示例代码均在:https://github.com/dengchengchao/design-pattern

总结上面的代码,我们可以归纳出一种新的设计模式:工厂方法模式


工厂方法模式

定义

定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行,它也属于类创建型模式

工厂方法模式解决了什么问题?

简单说,工厂方法模式也是属于类创建模式,因此它解决的是创建类的时候所带来的问题。

  1. 当类的行为依赖于用户的多次输入而定的时候,我们可以通过工厂方法模式分离出每次选择
  2. 当类在被实例化之前或之后需要执行某些特定的操作的时候,我们可以通过工厂方法管理,并执行方法,可以规范化流程
  3. 当类的选择比较复杂的时候,我们可以通过工厂方法解耦,而用户不需要关心所产生的具体的类

UML

image

优点

  • 工厂方法模式是对简单工厂的进一步扩展,从总体上来看,比较像是“使用简单工厂将简单工厂封装起来“,它同样可以将类的创建和使用的职责分离开来
  • 工厂方法模式比简单工厂更加符合OCP,当新创建一个大类型的时候,可以完全不用修改原本的代码
  • 工厂方法定义了创建对象的接口,并让子类去决定具体需要创建的对象,当时工厂方法作为获取所有类的控制类,可以统一管理所产生的对象
  • 工厂方法隐藏了创建对象的具体细节,让用户可以更加关心所使用的功能

工厂方法隐藏了创建对象的具体细节,并且可以统一控制所有的对象,这是一个很重要的优点

比如在生产汽车的时候,我们需要根据用户的选择实例化车的材料,但是获取材料后需要将材料组装后才能返回给用户

再比如:在用户获取日志对象的时候,有时候可能在获取对象之前需要连接数据库等操作,这些操作都可以放在工厂类中实现,而对用户来说都是透明的

缺点

  • 有些时候再扩展具体的产品的时候,依然会违背OCP
  • 产生过多的类,还增加了代码量
  • 增加了系统的复杂度

尊重劳动成果,转载注明出处

参考文章:
From-No-Factory-to-Factory-Method

工厂方法–wiki

《大话设计模式》

《Head First 设计模式》