深入理解设计模式-创建型之工厂模式

  • 一般情况下,工厂模式分为三种更加细分的类型:简单工厂、工厂方法和抽象工厂。
  • 工厂模式只被分成了工厂方法和抽象工厂两类。实际上,前面一种分类方法更加常见,所以,在今天的讲解中,我们沿用第一种分类方法。
  • 在这三种细分的工厂模式中,简单工厂、工厂方法原理比较简单,在实际的项目中也比较常用。而抽象工厂的原理稍微复杂点,在实际的项目中相对也不常用。

一、如何实现工厂模式

1、简单工厂(Simple Factory)案例

  • 简单工厂叫作静态工厂方法模式(Static Factory Method Pattern)。学习此设计模式时,我们会从一个案例不断优化带着大家领略工厂设计模式的魅力。
  • 现在有一个场景,我们需要一个资源加载器,他要根据不用的url进行资源加载,但是如果我们将所有的加载实现代码全部封装在了一个load方法中,就会导致一个类很大,同时扩展性也非常差,当想要添加新的前缀解析其他类型的url时,发现需要修改大量的源代码,我们的代码如下:
public class ResourceLoader {
   
    public Resource load(String filePath) {
        String prefix = getResourcePrefix(filePath);
 Resource resource = null;
        if("http".equals(type)){
            // ..发起请求下载资源... 可能很复杂

            return new Resource(url);
       } else if ("file".equals(type)) {
            // ..建立流,做异常处理等等

            return new Resource(url);
       } else if ("classpath".equals(type)) {
            // ...

            return new Resource(url);
       } else {
            return new Resource("default");
       }
        return resource;
   }
    
    private String getPrefix(String url) {
        if(url == null || "".equals(url) || !url.contains(":")){
            throw new ResourceLoadException("此资源url不合法.");
       }
        String[] split = url.split(":");
        return split[0];
   }
}

在上边的案例中,存在很多的if分支,如果分支数量不多,且不需要扩展,这样的编写方式当然没错,然而在实际的工作场景中,我们的业务代码可能会很多,分支逻辑也可能十分复杂,这个时候简单工厂设计模式就要发挥作用了。
我们可以看到不管有多少个分支逻辑,他的本质就是一个,创造一个资源产品,我们只需要创建一个工厂类,将创建资源的能力交给工厂(不管其中如何实现)即可:

public class ResourceFactory {

    public static Resource create(String type,String url){
        if("http".equals(type)){
            // ..发起请求下载资源... 可能很复杂

            return new Resource(url);
       } else if ("file".equals(type)) {
            // ..建立流,做异常处理等等

            return new Resource(url);
       } else if ("classpath".equals(type)) {
            // ...

            return new Resource(url);
       } else {
            return new Resource("default");
       }
   }
}

有了上边的工厂类,我们将【创建资源产品】这个单一的能力赋予产品工厂,这样能更好的符合单一原则。有了工厂之后,我们的主要逻辑就会简化:

public class ResourceLoader {
    public Resource load(String url){
        // 1、根据url获取前缀

        String prefix = getPrefix(url);
        // 2、根据前缀处理不同的资源

        return ResourceFactory.create(prefix,url);
   }

1.1简单工厂好处与缺陷

这就是简单工厂设计模式,提取一个工厂类,工厂会根据传入的不同的类型,创建不同的产品,好处如下:将创建对象的过程交给工厂类、其他业务需要某个产品时,直接使用create(方法名字不重要)创建即可,这样的好处是:

1、工厂将创建的过程进行封装,不需要关系创建的细节,更加符合面向对象思想

2、这样主要的业务逻辑不会被创建对象的代码干扰,代码更易阅读

3、产品的创建可以独立测试,更将容易测试

4、独立的工厂类只负责创建产品,更加符合单一原则

q:但是有的人会问,如果需要修改或者添加新的功能,我们还是要修改源代码呀,这不符合开闭原则呀?

  • 确实如此,但是原则这种东西,一定要结合业务创建,在创建对象的过程相对简单,业务改动不是很频繁的情况下,适当的不按原则出牌才是更好的选择,只是偶尔修改一下 ResourceLoaderFactory代码,稍微不符合开闭原则,也是完全可以接受的。
  • 因为这样可以更加简单的编码,在进行软件开发时编码难度也是一个很重要的考量标准。我们一定要在合理设计和过度设计之间进行权衡,明白一点,适合的才是最好的。
  • 绝大部分工厂类都是以“Factory”单词结尾,但也不是必须的,比如 Java 中的 DateFormat、Calender。除此之外,工厂类中创建对象的方法一般都是 create 开头,比如代码中的 createParser(),但有的也命名为 getInstance()、createInstance()、newInstance(),有的甚至命名为 valueOf()(比如 Java String 类的 valueOf() 函数)等等,这个我们根据具体的场景和习惯来命名就好。

2、工厂方法(Factory Method)

  • 如果有一天,我们的if分支逻辑不断膨胀,有变为肿瘤代码的可能,就有必要将 if 分支逻辑去掉,那又该怎么办呢?比较经典的处理方法就是利用多态。按照多态的实现思路,对上面的代码进行重构。我们会为每一个 Resource 创建一个独立的工厂类,形成一个个小作坊,将每一个实例的创建过程交给工厂类完成,重构之后的代码如下所示:
  • 之前(简单工厂)是一个大而全的工厂,一个工厂需要创建不同的产品工厂方法讲究的是工厂也要专而精,一个工厂只创建一种资源(产品),奔驰工厂只负责生产奔驰,宝马工厂只负责生产宝马。
  • 回到我们的例子中,每一种url加载成不同的资源产品,那每一种资源都可以由一个独立的ResourceFactory生产,在这个案例中我们觉得
    ResourceLoader这个名字更加合适。为了实现这一种场景,我们需要将生产资源的工厂类进行抽象:
public interface IResourceLoader {
    Resource load(String url);
}

并为每一种资源创建与之匹配的实现:实现接口的实现类

public class ClassPathResourceLoader implements IResourceLoader {
    @Override

    public Resource load(String url) {
        // 中间省略复杂的创建过程

        return new Resource(url);
   }
}
}
public class FileResourceLoader implements IResourceLoader {
    @Override

    public Resource load(String url) {
        // 中间省略复杂的创建过程

        return new Resource(url);
   }
}
public class HttpResourceLoader implements IResourceLoader {
    @Override

    public Resource load(String url) {
        // 中间省略复杂的创建过程

        return new Resource(url);
   }

实际上,这就是工厂方法模式的典型代码实现。这样当我们新增一种读取资源的方式时,只需要新增一个实现,并实现 IResourceLoader 接口即可。所以,工厂方法模式比起简单工厂模式更加符合开闭原则。
当然有人就会说了这有什么用能呢,到时候使用的时候还不是需要如下的方式吗?这个工厂不就是来添乱的吗?

public class ResourceLoader {
    public Resource load(String url){
        // 1、根据url获取前缀

        String prefix = getPrefix(url);
        ResourceLoader resourceLoader = null;
        // 2、根据前缀选择不同的工厂,生产独自的产品

        // 版本一
        if("http".equals(prefix)){
            resourceLoader = new HttpResourceLoader();
       } else if ("file".equals(prefix)) {
            resourceLoader = new FileResourceLoader();
       } else if ("classpath".equals(prefix)) {
            resourceLoader = new ClassPathResourceLoader()
       } else {
            resourceLoader = new DefaultResourceLoader();
       }
        return resourceLoader.load();   
   }

不要急,我们为每个产品引入了工厂,却发现需要为创建工厂这个行为付出代价,在创建工厂这件事上,仍然不符合开闭原则,为了解决上述的问题我们又不得不去创建一个工厂的缓存来统一管理工厂实例,以后使用工厂会更加的简单,代码如下:

private static Map<String,IResourceLoader> resourceLoaderCache = new HashMap<>(8);

// 版本二

static {
    resourceLoaderCache.put("http",new HttpResourceLoader());
    resourceLoaderCache.put("file",new FileResourceLoader());
    resourceLoaderCache.put("classpath",new ClassPathResourceLoader());
    resourceLoaderCache.put("default",new DefaultResourceLoader());
}

事实上,ResourceLoader的核心方法就可以简化成这个样子了:

public class ResourceLoader {
public Resource load(String url){
    // 1、根据url获取前缀

    String prefix = getPrefix(url);
    return resourceLoaderCache.get(prefix).load(url);
}

当然你如果觉得还是不够,你觉得修改需求还是不够灵活,仍然需要修改static中的代码,我们可以这样做,搞一个配置文件如下,将我们的工厂类进行配置,如下:

http=com.ydlclass.factoryMethod.resourceFactory.impl.HttpResourceLoader

file=com.ydlclass.factoryMethod.resourceFactory.impl.FileResourceLoader

classpath=com.ydlclass.factoryMethod.resourceFactory.impl.ClassPathResource
Loader

default=com.ydlclass.factoryMethod.resourceFactory.impl.DefaultResourceLoad
er

这样我们可以在static中这样编写代码,让我完全满足开闭原则:

public class ResourceLoader {
static {
		//读取配置文件
    InputStream inputStream = Thread.currentThread().getContextClassLoader()
       .getResourceAsStream("resourceLoader.properties");
    Properties properties = new Properties();
    try {
        properties.load(inputStream);
        //循环遍历配置文件数据 加入到map中
        for (Map.Entry<Object,Object> entry : properties.entrySet()){
            String key = entry.getKey().toString();
            Class<?> clazz = Class.forName(entry.getValue().toString());
            IResourceLoader loader = (IResourceLoader) 
clazz.getConstructor().newInstance();
            resourceLoaderCache.put(key,loader);//加入到map中
       }
   } catch (IOException | ClassNotFoundException | NoSuchMethodException | 

InstantiationException |

             IllegalAccessException | InvocationTargetException e) {
        throw new RuntimeException(e);
   }
}

public Resource load(String url){
    // 1、根据url获取前缀

    String prefix = getPrefix(url);
    return resourceLoaderCache.get(prefix).load(url);//根据不同的前缀从map中取不同的实现类(工厂)
}

以后我们想新增或删除一个resourceLoader只需要写一个类实现IResourceLoader接口,并且在配置文件中进行配置即可。此时此刻我们已经看不到if-else的影子了。

2.1 产品抽象

我们的代码中产品是简单单一的类,事实上,在工作中,我们的产品可能是及其复杂的,我们同样需要对整个产品线进行抽象

public abstract class AbstractResource {
    private String url;
    public AbstractResource(){}

    public AbstractResource(String url) {
        this.url = url;
   }
    protected void shared(){
        System.out.println("这是共享方法");
   }
    /**

     * 每个子类需要独自实现的方法

     * @return 字节流

     */

    public abstract InputStream getInputStream();
}

具体的产品需要继承这个抽象类:

产品类:
public class ClasspathResource extends AbstractResource {
    public ClasspathResource() {
   }
    public ClasspathResource(String url) {
        super(url);
   }
    @Override

    public InputStream getInputStream() {
        return null;
   }
}

其他产品同理,我们的工厂类也需要面向产品的抽象进行编程了:

工厂类:
public class ClassPathResourceLoader implements IResourceLoader {
    @Override
    public AbstractResource load(String url) {
        // 中间省略复杂的创建过程
 AbstractResource classpathResource = new ClasspathResource(url)
        return classpathResource;
   }
}

我们编写测试用例进行测试:

@Test
public void testFactoryMethod(){
    String url = "Classpath://D://a.txt";
    ResourceLoader resourceLoader = new ResourceLoader();//核心类 用于调用load方法,通过load方法返回不同类型的工厂
    AbstractResource resource = resourceLoader.load(url); //产品抽象类(也可以是接口)AbstractResource,依赖于抽象而不依赖于具体的实现!!
    		// resourceLoader.load(url)它会根据URL前缀类型,走不同的工厂类,工厂类的方法再返回和类型对应的产品对象
    		//代码层面没有看到具体哪一种类型的工厂类和产品类,
    		//只有产品接口的类型和一个用于加载工厂的核心类ResourceLoader  体会依赖于抽象而不依赖于具体的实现!!
    log.info("resource --> {}",resource.getClass().getName());//返回的是具体类型的产品类ClasspathResource
}

简单工厂、工厂方法和抽象工厂之间的区别

  • 简单工厂模式(Simple Factory Pattern):

    • 简单工厂模式是工厂模式的基础形式,它通过一个工厂类来创建对象,将对象的创建过程封装在工厂方法中,客户端只需要通过工厂方法来获取所需的对象,而无需关心对象的创建细节。简单工厂模式适用于对象的创建过程相对简单,且只有少数几种可能的情况。
  • 工厂方法模式(Factory Method Pattern):

    • 工厂方法模式扩展了简单工厂模式的概念,它将对象的创建过程委托给子类的工厂方法每个具体子类工厂都实现了一个工厂方法,用于创建特定类型的对象。这样,客户端可以通过调用不同的工厂方法来获取所需的对象,从而实现了更高层次的灵活性。工厂方法模式适用于对象的种类较多,需要支持扩展和变化的情况。
  • 抽象工厂模式(Abstract Factory Pattern):

    • 抽象工厂模式进一步扩展了工厂方法模式,它提供了一种创建一系列相关或依赖对象的接口,而不需要指定具体的类。抽象工厂模式的关键在于定义一组抽象工厂接口,每个具体工厂类都实现了这些接口,用于创建一族相关的产品。这种模式适用于需要创建多个相关对象组合的情况,如创建不同操作系统下的界面元素。

总结:

  • 简单工厂模式适用于简单的对象创建情况,但缺乏灵活性。
  • 工厂方法模式提供了更高的灵活性,每个子类工厂负责创建特定类型的对象。
  • 抽象工厂模式适用于创建一组相关的对象,它通过提供一组抽象工厂接口来实现。

源码应用

1、jdk种的使用

1)Calendar

jdk中的日历类可以根据时区、地点创建一个满足当时需求的日历实例,这就是一个简单工厂:

2)DateFormat

DateFormat同样可以根据类型和地域生成一个满足本地特色的Date格式化工具:

DateFormat dateInstance = DateFormat.getDateInstance(DateFormat.FULL, Locale.CHINA);

log.info("date-->{}", dateInstance.format(new Date()));

2、spring

2.1典型的简单工厂

spring中的bean工厂就是一个典型的简单工厂设计模式:

beanFactory.getBean("userService");

2.2典型的工厂方法

FactoryBean提供了三个方法,其中getObject就是一个典型的工厂方法,

FactoryBean定制bean的创建过程,我们将工厂bean注入容器,有容器统一管理工厂对象,再有工厂对象创建具体的bean。

public interface FactoryBean<T> {
 @Nullable

 T getObject() throws Exception;
 Class<?> getObjectType();
 default boolean isSingleton() {
 return true;
 }
}

Spring–BeanFactory和FactoryBean区别

3、mybatis

mybatis中有很多Factory结尾的类,也是使用工厂设计模式如:

(1)SqlSessionFactory

public interface SqlSessionFactory {
    SqlSession openSession();
    SqlSession openSession(boolean autoCommit);
    SqlSession openSession(Connection connection);
    SqlSession openSession(TransactionIsolationLevel level);
    SqlSession openSession(ExecutorType execType);
    SqlSession openSession(ExecutorType execType, boolean autoCommit);
    SqlSession openSession(ExecutorType execType, TransactionIsolationLevel 

level);
    SqlSession openSession(ExecutorType execType, Connection connection);
    Configuration getConfiguration();
}

(2)MapperProxyFactory

该类可以根据接口类型生成对应的具体实现,也是一种代理,核心方法

newInstance:

@SuppressWarnings("unchecked")
    protected T newInstance(MapperProxy<T> mapperProxy) {
        return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new 

Class[] { mapperInterface }, mapperProxy);
   }
    public T newInstance(SqlSession sqlSession) {
        final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, 

mapperInterface, methodCache);
        return newInstance(mapperProxy);
   }

应用场景

假设您正在开发一个支持多种数据库(例如 MySQL、PostgreSQL、Oracle 等)的应用程序。根据配置文件或用户选择的数据库类型,您需要创建相应类型的数据库连接。这是一个工厂模式可以发挥作用的场景。首先,定义一个数据库连接接口:

public interface DatabaseConnection {
   Connection getConnection();
}

然后,实现多种数据库连接类型:

public class MySQLConnection implements DatabaseConnection {
   @Override
   public Connection getConnection() {
       // 实现 MySQL 连接的创建逻辑

   }
}
public class PostgreSQLConnection implements DatabaseConnection {
   @Override
   public Connection getConnection() {
       // 实现 PostgreSQL 连接的创建逻辑

   }
}
public class OracleConnection implements DatabaseConnection {
   @Override
   public Connection getConnection() {
       // 实现 Oracle 连接的创建逻辑

   }
}

接下来,创建一个工厂类,用于根据数据库类型创建相应的数据库连接实例:

public class DatabaseConnectionFactory {
   public static DatabaseConnection createDatabaseConnection(String 
databaseType) {
       if (databaseType == null) {
           throw new IllegalArgumentException("Database type cannot be null.");
       }
       if (databaseType.equalsIgnoreCase("MySQL")) {
           return new MySQLConnection();
       } else if (databaseType.equalsIgnoreCase("PostgreSQL")) {
           return new PostgreSQLConnection();
       } else if (databaseType.equalsIgnoreCase("Oracle")) {
       return new OracleConnection();
       } else {
           throw new IllegalArgumentException("Invalid database type: " + 
databaseType);
       }
   }
}

现在,您可以使用工厂类根据配置或用户选择创建相应的数据库连接实例:

DatabaseConnection connection = 
DatabaseConnectionFactory.createDatabaseConnection("MySQL");
Connection conn = connection.getConnection();

通过工厂设计模式,您可以轻松地在运行时根据需要创建不同类型的数据库连接,提高代码的可扩展性和灵活性。

总结

当创建逻辑比较复杂,是一个“大工程”的时候,我们就应该考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。
何为创建逻辑比较复杂呢?我总结了下面两种情况。

  • 第一种情况:类似规则配置解析的例子,代码中存在 if-else 分支判断,动态地根据不同的类型创建不同的对象。针对这种情况,我们就考虑使用工厂模式,将这一大坨 if-else 创建对象的代码抽离出来,放到工厂类中。
  • 还有一种情况,尽管我们不需要根据不同的类型创建不同的对象,但是,单个对象本身的创建过程比较复杂,比如前面提到的要组合其他类对象,做各种初始化操作。在这种情况下,我们也可以考虑使用工厂模式,将对象的创建过程封装到工厂类中。
  • 对于第一种情况,当每个对象的创建逻辑都比较简单的时候,我推荐使用简单工厂模式,将多个对象的创建逻辑放到一个工厂类中。当每个对象的创建逻辑都比较复杂的时候,为了避免设计一个过于庞大的简单工厂类,我推荐使用工厂方法模式,将创建逻辑拆分得更细,每个对象的创建逻辑独立到各自的工厂类中。同理,对于第二种情况,因为单个对象本身的创建逻辑就比较复杂,所以,我建议使用工厂方法模式。除了刚刚提到的这几种情况之外,如果创建对象的逻辑并不复杂,那我们就直接通过 new 来创建对象就可以了,不需要使用工厂模式。
  • 现在,我们上升一个思维层面来看工厂模式,它的作用无外乎下面这四个。这也是判断要不要使用工厂模式的最本质的参考标准。
    • 封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明。
    • 代码复用:创建代码抽离到独立的工厂类之后可以复用。
    • 隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象。
    • 控制复杂度:将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁。

日常工作中很多场景都可以使用工厂设计模式,如使用不同的支付方式支付,使用不同的登录器登录等等。

你可能感兴趣的:(设计模式,设计模式)