21

【设计模式】第三篇:一篇搞定工厂模式【简单工厂、工厂方法模式、抽象工厂模式】

 4 years ago
source link: https://segmentfault.com/a/1190000037729289
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

IFVjY3B.png!mobile

一 为什么要用工厂模式

之前讲解 Spring 的依赖注入的文章时,我们就已经有提到过工厂这种设计模式,我们直接先通过一个例子来看一下究竟工厂模式能用来做什么?

【万字长文】Spring框架 层层递进轻松入门 (IOC和DI)

首先,我们简单的模拟一个对账户进行添加的操作,我们先采用我们以前常常使用的方式进行模拟,然后再给出改进方案

(一) 举一个模拟 Spring IOC 的例子

(1) 以前的程序

首先,按照我们常规的方式先模拟,我们先将一套基本流程走下来

A:Service 层

/**
 * 账户业务层接口
 */
public interface AccountService {
    void addAccount();
}

/**
 * 账户业务层实现类
 */
public class AccountServiceImpl implements AccountService {
    
    private AccountDao accountDao = new AccountDaoImpl();
    
    public void addAccount() {
        accountDao.addAccount();
    }
}

B:Dao 层

/**
 * 账户持久层接口
 */
public interface AccountDao {
    void addAccount();
}

/**
 * 账户持久层实现类
 */
public class AccountDaoImpl implements AccountDao {

    public void addAccount() {
        System.out.println("添加用户成功!");
    }
}

C:调用

由于,我们创建的Maven工程并不是一个web工程,我们也只是为了简单模拟,所以在这里,创建了一个 Client 类,作为客户端,来测试我们的方法

public class Client {
    public static void main(String[] args) {
        AccountService  as = new AccountServiceImpl();
        as.addAccount();
    }
}

运行的结果,就是在屏幕上输出一个添加用户成功的字样

D:分析:new 的问题

上面的这段代码,应该是比较简单也容易想到的一种实现方式了,但是它的耦合性却是很高的,其中这两句代码,就是造成耦合性高的根由,因为业务层(service)调用持久层(dao),这个时候业务层将很大的依赖于持久层的接口(AccountDao)和实现类(AccountDaoImpl)

private AccountDao accountDao = new AccountDaoImpl();

AccountService as = new AccountServiceImpl();

这种通过 new 对象的方式,使得不同类之间的依赖性大大增强,其中一个类的问题,就会直接导致出现全局的问题,如果我们将被调用的方法进行错误的修改,或者说删掉某一个类,执行的结果就是:

YFjiaiZ.png!mobile

编译期 就出现了 错误 ,而我们作为一个开发者,我们应该努力让程序在编译期不依赖,而运行时才可以有一些必要的依赖(依赖是不可能完全消除的)

所以,我们应该想办法进行 解耦 ,要解耦就要使 调用者被调用者 之间没有什么直接的联系,那么 工厂模式 就可以帮助我们很好的解决这个问题

(2) 工厂模式改进

A:BeanFactory

具体怎么实现呢?在这里可以将 serivice 和 dao 均配置到配置文件中去(xml/properties),通过一个类读取配置文件中的内容,并使用反射技术创建对象,然后 存起来 ,完成这个操作的类就是我们的工厂

注:在这里我们使用了 properties ,主要是为了实现方便,xml还涉及到解析的一些代码,相对麻烦一些,不过我们下面要说的 Spring 就是使用了 xml做配置文件

  • bean.properties:先写好配置文件,将 service 和 dao 以 key=value 的格式配置好
accountService=cn.ideal.service.impl.AccountServiceImpl
accountDao=cn.ideal.dao.impl.AccountDaoImpl
  • BeanFactory
public class BeanFactory {
    //定义一个Properties对象
    private static Properties properties;
    //使用静态代码块为Properties对象赋值
    static {
        try{
            //实例化对象
            properties = new Properties();
            //获取properties文件的流对象
            InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("bean.properties");
            properties.load(in);
        }catch (Exception e){
            throw  new ExceptionInInitializerError("初始化properties失败");
        }
    }  
}

简单的解释一下这部分代码(当然还没写完):首先就是要将配置文件中的内容读入,这里通过类加载器的方式操作,读入一个流文件,然后从中读取键值对,由于只需要执一次,所以放在静态代码块中,又因为 properties 对象在后面的方法中还要用,所以写在成员的位置

接着在 BeanFactory 中继续编写一个 getBean 方法其中有两句核心代码的意义就是:

  • 通过方法参数中传入的字符串,找到对应的全类名路径,实际上也就是通过刚才获取到的配置内容,通过key 找到 value值

32UNRfi.png!mobile

  • 下一句就是通过 Class 的加载方法加载这个类,实例化后返回
public static Object getBean(String beanName){
    Object bean = null;

    try {
        //根据key获取value
        String beanPath = properties.getProperty(beanName);
        bean = Class.forName(beanPath).newInstance();
    }catch (Exception e){
        e.printStackTrace();
    }
    return bean;
}

B:测试代码:

public class Client {
    public static void main(String[] args) {
        AccountService as =                        (AccountService)BeanFactory.getBean("accountService");
        as.addAccount();
    }
}

C:执行效果:

当我们按照同样的操作,删除掉被调用的 dao 的实现类,可以看到,这时候编译期错误已经消失了,而报出来的只是一个运行时异常,这样就解决了前面所思考的问题

我们应该努力让程序在编译期不依赖,而运行时才可以有一些必要的依赖(依赖是不可能完全消除的)

EZZVryA.png!mobile

(3) 小总结:

为什么使用工厂模式替代了 new 的方式?

打个比方,在你的程序中,如果一段时间后,你发现在你 new 的这个对象中存在着bug或者不合理的地方,或者说你甚至想换一个持久层的框架,这种情况下,没办法,只能修改源码了,然后重新编译,部署,但是如果你使用工厂模式,你只需要重新将想修改的类,单独写好,编译后放到文件中去,只需要修改一下配置文件就可以了

我分享下我个人精简下的理解就是:

【new 对象依赖的是具体事物,而不 new 则是依赖抽象事物】

Break it down:

  • 依赖具体事物,这个很好理解,你依赖的是一个具体的,实实在在内容,它与你系相关,所以有什么问题,都是连环的,可能为了某个点,我们需要修改 N 个地方,绝望
  • 依赖抽象事物,你所调用的并不是一个直接就可以触手可及的东西,是一个抽象的概念,所以不存在上面那种情况下的连环反应

二 三种工厂模式

看完前面的例子,我想大家已经已经对工厂模式有了一个非常直观的认识了

说白了,工厂模式就是使用一种手段,代替了 new 这个操作

以往想要获取一个实例的时候要 new 出来,但是这种方式耦合性就会很高,我们要尽量的减少这种可避免的耦合负担,所以工厂模式就来了

工厂就是在调用者和被调用者之间起一个连接枢纽的作用,调用者和被调用者都只与工厂进行联系,从而减少了两者之间直接的依赖

工厂模式一共有三种 ① 简单工厂模式,② 工厂方法模式 ③ 抽象工厂模式

下面我们一个一个来说

(一) 简单工厂模式

(1) 实现

下面我们以一个车的例子来讲,首先我们有一个抽象的 Car 类

public abstract class Car {
    // 任何汽车都会跑
    public abstract void run();
}

接着就是它的子类,我们先来两个,一个宝马类,一个奔驰类(为阅读方便写成了拼音命名,请勿模仿,不建议)

public class BaoMa extends Car {
    @Override
    public void run() {
        System.out.println("【宝马】在路上跑");
    }
}
public class BenChi extends Car {
    @Override
    public void run() {
        System.out.println("【奔驰】在路上跑");
    }
}

那如果我想要实例化这个类,实际上最原始的写法可以这样(也就是直接 new 出来)

public class Test {
    public static void main(String[] args) {
        Car baoMa = new BaoMa();
        baoMa.run();
        Car benChi = new BenChi();
        benChi.run();
    }
}

如果使用简单工厂模式,就需要创建一个专门的工厂类,用来实例化对象

public class CarFactory {
    public static Car createCar(String type) {
        if ("宝马".equals(type)) {
            return new BaoMa();
        } else if ("奔驰".equals(type)) {
            return new BenChi();
        } else {
            return null;
        }
    }
}

真正去调用的时候,我只需要传入一个正确的参数,通过 CarFactory 创建出想要的东西就可以了,具体怎么去创建就不需要调用者操心了

public class Test {
    public static void main(String[] args) {
        Car baoMa = CarFactory.createCar("宝马");
        baoMa.run();
        Car benChi = CarFactory.createCar("奔驰");
        benChi.run();
    }
}

(2) 优缺点

先说一下优点:

简单工厂模式的优点就在于其工厂类中含有必要的逻辑判断(例如 CarFactory 中判断是宝马还是奔驰),客户端只需要通过传入参数(例如传入 “宝马”),动态的实例化想要的类,客户端就免去了直接创建产品的职责,去除了与具体产品的依赖(都不需要知道具体类名了,反正我不负责创建)

但是其缺点也很明显:

简单工厂模式的工厂类职责过于繁重,违背了高聚合原则,同时其内容多的情况下,逻辑太复杂。最关键的是,当我想要增加一个新的内容的时候,例如增加一个保时捷,我就不得不去修改 CarFactory 工厂类中的代码,这很显然违背了 “开闭原则”

所以,工厂模式他就来了

(二) 工厂模式

(1) 实现

依旧是一个汽车抽象类,一个宝马类和一个奔驰类是其子类

public abstract class Car {
    // 任何汽车都会跑
    public abstract void run();
}
public class BaoMa extends Car {
    @Override
    public void run() {
        System.out.println("【宝马】在路上跑");
    }
}
public class BenChi extends Car {
    @Override
    public void run() {
        System.out.println("【奔驰】在路上跑");
    }
}

如果是简单工厂类,就会 有一个总的工厂类来实例化对象,为了解决其缺点,工厂类首先需要创建一个汽车工厂接口类

public interface CarFactory {
    // 可以获取任何车
    Car createCar();
}

然后宝马和奔驰类分别实现它,内容就是创建一个对应宝马或者奔驰(实例化宝马类或者奔驰类)

public class BaoMaFactory implements CarFactory {
    @Override
    public Car createCar() {
        return new BaoMa();
    }
}
public class BenChiFactory implements CarFactory {
    @Override
    public Car createCar() {
        return new BenChi();
    }
}

想要获取车的时候,只需要通过多态创建出想要获得的那种车的工厂,然后通过工厂再创建出对应的车,例如我分别拿到奔驰和宝马就可以这样做:

public class Test {
    public static void main(String[] args) {
        // 先去奔驰工厂拿到一台奔驰
        CarFactory benChiFactory = new BenChiFactory();
        // 4S店拿到一台奔驰,给了你
        Car benChi = benChiFactory.createCar();
        benChi.run();

        // 先去宝马工厂拿到一台宝马
        CarFactory baoMaFactory = new BaoMaFactory();
        // 4S店拿到一台宝马,给了你
        Car baoMa = baoMaFactory.createCar();
        baoMa.run();
    }
}

这种情况下,如果我还想要增加一台保时捷类型的车,创建出对应的保时捷类(继承 Car)以及对应保时捷工厂类后后,仍只需要通过以上方法调用即可

// 先去保时捷工厂拿到一台保时捷
CarFactory baoShiJieFactory = new BaoShiJieFactory();
// 4S店拿到一台保时捷,给了你
Car baoShiJie = baoShiJieFactory.createCar();
baoShiJie.run();

(2) 定义

工厂方法模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类,工厂方法使一个类的实例化延迟到其子类

看其结构图

e2EvQ3u.png!mobile

(3) 优缺点

优点:

  • 对象的创建,被明确到了各个子工厂类中,不再需要在客户端中考虑
  • 新内容增加非常方便,只需要增加一个想生成的类和创建其的工厂类
  • 不违背 “开闭原则”,后期维护,扩展方便

缺点:

  • 代码量显著增加

(三) 抽象工厂模式

抽象工厂模式是一种比较复杂的工厂模式,下面先直接通过代码了解一下

还是说车,我们将车分为两种,一种是普通轿车,一种是卡车,前面的工厂方法模式中,如果不断的增加车的类型,这势必会造成工厂过多,但是对于常见的车来说,还可以寻找可抽取的特点,来进行抽象

所以在此基础之上,我们又分别设定了自动挡和手动挡两种类型,所以两两搭配,就有四种情况了(eg:自动挡卡车,手动挡轿车等等)

(1) 创建抽象产品

  • 首先分别创建普通轿车和卡车的抽象类,然后定义两个方法(这里我就写成一样的了,可以根据轿车和卡车的特点写不同的方法)
public abstract class CommonCar {
    // 所有车都能,停车
    abstract void parking();
    // 所有车都能,换挡
    abstract void shiftGear();
}
public abstract class Truck  {
    // 所有车都能,停车
    abstract void parking();
    // 所有车都能,换挡
    abstract void shiftGear();
}

(2) 实现抽象产品

说明: A是自动的意思,H是手动的意思,eg:CommonCarA 代表普通自动挡轿车

  • 实现抽象产品——小轿车(自动挡)
public class CommonCarA extends CommonCar{
    @Override
    void parking() {
        System.out.println("自动挡轿车A,停车挂P档");
    }

    @Override
    void shiftGear() {
        System.out.println("自动挡轿车A,可换挡 P N D R");
    }
}
  • 实现抽象产品——小轿车(手动挡)
public class CommonCarH extends CommonCar {
    @Override
    void parking() {
        System.out.println("手动挡轿车H,停车挂空挡,拉手刹");
    }

    @Override
    void shiftGear() {
        System.out.println("手动挡轿车H,可换挡 空 1 2 3 4 5 R");
    }
}
  • 实现抽象产品——货车(自动挡)
public class TruckA extends Truck {
    @Override
    void parking() {
        System.out.println("自动挡货车A,停车挂P档");
    }

    @Override
    void shiftGear() {
        System.out.println("自动挡货车A,可换挡 P N D R");
    }
}
  • 实现抽象产品——货车(手动挡)
public class TruckH extends Truck {

    @Override
    void parking() {
        System.out.println("手动档货车H,停车挂空挡,拉手刹");
    }

    @Override
    void shiftGear() {
        System.out.println("手动档货车H,可换挡 空 1 2 3 4 5 R");
    }
}

(3) 创建抽象工厂

public interface CarFactory {
    // 创建普通轿车
    CommonCar createCommonCar();
    // 创建货车
    Truck createTruckCar();
}

(4) 实现抽象工厂

通过自动挡手动挡这两个抽象概念,创建出这两个工厂,创建具有特定实现类的产品对象

  • 自动挡汽车工厂类
public class AutomaticCarFactory implements CarFactory {
    @Override
    public CommonCarA createCommonCar() {
        return new CommonCarA();
    }

    @Override
    public TruckA createTruckCar() {
        return new TruckA();
    }
}
  • 手动挡汽车工厂类
public class HandShiftCarFactory implements CarFactory {
    @Override
    public CommonCarH createCommonCar() {
        return new CommonCarH();
    }

    @Override
    public TruckH createTruckCar() {
        return new TruckH();
    }
}

(5) 测试一下

public class Test {
    public static void main(String[] args) {
        // 自动挡车工厂类
        CarFactory automaticCarFactory = new AutomaticCarFactory();
        // 手动挡车工厂类
        CarFactory handShiftCarFactory = new HandShiftCarFactory();

        System.out.println("=======自动挡轿车系列=======");
        CommonCar commonCarA = automaticCarFactory.createCommonCar();
        commonCarA.parking();
        commonCarA.shiftGear();

        System.out.println("=======自动挡货车系列=======");
        Truck truckA = automaticCarFactory.createTruckCar();
        truckA.parking();
        truckA.shiftGear();

        System.out.println("=======手动挡轿车系列=======");
        CommonCar commonCarH = handShiftCarFactory.createCommonCar();
        commonCarH.parking();
        commonCarH.shiftGear();

        System.out.println("=======手动挡货车系列=======");
        Truck truckH = handShiftCarFactory.createTruckCar();
        truckH.parking();
        truckH.shiftGear();
    }
}

运行结果:

=======自动挡轿车系列=======
自动挡轿车A,停车挂P档
自动挡轿车A,可换挡 P N D R
=======自动挡货车系列=======
自动挡货车A,停车挂P档
自动挡货车A,可换挡 P N D R
=======手动挡轿车系列=======
手动挡轿车H,停车挂空挡,拉手刹
手动挡轿车H,可换挡 空 1 2 3 4 5 R
=======手动挡货车系列=======
手动档货车H,停车挂空挡,拉手刹
手动档货车H,可换挡 空 1 2 3 4 5 R

补充两个概念

  • 产品等级结构产品的等级结构就是其继承结构 ,例如上述代码中,CommonCar(普通轿车) 是一个抽象类,其子类有 CommonCarA (自动挡轿车)和 CommonCarH(手动挡轿车),则 普通轿车抽象类 就与具体 自动挡 或者手动挡的轿车构成一个产品等级结构。
  • 产品族产品族是同一个工厂生产,位于不同产品等级结构中的一组产品 ,例如上述代码中,CommonCarA(自动挡轿车)和 TruckA(自动挡货车),都是AutomaticCarFactory(自动挡汽车工厂)这个工厂生成的

Iryuquq.png!mobile

(6) 结构图

6VRZJzz.png!mobile

看着结构图,我们再捋一下

首先 AbstractProductA 和 AbstractProductB 是两个抽象产品,分别对应我们上述代码中的 CommonCar 和 Truck,为什么是抽象的,因为它们可以都有两种不同的实现,即自动挡轿车和自动货车,手动挡轿车和手动挡卡车

ProductA1 和 ProductA2 和 ProductB1 和 ProductB2 就是具体的实现,代表 CommonCarA 和 CommonCarH 和 TruckA 和 TruckH

抽象工厂 AbstractFactory 里包含了所有产品创建的抽象方法,ConcreteFactory1 和 ConcreteFactory2 就是具体的工厂,通常是在运行时再创建一个 ConcreteFactory 的实例,这个工厂再创建具有特定实现的产品对象,也就是说为了创建不同的产品对象,客户端应该使用不同的具体工厂

(7) 反射+配置文件实现优化

抽象工厂说白了就是通过内容抽象的方式,减少了工厂的数量,同时在具体工厂我们可以这么用

CarFactory factory = new AutomaticCarFactory();

具体工厂只需要在初始化的时候出现一次,这也使得修改一个具体工厂也是比较容易的

但是缺点也是非常明显,当我想扩展一,比如加一个拖拉机类型,我就需要修改 CarFactory接口,AutomaticCarFactory 类 HandShiftCarFactory 类,(当然,拖拉机貌似没有什么自动挡,我只是为了举例子),还需要增加拖拉机对应的内容

也就是说,增加的基础上,我还需要修改原先的三个类,这是一个非常显著的缺点

除此之外还有一个问题,如果很多地方都声明了

CarFactory factory = new AutomaticCarFactory();

并且进行了调用,如果我更换了这个工厂,就需要大量的进行修改,很显然这一点是有问题的,我们下面来使用反射优化一下

public class Test {
    public static void main(String[] args) throws Exception {

        Properties properties = new Properties();
        // 使用ClassLoader加载properties配置文件生成对应的输入流
        InputStream in = Test.class.getClassLoader().getResourceAsStream("config.properties");
        // 使用properties对象加载输入流
        properties.load(in);
        //获取key对应的value值
        String factory = properties.getProperty("factory");

        CarFactory automaticCarFactory = (CarFactory) Class.forName(factory).newInstance();

        System.out.println("======轿车系列=======");
        CommonCar commonCarA = automaticCarFactory.createCommonCar();
        commonCarA.parking();
        commonCarA.shiftGear();

        System.out.println("=======货车系列=======");
        Truck truckA = automaticCarFactory.createTruckCar();
        truckA.parking();
        truckA.shiftGear();

    }
}

config.properties

factory=cn.ideal.factory.abstractFactory.AutomaticCarFactory
#factory=cn.ideal.factory.abstractFactory.HandShiftCarFactory

运行结果:

=======轿车系列=======
自动挡轿车A,停车挂P档
自动挡轿车A,可换挡 P N D R
=======货车系列=======
自动挡货车A,停车挂P档
自动挡货车A,可换挡 P N D R

通过反射+配置文件我们就可以使得使用配置文件中的键值对(字符串)来实例化对象,而变量是可以更换的,也就是说程序由编译时转为运行时,增大了灵活性,去除了判断的麻烦

回到前面的问题,如果我们现在要增加一个新的内容,内容的增加没什么好说的,这是必须的,这是扩展,但是对于修改我们却要尽量关闭,现在我们可以通过修改配置文件来达到实例化不同具体工厂的方式

但是还需要修改三个类,以添加新内容,这里还可以通过简单工厂来进行优化,也就是去掉这几个工厂,使用一个简单工厂,其中写入 createCommonCar(); 等这些方法, 再配合反射+配置文件也能实现刚才的效果,这样如果新增内容的时候只需要修改配置文件后,再修改这一个类就可以,即增加一个 createXXX 方法,就不需要修改多个内容了


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK