从Vertx的日志框架体会委派模式(Delegate)、策略模式(Strategy)和工厂模式(Factory)的结合

基本背景

假如你现在要写一个全新的框架,那这个框架里面必然需要提供一种打印日志的功能。但是业界其实有很多种优秀的日志框架了,你还需要去新写吗?肯定没有必要了。肯定最好选择其中一个就行了,但是日志框架很多,之间的性能差异又不是特别大,固定使用一种也可能不合适。所以需要写一个日志框架,并且这个框架是依赖于已有的框架,但不是固定的。具体的由开发者指定,并且能够由开发者去扩展。那究竟要怎么做呢?从vert.x的日志框架中学习一下。

总述

使用vertx一段时间,其实没有用过它的日志框架,都使用的是slf4j的实现logback。这几天无意中看了一下,我还以为它的日志扩展做了什么特殊处理,实际上什么都没有做,实现其实挺简单的,它把日志的所有实现全部委托给一个日志委托接口,同时自己实现了一些日志框架,比如jullog,log4j,log4j2,slf4j,目前看见实现了这4种。在运行阶段,只能选择其中一个,默认是java的日志框架。并且它采用工厂模式对外提供了一种SPI,允许用户进一步实现其它的日志框架的委托,进而应用在vertx实例中。

整体介绍

4个类的类图,就能够说明白它的设计方案:
在这里插入图片描述
先说一下这4个类的职责:

  1. Logger:日志类,对外暴露的API。内部依赖了一个日志委托对象LogDelegate。所有的操作都由委托对象去执行。同时这个对象本身由日志工厂对象LoggerFactory来创建。
  2. LoggerFactory:日志工厂对象;用于创建日志对象,内部依赖了一个日志委托工厂的实例,并且是静态的。委托工厂创建被委托对象,
  3. LogDelegate:日志委托接口;它定义了所有的日志操作方法,它的实现类中可以依赖于具体的日志框架,真正去实现。它由日志委托工厂LogDelegateFactory来创建。
  4. LogDelegateFactory:日志委托工厂接口;负责创建一个真正的委托对象。
    对于开发者来说,vertx提供的获取日志对象的方式和其它的框架是一模一样的
Logger log = LoggerFactory.getLogger(Test.class.getName());

在LoggerFactory类里面依赖了一个日志委托接口LogDelegateFactory,类在加载的时候会初始化,通过一个系统变量来指定这个委托工厂的实现类,然后并实例化。

  static {
    initialise();
  }

  public static synchronized void initialise() {
    LogDelegateFactory delegateFactory;
    //这边是默认的实现。
    String className = JULLogDelegateFactory.class.getName();
    try {
    //取出系统变量。
      className = System.getProperty(LOGGER_DELEGATE_FACTORY_CLASS_NAME);
    } catch (Exception e) {
    }
    if (className != null) {
      ClassLoader loader = Thread.currentThread().getContextClassLoader();
      try {
        Class<?> clz = loader.loadClass(className);
        delegateFactory = (LogDelegateFactory) clz.newInstance();
      } catch (Exception e) {
        throw new IllegalArgumentException("Error instantiating transformer class \"" + className + "\"", e);
      }
    } else {
      delegateFactory = new JULLogDelegateFactory();
    }

    LoggerFactory.delegateFactory = delegateFactory;
  }

之后就Logger对象的创建,就会使用日志委托工厂创建一个日志委托对象,封装一下。并缓存在map中

  public static Logger getLogger(final String name) {
    Logger logger = loggers.get(name);
    if (logger == null) {
      LogDelegate delegate = delegateFactory.createDelegate(name);
      logger = new Logger(delegate);
      Logger oldLogger = loggers.putIfAbsent(name, logger);

      if (oldLogger != null) {
        logger = oldLogger;
      }
    }
    return logger;
  }

看一下详细的类图:
在这里插入图片描述
这个图里面可以看见日志委托接口和日志委托工厂有多种实现,两者也是一一对应的。
其实到这边就结束了,不过我更感兴趣的是它的设计过程,虽然看起来真的很简单。

设计初衷是什么

设计这个的人的初衷应该有如下需求:

  1. 我要新写一个日志API,但我不想实现新的。
  2. 我想把主流的日志框架都整合进来,运行的时候用其中一个就行了。
  3. 具体用哪个的话,需要由开发者去指定,并且开发者可以扩展。

其实第一个点,我个人稍微纠结了一点点,为啥非得新写一个?vertx不提供日志API不行吗?反正主流的有那么多,开发者爱用哪个就去用,完全不管了(比如本人一直就没用他的)。他们可能觉得没有日志API,不算一个完整的框架吧?或许是这个吧。因为虽然写了个日志API,但是内部什么都没有做。

接下来看一下2和3需求,就把他们拆成两部分,要好好地品味其中的设计。

扫描二维码关注公众号,回复: 11382203 查看本文章

看一下纯粹的委派

假如说,直接委派给SL4J,也是可以的,类图也会变得比较简单:
在这里插入图片描述
(图中少了一个线,不过不影响说明)

这个是完整的一个委派模式;两种角色,委派者(Logger)和被委派者(SLF4JLogDelegate)。
委派者对外提供能力,外界不知道实际的执行者。并且它们之间没有太复杂的关系,仅仅是委派者依赖了被委派者而已。如果从这个角度来讲,这不就是常见的组合吗?这不就是面向对象的封装吗?太常见了吧。这或许是它不在23种设计中的理由吧。实在是有点儿太笼统了。这一点也就是我们常说的,多使用组合而不是继承,它能够解决这个问题。

但是总觉得两种角色之间应该有某种关系,当然是功能上的一种关系。我印象比较深的是,spring整合了非常多的框架,比如jpa,rabbitmq,quartz等等。使用的时候,直接引用spring的API即可,但是实际上它的内部实现都是委派给了本身的框架实现,它只是在外层封装了一层,并且是一种增强,方便我们去使用。但有的情况下,也有可能会削弱吧~
从这个角度来看,被委派的对象应该是独立且完整的一部分,它本身已经能够提供完整的功能。而委派者只是对其进行了封装,说到底它还是封装。

升级到策略模式

我们再升级一下,假如委派者把任务委派给的是一个被委派接口(LogDelegate),类图如下:
在这里插入图片描述
显然这完全就是策略模式的类图呀,Logger依赖于日志策略,策略可以有多种实现,至于用哪一个自己去指定即可。
但是它还是委派模式吗?还是呀。和上面的没有变化。但同时又是策略模式呀?这怎么解释?从这个角度出发,我的结论就出来了。

根本就没有什么委派(Delegate)模式

写这篇文章的时候,网上也查了很久,不少人也解释过委派模式是什么,甚至和代理模式以及策略模式各种比较以及各自的侧重点,把我看得云里雾里的。最后我采用了一种简化的思考方式,把复杂的问题简单化,然后一步步升级到复杂的情况。就能想明白一些事情。
而且在这个例子里面,也可以把它升级成代理模式。会得到同样的结论,委派的思想仍然存在。
结论就是:根本不存在委派模式。它仅仅只是面向对象中的封装特性带给我们带来的一种编程的方式,它特别的抽象和笼统,根本就没有模式可见,而且确实23种设计模式中就没有。它的本质就是封装

再加一个工厂方法模式吧

还有另外一个需求:具体用哪个的话,需要由开发者去指定,并且开发者可以扩展。
在策略模式之上,我们所依赖的具体策略是动态的,那么这是实例的创建,显然交给工厂来做是最合适的。一个具体的日志策略由一个具体工厂去创建,这个具体工厂由开发者来指定,我们只需要指定配置方式然后实例化一下工厂即可。这就是采用的工厂方法模式了。
话说让开发者来指定一下具体的日志策略行不行?这样也就不需要工厂这个角色了。或许可行,但我现在唯一觉得不行的就是,具体对象如何来创建,我们或许不知道也没有办法干预,毕竟接口上面没有定义构造器,所以对象的创建还是交给工厂吧,这也是它的职责。
采用工厂方法以后,就实现了日志框架的扩展性。

关于这种设计的思考

不管是策略呀还是工厂模式,其实很久之前就已经学习过了,不过这次又重温了一次,也多多少少有一些收获,学习设计模式,一定要理解它的应用场景,用心去体会。
再回顾一下之前的3个需求:

  1. 我要新写一个日志API,但我不想实现新的。
  2. 我想把主流的日志框架都整合进来,运行的时候用其中一个就行了。
  3. 具体用哪个的话,需要由开发者去指定,并且开发者可以扩展。

他们分别的解决方案是:

  1. 用委派思想(封装)。依赖一个具体的日志框架即可。
  2. 依赖日志策略接口,采用策略模式。
  3. 采用工厂方法模式,具体用哪一个由开发者指定。

关于委派再说一句:没有委派模式。它就是封装,遍布在代码的角角落落。
思考问题的方式很重要,有时候想不明白了,换个思路也许会想明白。

猜你喜欢

转载自blog.csdn.net/ywg_1994/article/details/103486067