设计模式 | 外观模式及典型应用

前言

本文的主要内容:

  • 介绍外观模式
  • 示例
    • 自己泡茶
    • 到茶馆喝茶
  • 外观模式总结
  • 外观模式的典型应用
    • spring JDBC 中的外观模式
    • Mybatis中的外观模式
    • Tomcat 中的外观模式
    • SLF4J 中的外观模式

外观模式

外观模式是一种使用频率非常高的结构型设计模式,它通过引入一个外观角色来简化客户端与子系统之间的交互,为复杂的子系统调用提供一个统一的入口,降低子系统与客户端的耦合度,且客户端调用非常方便。

外观模式又称为门面模式,它是一种对象结构型模式。外观模式是迪米特法则的一种具体实现,通过引入一个新的外观角色可以降低原有系统的复杂度,同时降低客户类与子系统的耦合度。

外观模式包含如下两个角色:

Facade(外观角色):在客户端可以调用它的方法,在外观角色中可以知道相关的(一个或者多个)子系统的功能和责任;在正常情况下,它将所有从客户端发来的请求委派到相应的子系统去,传递给相应的子系统对象处理。

SubSystem(子系统角色):在软件系统中可以有一个或者多个子系统角色,每一个子系统可以不是一个单独的类,而是一个类的集合,它实现子系统的功能;每一个子系统都可以被客户端直接调用,或者被外观角色调用,它处理由外观类传过来的请求;子系统并不知道外观的存在,对于子系统而言,外观角色仅仅是另外一个客户端而已。

外观模式的目的不是给予子系统添加新的功能接口,而是为了让外部减少与子系统内多个模块的交互,松散耦合,从而让外部能够更简单地使用子系统。

外观模式的本质是:封装交互,简化调用

示例

泡茶需要水 Water

public class Water {
    private int temperature;    // 温度
    private int capacity;       // 容量
    public Water() {
        this.temperature = 0;
        this.capacity = 10;
    }
    // 省略...
}    

泡茶需要茶叶 TeaLeaf

public class TeaLeaf {
    private String teaName;
    // 省略...
}    

烧水需要用水壶烧,将水加热

public class KettleService {
    public void waterBurning(String who, Water water, int burnTime) {
        // 烧水,计算最终温度
        int finalTermperature = Math.min(100, water.getTemperature() + burnTime * 20);
        water.setTemperature(finalTermperature);
        System.out.println(who + " 使用水壶烧水,最终水温为 " + finalTermperature);
    }
}

泡茶,将烧好的水与茶叶进行冲泡,最终得到一杯茶水

public class TeasetService {
    public Teawater makeTeaWater(String who, Water water, TeaLeaf teaLeaf) {
        String teawater = "一杯容量为 " + water.getCapacity() + ", 温度为 " + water.getTemperature() + " 的" + teaLeaf.getTeaName() + "茶水";
        System.out.println(who + " 泡了" + teawater);
        return new Teawater(teawater);
    }
}

人喝茶水

public class Man {
    private String name;
    public Man(String name) {
        this.name = name;
    }
    public void drink(Teawater teawater) {
        System.out.println(name + " 喝了" + teawater.getTeaWater());
    }
}

自己泡茶喝

张三、李四各自泡茶喝,各自都需要准备茶具、茶叶、水,各自还要完成烧水、泡茶等操作

public class Main {
    public static void main(String[] args) {
        Man zhangsan = new Man("张三");
        KettleService kettleService1 = new KettleService();
        TeasetService teasetService1 = new TeasetService();
        Water water1 = new Water();
        TeaLeaf teaLeaf1 = new TeaLeaf("西湖龙井");
        kettleService1.waterBurning(zhangsan.getName(), water1, 4);
        Teawater teawater1 = teasetService1.makeTeaWater(zhangsan.getName(), water1, teaLeaf1);
        zhangsan.drink(teawater1);
        System.out.println();

        Man lisi = new Man("李四");
        KettleService kettleService2 = new KettleService();
        TeasetService teasetService2 = new TeasetService();
        Water water2 = new Water(10, 15);
        TeaLeaf teaLeaf2 = new TeaLeaf("碧螺春");
        kettleService2.waterBurning(lisi.getName(), water2, 4);
        Teawater teawater2 = teasetService2.makeTeaWater(lisi.getName(), water2, teaLeaf2);
        lisi.drink(teawater2);
    }
}

输出为

张三 使用水壶烧水,最终水温为 80
张三 泡了一杯容量为 10, 温度为 80 的西湖龙井茶水
张三 喝了一杯容量为 10, 温度为 80 的西湖龙井茶水

李四 使用水壶烧水,最终水温为 90
李四 泡了一杯容量为 15, 温度为 90 的碧螺春茶水
李四 喝了一杯容量为 15, 温度为 90 的碧螺春茶水

自己泡茶喝模式图

自己泡茶喝模式图

到茶馆喝茶

茶馆,茶馆有不同的套餐

public class TeaHouseFacade {
    private String name;
    private TeasetService teasetService;
    private KettleService kettleService;

    public TeaHouseFacade(String name) {
        this.name = name;
        this.teasetService = new TeasetService();
        this.kettleService = new KettleService();
    }

    public Teawater makeTea(int teaNumber) {
        switch (teaNumber) {
            case 1:
                Water water1 = new Water();
                TeaLeaf teaLeaf1 = new TeaLeaf("西湖龙井");
                kettleService.waterBurning(this.name, water1, 4);
                Teawater teawater1 = teasetService.makeTeaWater(this.name, water1, teaLeaf1);
                return teawater1;
            case 2:
                Water water2 = new Water(10, 15);
                TeaLeaf teaLeaf2 = new TeaLeaf("碧螺春");
                kettleService.waterBurning(this.name, water2, 4);
                Teawater teawater2 = teasetService.makeTeaWater(this.name, water2, teaLeaf2);
                return teawater2;
            default:
                Water water3 = new Water();
                TeaLeaf teaLeaf3 = new TeaLeaf("招牌乌龙");
                kettleService.waterBurning(this.name, water3, 5);
                Teawater teawater3 = teasetService.makeTeaWater(this.name, water3, teaLeaf3);
                return teawater3;
        }
    }
}

张三和李四点茶,只需要告诉茶馆套餐编号即可,水、茶叶由茶馆准备,烧水泡茶的操作由茶馆统一完成

public class Test {
    public static void main(String[] args) {
        TeaHouseFacade teaHouseFacade = new TeaHouseFacade("老舍茶馆");

        Man zhangsan = new Man("张三");
        Teawater teawater = teaHouseFacade.makeTea(1);
        zhangsan.drink(teawater);
        System.out.println();

        Man lisi = new Man("李四");
        Teawater teawater1 = teaHouseFacade.makeTea(2);
        lisi.drink(teawater1);
    }
}

输出为

老舍茶馆 使用水壶烧水,最终水温为 80
老舍茶馆 泡了一杯容量为 10, 温度为 80 的西湖龙井茶水
张三 喝了一杯容量为 10, 温度为 80 的西湖龙井茶水

老舍茶馆 使用水壶烧水,最终水温为 90
老舍茶馆 泡了一杯容量为 15, 温度为 90 的碧螺春茶水
李四 喝了一杯容量为 15, 温度为 90 的碧螺春茶水

到茶馆喝茶模式图

到茶馆喝茶模式图

外观模式总结

外观模式的主要优点如下:

  • 它对客户端屏蔽了子系统组件,减少了客户端所需处理的对象数目,并使得子系统使用起来更加容易。通过引入外观模式,客户端代码将变得很简单,与之关联的对象也很少。
  • 它实现了子系统与客户端之间的松耦合关系,这使得子系统的变化不会影响到调用它的客户端,只需要调整外观类即可。
  • 一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象。

外观模式的主要缺点如下:

  • 不能很好地限制客户端直接使用子系统类,如果对客户端访问子系统类做太多的限制则减少了可变性和灵活性。
  • 如果设计不当,增加新的子系统可能需要修改外观类的源代码,违背了开闭原则。

适用场景:

  • 当要为访问一系列复杂的子系统提供一个简单入口时可以使用外观模式。
  • 客户端程序与多个子系统之间存在很大的依赖性。引入外观类可以将子系统与客户端解耦,从而提高子系统的独立性和可移植性。
  • 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。

源码分析外观模式的典型应用

spring jdbc中的外观模式

查看 org.springframework.jdbc.support.JdbcUtils

public abstract class JdbcUtils {
    public static void closeConnection(Connection con) {
        if (con != null) {
            try {
                con.close();
            }
            catch (SQLException ex) {
                logger.debug("Could not close JDBC Connection", ex);
            }
            catch (Throwable ex) {
                // We don't trust the JDBC driver: It might throw RuntimeException or Error.
                logger.debug("Unexpected exception on closing JDBC Connection", ex);
            }
        }
    }
    
    public static Object getResultSetValue(ResultSet rs, int index, Class<?> requiredType) throws SQLException {
        if (requiredType == null) {
            return getResultSetValue(rs, index);
        }

        Object value = null;
        boolean wasNullCheck = false;

        // Explicitly extract typed value, as far as possible.
        if (String.class.equals(requiredType)) {
            value = rs.getString(index);
        }
        else if (boolean.class.equals(requiredType) || Boolean.class.equals(requiredType)) {
            value = rs.getBoolean(index);
            wasNullCheck = true;
        }
        else if (byte.class.equals(requiredType) || Byte.class.equals(requiredType)) {
            value = rs.getByte(index);
            wasNullCheck = true;
        }
        else if (short.class.equals(requiredType) || Short.class.equals(requiredType)) {
            value = rs.getShort(index);
            wasNullCheck = true;
        }
        else if (int.class.equals(requiredType) || Integer.class.equals(requiredType)) {
            value = rs.getInt(index);
            wasNullCheck = true;
        }
        else if (long.class.equals(requiredType) || Long.class.equals(requiredType)) {
            value = rs.getLong(index);
            wasNullCheck = true;
        }
        else if (float.class.equals(requiredType) || Float.class.equals(requiredType)) {
            value = rs.getFloat(index);
            wasNullCheck = true;
        }
        else if (double.class.equals(requiredType) || Double.class.equals(requiredType) ||
                Number.class.equals(requiredType)) {
            value = rs.getDouble(index);
            wasNullCheck = true;
        }
        else if (byte[].class.equals(requiredType)) {
            value = rs.getBytes(index);
        }
        else if (java.sql.Date.class.equals(requiredType)) {
            value = rs.getDate(index);
        }
        else if (java.sql.Time.class.equals(requiredType)) {
            value = rs.getTime(index);
        }
        else if (java.sql.Timestamp.class.equals(requiredType) || java.util.Date.class.equals(requiredType)) {
            value = rs.getTimestamp(index);
        }
        else if (BigDecimal.class.equals(requiredType)) {
            value = rs.getBigDecimal(index);
        }
        else if (Blob.class.equals(requiredType)) {
            value = rs.getBlob(index);
        }
        else if (Clob.class.equals(requiredType)) {
            value = rs.getClob(index);
        }
        else {
            // Some unknown type desired -> rely on getObject.
            value = getResultSetValue(rs, index);
        }
        
        if (wasNullCheck && value != null && rs.wasNull()) {
            value = null;
        }
        return value;
    }
    // ...省略...
}    

该工具类主要是对原生的 jdbc 进行了封装

Mybatis中的外观模式

查看 org.apache.ibatis.session.Configuration 类中以 new 开头的方法

public class Configuration {
    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;
        if (ExecutorType.BATCH == executorType) {
          executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
          executor = new ReuseExecutor(this, transaction);
        } else {
          executor = new SimpleExecutor(this, transaction);
        }
        if (cacheEnabled) {
          executor = new CachingExecutor(executor);
        }
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }
    
    public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
          ResultHandler resultHandler, BoundSql boundSql) {
        ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
        resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
        return resultSetHandler;
    }
    // ...省略...
}

该类主要对一些创建对象的操作进行封装

Tomcat 中的外观模式

Tomcat 源码中大量使用了很多外观模式

Tomcat中的外观模式

org.apache.catalina.connector.Requestorg.apache.catalina.connector.RequestFacade 这两个类都实现了 HttpServletRequest 接口

Request 中调用 getRequest() 实际获取的是 RequestFacade 的对象

protected RequestFacade facade = null;

public HttpServletRequest getRequest() {
    if (facade == null) {
        facade = new RequestFacade(this);
    }
    return facade;
}

RequestFacade 中再对认为是子系统的操作进行封装

public class RequestFacade implements HttpServletRequest {
    /**
     * The wrapped request.
     */
    protected Request request = null;
    
    @Override
    public Object getAttribute(String name) {
        if (request == null) {
            throw new IllegalStateException(sm.getString("requestFacade.nullRequest"));
        }
        return request.getAttribute(name);
    }
    // ...省略...
}    

SLF4J 中的外观模式

SLF4J 是简单的日志外观模式框架,抽象了各种日志框架例如 LogbackLog4jCommons-loggingJDK 自带的 logging 实现接口。它使得用户可以在部署时使用自己想要的日志框架。

SLF4J 没有替代任何日志框架,它仅仅是标准日志框架的外观模式。如果在类路径下除了 SLF4J 再没有任何日志框架,那么默认状态是在控制台输出日志。

日志处理框架 Logback 是 Log4j 的改进版本,原生支持SLF4J(因为是同一作者开发的),因此 Logback+SLF4J 的组合是日志框架的最佳选择,比 SLF4J+其它日志框架 的组合要快一些。而且Logback的配置可以是XML或Groovy代码。

SLF4J 的 helloworld 如下:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorld {
  public static void main(String[] args) {
    Logger logger = LoggerFactory.getLogger(HelloWorld.class);
    logger.info("Hello World");
  }
}

下图为 SLF4J 与日志处理框架的绑定调用关系

SLF4J与日志处理框架的绑定调用关系

应用层调用 slf4j-api.jarslf4j-api.jar 再根据所绑定的日志处理框架调用不同的 jar 包进行处理

参考:
刘伟:设计模式Java版
慕课网java设计模式精讲 Debug 方式+内存分析
Java日志框架:slf4j作用及其实现原理

后记

欢迎评论、转发、分享,您的支持是我最大的动力

更多内容可访问我的个人博客:http://laijianfeng.org

关注【小旋锋】微信公众号,及时接收博文推送

关注_小旋锋_微信公众号
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容