设计模式学习之“工厂模式“

工厂模式

  • 作用:

    • 实现创建者与调用者分离
  • 核心本质:

    • 实例化对象不使用new,而是用工厂方法代替
    • 将选择实现类,创建对象统一管理和控制。从而将调用者和实现类解耦。
  • 三种模式:

    • 简单工厂模式

      用来生成同一等级结构中的任意产品(对于增加新的产品,需要覆盖已有代码)

    • 工厂方法模式:

      用来生产同一等级结构中的固定产品(支持添加任意产品

    • 抽象工厂模式:

      围绕一个超级工厂创建其他工厂,这个超级工厂又可以被视为生产其他工厂的工厂

  • 小结:

    • 静态工厂模式

      虽然某种程度上不符合设计模式,但实际使用最多!

    • 工厂方法模式

      可以在不修改已有类的前提下,通过增加新的工厂实现横向扩展

    • 抽象工厂模式

      不可以增加产品,但可以增加产品种类

  • 应用场景:

    1. JDK中Calendar的getInstance()方法
    2. JDBC中数据源的创建,Connection对象的获取
    3. Spring中IOC容器创建管理bean对象
    4. 反射中Class对象的newInstance()方法

1、简单工厂模式

这是最简单工厂模式,基本实现了对象创建工作从用户移交到了工厂!用户需要什么对象,只需要调用工厂方法即可!
用户无需关心这些产品对象之间的关系,一切让工厂来做就OK。

1.1、实现过程——汽车工厂案例

  1. 确定一个商品类,是抽象类!并同时创建一个工厂,对应生产此类商品,提供一个开放API用于外部从工厂获取商品!
  2. 创建多个商品类的实现(即此商品类下的具体商品)
  3. 工厂制定规则,设置什么情况下返回什么商品!
  4. 用户创建工厂,调用工厂开放的API,根据规则传入参数,获取对应的商品

image-20210828161712224

抽象的商品类:Car类,有一个名为getBrand()的方法

public abstract class Car {
  /**
   * 获取车的品牌
   * 
   * @return
   */
  abstract String getBrand();
}

具体的商品类

public class AudiCar extend Car {
    @Override
    public String getBrand() {
        return "Audi";
    }
}

public class BenzCar extend Car {
    @Override
    public String getBrand() {
        return "Benz";
    }
}

工厂类:生产对应的产品类

/**
   * 获取品牌为brand的Car对象
   * 
   * @param brand 对应车辆品牌
   * @return
   * @throws IllegalArgumentException 当品牌超出当前工厂能力范围,抛出异常
   */
  public Car getCar(String brand) throws IllegalArgumentException {
    if ("Audi".equalsIgnoreCase(brand)) {
      return new AudiCar();
    } else if ("Benz".equalsIgnoreCase(brand)) {
      return new BenzCar();
    } else {
      throw new IllegalArgumentException("本工厂暂时无法生产此品牌车:" + brand);
    }
  }

用户使用

public class Consumer {
  public static void main(String[] args) {
    // 1. 创建工厂
    CarFactory factory = new CarFactory();

    // 2. 调用开放的API获取产品
    Car MyAudi = factory.getCar("Audi");
    Car MyBenz = factory.getCar("benz");
    Car MyFerrari = factory.getCar("Ferrari");

    MyAudi.getBrand();
    MyBenz.getBrand();
    MyFerrari.getBrand();
  }
}

案例中还存在一些问题,例如如果用户不熟悉此工厂,可能会传入一些“不合理”的参数,导致工厂抛出异常,而导致用户工作无法继续进行!就算用户使用try…catch进行了异常捕获,用户最终得到的也只是一个null,同样会导致问题。可以提供一个默认商品类实现,来避免此问题!


2、工厂方法模式

现在我们进入较为高级的工厂模式实现:工厂方法模式。前面的简单工厂模式还存在很多明显的问题:

  • 当工厂升级后,商品类实现变多了,就需要修改原来的开发API,增加分支,违反了开放-封闭原则
  • 同时这也导致,商品类和其对应的工厂耦合在了一起!

因为扩展商品类时,在简单工厂模式下我们需要修改工厂类,那么在工厂方法模式中就要设法解决这个问题!既然我们不可以修改,可以==尝试将工厂设计为可扩展的!==我们可以将工厂抽象为接口,然后让对应的工厂生产对应的产品,当扩展产品的同时,同步扩展出一个工厂即可!

2.1、实现过程

  1. 产品部分不用修改
  2. 删除汽车工厂类,取而代之的是一个汽车工厂接口
  3. 针对不同的商品实现,创建具体的汽车工厂(实现工厂接口)
image-20210828203804040

工厂接口

public interface CarFactory {
    /**
     * 获取Car对象的开放API
     *
     * @return
     */
    Car getCar();
}

具体的工厂实现:

public class AudiFactory implements CarFactory {
    @Override
    public Car getCar() {
        return new Audi();
    }
}

public class BenzFactory implements CarFactory {
    @Override
    public Car getCar() {
        return new Benz();
    }
}

public class TeslaFactory implements CarFactory {
    @Override
    public Car getCar() {
        return new Tesla();
    }
}

现在如果你需要添加新产品实现时,你就不需要去修改原有的代码了,只需要针对此商品扩展一个工厂即可!并且这种实现方法也不用担心用户因为不了解工厂而无法构建出合适的商品。在创建工厂的时候用户就已经心里有数了。


3、抽象工厂模式

先分清楚产品等级与产品族

在这里插入图片描述

前面的工厂方法模式解决了同一个商品类(即同一个商品级)中增加具体商品实现时需要修改工厂的问题,转而针对同商品级的每个商品都创建一个工厂。但是还会存在一个问题:工厂业务线扩展了,咱不止生成汽车了,咱还要造手机,造电脑,这就属于是商品种类扩展了,那么也就是说我们要添加商品抽象类了,那么也会对应增加商品实现,那么对于扩充出来的商品类,原来的工厂接口就不适用了了(原来的工厂只能造汽车,我们需要改进工厂接口,针对其他产品线增加方法。)

image-20210828231106529

  • 创建一个抽象工厂,相当于一个工厂的模板,后面创建的工厂都要实现这个抽象工厂接口。
  • 抽象工厂规定产品等级,由真实工厂创建出产品族

3.1、实现过程

新工厂接口

public interface Factory {
    /**
     * 生产手机
     * 
     * @return
     */
    Phone getPhone();

    /**
     * 生产路由器
     * 
     * @return
     */
    Router getRouter();

    /**
     * 生产笔记本电脑
     * 
     * @return
     */
    Laptop getLaptop();
}

一个工厂实现

public class HuaweiFactory implements Factory {
    @Override
    public Phone getPhone() {
        return new HuweiPhone();
    }

    @Override
    public Router getRouter() {
        return new HuaweiRouter();
    }

    @Override
    public Laptop getLaptop() {
        return new HuaweiLaptop();
    }
}

这样我们同一个品牌的工厂就可以生产出多类型的产品,同一个工厂的产品可以统一起来进行管理更加方便!但是同时他还存在一个严重的问题!

如果要新增产品种类,那么从工厂接口开始,其下的所有工厂实现都要修改! 最好的情况就是:创建工厂接口的时候就确定了工厂能生产那些种类的商品,此后不再修改!

优点:具体的产品在应用层的代码隔离,无需关系创建细节。

​ 将同族产品统一到一起创建

缺陷:规定了产品级,同一产品族中,增加新产品级困难

​ 增加了系统的抽象性和理解难度

适用场景:

  1. 客户端不依赖于产品类实例如何创建,实现等细节。
  2. 强调一系列相关的产品对象(即同一产品族)一起使用创建对象需要大量代码。
  3. 提供一个产品等级的库,不同族的同等级的产品以同样的接口出现,从而是客户端不依赖于具体实现。

4、JDBC与工厂模式

4.0、基础知识

JDBC就是工厂模式应用的一个很典型的案例!JDK官方提供一套接口,各大数据库厂商按照接口设计积极适配,虽然都是同一套接口,但是你使用的数据源不同,对应调用的代码也会有所不同。

那么其中ConnectionStatement等JDBC API中提供的接口,就好比我们案例中的产品抽象类!
DriverManager.getConnection() 是我们学习JDBC是用于获取连接的方法,但是DriverManager并不是我们这里要找的抽象工厂!!这里就是JDBC设计的高级之处了,使用反射!我们后面再说。这里我们先来找出我们期待的抽象工厂接口!

先来翻看一下DriverManager.getConnection()的实现源码:

for (DriverInfo aDriver : registeredDrivers) {
    // If the caller does not have permission to load the driver then
    // skip it.
    if (isDriverAllowed(aDriver.driver, callerCL)) {
        try {
            println("    trying " + aDriver.driver.getClass().getName());
            Connection con = aDriver.driver.connect(url, info);
            if (con != null) {
                // Success!
                println("getConnection returning " + aDriver.driver.getClass().getName());
                return (con);
            }
        } catch (SQLException ex) {
            if (reason == null) {
                reason = ex;
            }
        }

    } else {
        println("    skipping: " + aDriver.getClass().getName());
    }

}

这里我截取了getConnection的部分源码,这里遍历了所有已经注册的Driver的DriverInfo【Driver的描述信息类】(我们使用Class.forName()等方式触发类加载的时候,注册了Driver,什么原理我们等下说)。里面有一句很扎眼的代码:

Connection con = aDriver.driver.connect(url, info);

这里DriverInfo,利用自己内部的Driver属性,调用了其connect方法,然后得到了一个Connection对象!

是不是有点内味了,果不其然我们打开Driver代码,发现他正是一个接口!并且来自于java.sql 那这就是我们一直在找的抽象工厂!

接下来,我们就来看看这个抽象工厂Driver的实现类。这里我就选用比较常用的MySQL和SqlServer,来看看他们的连接驱动中是不是有针对Driver的工厂实现。

4.1、MySQL Driver探秘

解压mysql-java-connection.jar包。我们通常在加载驱动类的时候会使用Class.forName("com.mysql.cj.jdbc.Driver"), 我们就沿着这个包,找到了我们要查看的Driver类的字节码,进行反编译后得到代码:

package com.mysql.cj.jdbc;

import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements Driver {
  static {
    try {
      DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
      throw new RuntimeException("Can't register driver!");
    } 
  }
}

睁大你的眼睛!com.mysql.cj.jdbc.Driver是不是!实现了!java.sql.Driver接口!!

由于我们在使用Class.forName()会触发类加载【具体笔记查看反射、类加载相关知识!】那么JVM在执行类加载时,类静态初始化就会执行static代码块!那么就会执行DriverManager.registerDriver(new Driver()),这里就成功将MySQL的"工厂实现" 注册到了程序中,所以我们在使用DriverManager.getConnection()时就能获取到这个工厂生产的MySQL连接类了!【MySQL肯定针对Connection进行了实现,这里我就不找出来看了~】

image-20210828222915932

4.2、SqlServer Driver探秘

同样我们将连接驱动的jar包解压。

由于SqlServer 作者本人基本没有在开发中使用过,废了大劲找到了这个Driver类:

com.microsoft.sqlserver.jdbc.SQLServerDriver

image-20210828223256162

请大声告诉我!它!是不是!实现了!java.sql.Driver接口!!

同理,我们往下翻,我们又看到了这段熟悉的代码:

image-20210828223614760

一样是在static代码块中,执行了DriverManager.registerDriver()~

同理,SqlServer也有java.sql.Connection的实现:

image-20210828224122099

这里虽然是接口,其实只是针对性做了一些扩展~ 上面MySQL同理~~

4.3、启示

JDBC中,在创建工厂的时候为我们提供了一个很巧妙的思路!使用对应的Driver的全限定类名就可以找到对应的工厂类,从而完成工厂创建。也就是说我们通过一个字符串【全限定名】就可以确定你所要的工厂是哪个。那么是不是可以有更简单的实现呢?【这里并不是要挑战JDBC的设计,只是思考针对抽象工厂设计模式的一种简化~】


5、抽象工厂简化

5.1、简单工厂模式简化抽象工厂

上面我们提到过,如果我通过一个字符串就知道你需要哪个工厂,即某一个产品族时(例如第3节中的两个工厂可以通过"Xiaomi"、"Huawei"进行区分),并且他们生产的东西都是固定的!我们就可以移除这些接口约束,转而使用简单工厂模式来替代这些工厂接口/类!

移除Factory接口和XiaomiFactory、HuaweiFactory后,使用简单工厂模式:

public class SimplifyFactory {
    private static final String brand = "Xiaomi";
    
    public static Phone getPhone() {
        Phone phone = null;
        switch (brand) {
            case "Xiaomi": phone = new XiaomiPhone(); break;
            case "Huawei": phone = new HuaweiPhone(); break;
        }
        return phone;
    }
    
    public static Laptop getLaptop() {
        Laptop laptop = null;
        switch (brand) {
            case "Xiaomi": laptop = new XiaomiLaptop(); break;
            case "Huawei": laptop = new HuaweiLaptop(); break;
        }
        return laptop;
    }

    public static Router getRouter() {
        Router router = null;
        switch (brand) {
            case "Xiaomi": router = new XiaomiRouter(); break;
            case "Huawei": router = new HuaweiRouter(); break;
        }
        return router;
    }
}

这样看来确实简化了不少,直接省去了很多类!我们只需要按需预设好brand,我们调用静态方法就可以获取我们所需要的产品了!但是存在一些问题:

  • 简单工厂普遍存在的问题:可能由于brand设置错误而得到空对象!
  • 当我们添加新的产品族时,以前抽象工厂的做法只需要添加一个工厂实现类就可以了,这种做法需要重写switch,添加新的分支!

5.2、使用反射进行优化

解决问题的方法就是:不要在代码逻辑中通过字符串来判断然后决定new什么对象,我们只传入一个字符串,让工厂自己去找我们要实例化的类是哪一个!我们不进行人为干预!

说回JDBC中创建工厂Driver时,我们传入就是一个字符串com.mysql.cj.jdbc.Driver,然后利用反射加载了这个类然后完成工厂创建和注册。这里我们直接一点!直接使用反射创建对象!代替工厂创建产品!

public class ReflectFactory {
    private static String basePackage = "com.sakura.factory.abstraction.";
    private static String brand = "Xiaomi";

    public static Phone getPhone() throws Exception {
        String className = basePackage + brand + "Phone";
        return (Phone) Class.forName(className)
                .getDeclaredConstructor()
                .newInstance();
    }

    public static Laptop getLaptop() throws Exception {
        String className = basePackage + brand + "Laptop";
        return (Laptop) Class.forName(className)
                .getDeclaredConstructor()
                .newInstance();
    }

    public static Router getRouter() throws Exception {
        String className = basePackage + brand + "Router";
        return (Router) Class.forName(className)
                .getDeclaredConstructor()
                .newInstance();
    }
}

嘿嘿嘿!!如果我们的包结构能够进一步优化,可以完全使用一个String变量就能完成className的指定!

通过使用反射进行优化以后,当我们想要增加新的产品族时,也不需要修改此工厂代码了!反射会定位到对应的类进行加载!其实也就是大名鼎鼎的Spring框架中利用IoC容器进行“依赖注入”实现的基础理论!

现在还遗留一个问题:brand是我们写死的,目前只能手动修改!

5.3、反射+配置文件

在Spring中,我们使用.xml文件进行配置,Spring底层会对配置文件进行解析,然后获取配置信息。那么案例中brand我们也可以使用配置文件来解决!目前.properties.xml.yaml文件我们都是可以利用Java程序进行解析的!brand我们直接从配置文件解析使用即可!

这样我们就极大降低了工厂、用户的的耦合度!

  • 用户只需要按需写好配置文件即可,无需关心产品如何创建。
  • 工厂只需要专注于生成产品,不需要知道生产的产品在哪使用、被谁使用。

5.4、总结

“在所有使用简单工厂模式的地方,都可以使用反射技术来去除if-else / switch,以解除分判断带来的耦合!!“

posted @ 2022-01-21 20:33  5akura  阅读(109)  评论(0编辑  收藏  举报