外观模式

731 阅读9分钟

介绍

外观模式又叫门面模式,在开发过程中的运用频率非常高,尤其是在现阶段各种第三方库充斥在我们的周边,而这些库很大概率会使用外观模式。通过一个外观类使得整个系统的接口只有一个统一的高层接口,这样能够降低用户的使用成本,也对用户屏蔽了很多实现细节。外观模式允许我们让客户与子系统之间避免紧耦合。

概念

要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。此模式提供一个高层次的接口,使得子系统更易于使用。

外观模式结构图:

Facade:系统对外的统一接口「操控系统内部的工作」

SubSystemA、SubSystemB、SubSystemC、SubSystemD:子系统接口「实现部分未给出」。

外观模式接口比较简单,就是通过一个统一的接口对外提供服务,使得外部程序只通过一个类就可以实现系统内部的多种功能,而这些实现功能的内部子系统之间可能也有交互,或者说完成一个功能需要几个子系统之间进行协作,如果没有封装,那么用户就需要操作几个子系统的交互逻辑,容易出现错误。而通过外观类来对外屏蔽这些复杂的交互,降低用户的使用成本。如下所示:

模式实现

生活中使用外观模式的例子非常多,任何一个类似与中央空调结构的组织都类似外观模式。举个简单的例子,手机就是一个外观模式的例子。它集合了电话功能、短信功能、GPS、拍照于一身,通过手机你就可以完成各种功能。而不是你打电话时就使用一个诺基亚110,拍照时非得用一个相机,如果是这样每使用一个功能就必须操纵特定的设备,会使得整个过程很繁琐。而手机给了你一个统一的入口,集电话、上网、拍照等功能于一身,使用方便,操作简单。

MobilePhone类:

public class MobilePhone {
    private Phone phone = new PhoneImpl();
    private Camera camera = new CameraImpl();

    public void dial(){
        phone.dial();
    }

    public void hangUp(){
        phone.hangup();
    }

    public void videoChat(){
        camera.open();
        phone.dial();
    }

    public void takePicture() {
        camera.open();
        camera.takePicture();
    }

    public void closeCamera(){
        camera.close();
    }
}

Phone接口与Camera接口

public interface Phone {

    /**
     * 打电话
     */
    void dial();

    /**
     * 挂断电话
     */
    void hangup();
}
public interface Camera {

    /**
     * 打开相机
     */
    void open();

    /**
     * 拍照
     */
    void takePicture();

    /**
     * 关闭相机
     */
    void close();
}

PhoneImpl类与CameraImpl类

public class PhoneImpl implements Phone{

    @Override
    public void dial() {
        System.out.println("打电话");
    }

    @Override
    public void hangup() {
        System.out.println("挂断电话");
    }
}
public class CameraImpl implements Camera{

    @Override
    public void open() {
        System.out.println("打开相机");
    }

    @Override
    public void takePicture() {
        System.out.println("拍照");
    }

    @Override
    public void close() {
        System.out.println("关闭相机");
    }
}

客户端测试类:

public class Test {

    public static void main(String[] args) {
        MobilePhone mobilePhone = new MobilePhone();
        //拍照
        mobilePhone.takePicture();
        //视频聊天
        mobilePhone.videoChat();
    }
}

UML类图:

MobilePhone类含有两个子系统,也就是拨号系统和拍照系统。MobilePhone将这两个系统封装起来,为用户通过统一的操作接口,也就是说用户通过MobilePhone这个类可以操作打电话和拍照这两个功能。用户不需要知道有Phone这个接口和它的实现类PhoneImpl,同样也不需要Camera的相关信息,通过MobilePhone就可以包揽一切。

在MobilePhone中也封装了两个子系统的交互,例如视频电话时需要打开摄像头,如果没有这一步的封装,每次用户使用视频电话功能时都需要手动打开摄像头、进行拨号,这样会增加用户的使用成本,外观模式使得这些操作更加简单、易用。

优点

  • 对客户程序隐藏子系统细节,因而减少了客户对于子系统的耦合,符合“迪米特原则”。
  • 对子系统接口封装,使得系统更加易于使用。

缺点

  • 外观类接口膨胀。由于子系统的接口都有外观类统一暴露,使得外观类的API的接口较多。
  • 外观类没有遵循“开闭原则”,当业务变化时,可能需要直接修改外观类。

适用场景

  • 为一个复杂子系统提供一个简单接口。子系统往往因为不断演化而变得越来越复杂,甚至可能被替换。大多数模式使用时都会产生更多、更小的类,这使子系统更具可重用性的同时也更容易对子系统进行定制、修改,这种易变性使得隐藏子系统的具体实现变得尤为重要。Facade可以提供一个简单统一的接口,对外隐藏子系统的具体实现,隔离变化。
  • 当你需要构建一个层次结构子系统时,使用Facade模式定义子系统中每层入口点。如果子系统之间是相互依赖的,你可以让它们仅通过Facade接口进行通信,从而简化了它们之间的依赖关系。
  • 在维护一个遗留的大型系统时,可能这个系统已经非常难以维护和扩展了,但因为它包含非常重要的功能,新的需求必须依赖于它。此时用外观模式Facade也是非常合适的。你可以为新系统开发一个外观Facade类,来为粗糙或高度复杂的遗留代码设计比较清晰的简单接口,让新系统于Facade对象交互,Facade在与遗留代码交互所有的复杂工作。

总结

外观模式就是统一接口封装,将子系统的逻辑、交互隐藏起来,为用户提供一个高层次的接口,使得系统更加易用,同时也隐藏了具体的实现,这样即使具体的子系统发生了变化,用户也不会感知到,因为用户使用的是Facade高层接口,内部的变化对于用户来说并不可见。这样一来就将变化隔离开来,使得系统更加灵活。

外观模式是一个高频率使用的设计模式,它的精髓就在于“封装”二字。通过一个高层次结构为用户提供统一的API入口,使得用户通过一个类就基本能操作整个系统,这样就减少了用户的使用成本。

源码分析(log4j-slf4j-impl 2.13.1;logback 1.2.3)

阿里巴巴开发手册中有这样一条规定:

其中Log4j、Logback都是日志框架,它们都有着自己的独立的Api接口。如果单独使用某个框架,会大大增加系统的耦合性。而SLF4J并不是真正的日志框架,它有一套通用的API接口。所以阿里开发手册中直接强制用SLF4J日志门面,日志门面是门面模式的一个典型应用。 我们来看一下日志系统:

应用程序相当于客户端,抽象层SLF4J相当于门面,只提供接口api,不提供实现,而logback直接实现了Slf4j的api,所以不需要适配层,logback是对log4j的完善并进行了优化,和slf4j都是出自一个作者,而其它日志框架,想要能够被slf4j调用,就得需要一个适配层了,为什么呢?因为他们本身内部并没有对slf4j的实现,需要一个适配层来完成这项操作(可以理解为适配层将其他日志框架绑定到了slf4j提供的api上,已完成可以被slf4j调用的目的)。

我们以Log4j2为例:

Logger logger = LoggerFactory.getLogger(Test.class);

org.slf4j.LoggerFactory类:

public final class LoggerFactory {
    //省略无关代码...
    static final int ONGOING_INITIALIZATION = 1;
    static final int UNINITIALIZED = 0;
    //volatile关键字禁止指令重排序
    static volatile int INITIALIZATION_STATE = UNINITIALIZED;
   
    public static Logger getLogger(Class<?> clazz) {
        Logger logger = getLogger(clazz.getName());
        //省略...
        return logger;
    }
    public static Logger getLogger(String name) {
        //获得具体的LoggerFactory工厂
        ILoggerFactory iLoggerFactory = getILoggerFactory();
        //调用工厂方法返回具体的Logger产品
        return iLoggerFactory.getLogger(name);
    }
    public static ILoggerFactory getILoggerFactory() {
        //判断自己的StaticLoggerBinder类来和SLF4J是否已经绑定过
        if (INITIALIZATION_STATE == UNINITIALIZED) {
            //加锁,避免多线程绑定多次
            synchronized (LoggerFactory.class) {
                //双重检查(DCL单例模式)
                if (INITIALIZATION_STATE == UNINITIALIZED) {
                    INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                    //在这里面完成自己的StaticLoggerBinder类来和SLF4J对接
                    performInitialization();
                }
            }
        }
        switch (INITIALIZATION_STATE) {
        case SUCCESSFUL_INITIALIZATION:
            //得到具体工厂(Log4jLoggerFactory)
            return StaticLoggerBinder.getSingleton().getLoggerFactory();
        throw new IllegalStateException("Unreachable code");
    }
    private final static void performInitialization() {
        bind();
    }
    private final static void bind() {
        //进行绑定操作
        //the next line does the binding
        StaticLoggerBinder.getSingleton();
    }
}

进入到org.slf4j.impl.StaticLoggerBinder:

public final class StaticLoggerBinder implements LoggerFactoryBinder {
    private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
    private final ILoggerFactory loggerFactory;
    
    private StaticLoggerBinder() {
        //创建具体工厂
        loggerFactory = new Log4jLoggerFactory();
    }
    
    public static StaticLoggerBinder getSingleton() {
        return SINGLETON;
    }
    
    @Override
    public ILoggerFactory getLoggerFactory() {
        //返回的是具体工厂(Log4jLoggerFactory)
        return loggerFactory;
    }
}

从具体工厂(Log4jLoggerFactory)调用getLogger(xxx)方法获得具体产品,来到父类(AbstractLoggerAdapter):

public abstract class AbstractLoggerAdapter<L> implements LoggerAdapter<L>, LoggerContextShutdownAware {
   
    @Override
    public L getLogger(final String name) {
        final LoggerContext context = getContext();
        final ConcurrentMap<String, L> loggers = getLoggersInContext(context);
        final L logger = loggers.get(name);
        if (logger != null) {
            return logger;
        }
        //创建具体产品,放入到ConcurrentMap容器中
        loggers.putIfAbsent(name, newLogger(name, context));
        return loggers.get(name);
    }
    protected abstract L newLogger(final String name, final LoggerContext context);
}

来到子类,通过Log4jLoggerFactory工厂生产具体产品(Log4jLogger):

public class Log4jLoggerFactory extends AbstractLoggerAdapter<Logger> implements ILoggerFactory {
    
    @Override
    protected Logger newLogger(final String name, final LoggerContext context) {
        final String key = Logger.ROOT_LOGGER_NAME.equals(name) ? LogManager.ROOT_LOGGER_NAME : name;
       
        //返回具体产品
        return new Log4jLogger(validateContext(context).getLogger(key), name);
    }
}

最后通过Log4jLogger具体产品类完成日志的打印。

  • 首先,通过slf4j-api.jar的Logger logger = LoggerFactory.getLogger(xxx);获取日志对象,作为日志的入口,可以通过logger.info()等打印日志。
  • slf4j-log4j12.jar作为适配器,提供了org.slf4j.impl.StaticLoggerBinder类,所以,当我们导入了log4j-slf4j.impl.jar后,slf4j-api.jar会直接调用log4j-slf4j-impl.jar中的org.slf4j.impl.StaticLoggerBinder类。
  • LoggerFactory类中调用了org.slf4j.impl.StaticLoggerBinder类的getLoggerFactory()方法获取日志工厂Log4jLoggerFactory,然后通过日志工厂的getLogger()方法返回Log4jLogger实例。关键就是org.slf4j.impl.StaticLoggerBinder(该类是接口LoggerFactoryBinder的实现类),虽然slf4j-api.jar中的LoggerFactory类使用了这个类,但这个类并不存在于slf4j-api.jar中,所以,只导入了slf4j-api.jar时,编译是会报错的,因为org.slf4j.impl.StaticLoggerBinder根本就不存在。
  • 具体的写日志操作,由Log4jLogger类来完成。
  • logback日志框架实现了slf4j,所以不需要额外的适配器就能与slf4j结合,logback-classic.jar里有org.slf4j.impl.StaticLoggerBinder类,所以,如果同时导入logback-classic.jar和long4j-slf4j-impl.jar会报错,因为不能确定使用哪个org.slf4j.impl.StaticLoggerBinder类。