前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >设计模式 | 快速搞定【外观模式】

设计模式 | 快速搞定【外观模式】

作者头像
田维常
发布2020-05-14 21:03:42
3730
发布2020-05-14 21:03:42
举报
文章被收录于专栏:Java后端技术栈cwnait

概述

外观模式提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个高层接口,让子系统更容易使用。

外观模式的抽象结构图

例子1

飞机驾驶舱不少人都见过,当看到那些密密麻麻的按钮时,心想要是能一键启动就好了。在代码的世界里,我们也常常遇到一个业务功能需要调用很多接口甚至很多系统的情况,就像下图:

有时候被我们调用模块之间还需要互相调用,模块之间的关系都可以画出一张蜘蛛网。在这种情况下,要求开发者需要对每一个模块都有一定的了解,还需要了解他们之间的关系,开发一个功能的成本简直太高了,令人崩溃。

飞机驾驶舱的按钮由于某些原因不可能做成一键启动,但是我们的代码可以:

外观就是这个一键启动的按钮,它将多个模块或系统的代码进行了整合,而我们只要简单地调用外观暴露出来的一个接口。这就是外观模式( 也叫门面模式 ),其作用显而易见,就是提供一个简单接口来调用后方一群复杂的接口。

例子2

老田去饭店吃饭,这是老田自己要买菜、切菜、炒菜

你去饭店也是一样的,他去也是这样,都需要自己去买菜、切菜、炒菜等。

但是我们去饭店吃饭,都是把我们想吃的告诉服务员就可以了,服务员就是外观模式中的外观(门面)。

外观模式中的个角色
  • 子系统:已有模块或子系统,提供了一系列复杂的接口或功能。
  • 外观( 门面 ):它了解子系统,并对外暴露一个简单的接口。
  • 客户:调用外观提供的接口来实现功能,无需了解复杂的子系统。
代码示例

下面我们写一个简单的电脑启动的例子( 这个例子在许多设计模式教程中都曾出现,笔者认为这是最好的例子之一,直接借用了 )。

启动电脑我们通常只需要按下开机键就可以了,但电脑内部实际上启动了多个模块,如 CPU,硬盘,内存等。

开机键就是一个很好的外观,让程序员们无需了解 CPU 、内存和硬盘如何启动。

代码语言:javascript
复制
//CPU
public class CPU {
    public void start(){
        System.out.println("启动CPU");
    }
}
//硬盘
public class Disk {
    public void start(){
        System.out.println("启动硬盘");
    }
}
//内存
public class Memory {
    public void start(){
        System.out.println("启动内存");
    }
}

如果没有开机键,我们需要这么做:

代码语言:javascript
复制
new CPU().start();
new Disk().start();
new Memory().start();

有了开机键,这些操作都交给开机键去做:

代码语言:javascript
复制
//开机键
public class StartBtn {

    public void start(){
        new CPU().start();
        new Disk().start();
        new Memory().start();
    }
}

而我们只需要:

代码语言:javascript
复制
new StartBtn().start();

外观模式不仅为我们提供了一个简单方便的接口,也让我们的系统和子系统解耦。

迪米特法则( 最少知道原则 )

迪米特法则是说每一个类都应该尽量地少知道别的类,外观模式就是迪米特法则的应用。原本我们需要知道许多的子系统或接口,用了外观类之后,我们仅仅需要知道外观类即可。

换句话说就是:知道的太多对你没好处。

迪米特法则是希望类之间减少耦合,类越独立越好。有句话叫牵一发而动全身,如果类之间关系太紧密,与之关联的类太多,一旦你修改该类,也许会动到无数与之关联的类。

实际案例

1. JAVA 三层结构

用 JAVA 开发我们经常使用三层结构:

  • controller 控制器层。
  • Service 服务层。
  • Dao 数据访问层。

有时候业务很简单,例如根据用户ID或者用户信息,Service 层这样写:

代码语言:javascript
复制
User getUserById(Integer id){
    return userDao.getUserById(id);
}

Dao 层:

代码语言:javascript
复制
User getUserById(Integer id){
    //查询数据库
    return user;
}

Service 层直接调用了userDao 的 getUserById() , Service 本身并没有执行什么额外的代码,那么为什么不省去 Service 层呢?

其实三层结构也蕴含了外观模式的思想在内。假如 Service 有一个转账方法:

代码语言:javascript
复制
public boolean transMoney(Integer user1,Integer user2,Float money){
    //用户1加钱
    userDao.addMoney(user1,money);
    //用户2扣钱
    userDao.decMoney(user2,money);
    //转账日志
    logDao.addLog(user1,user2,money);
}

作为调用方来说,并不想知道转账操作具体要调用哪些 Dao,一行代码 transMoney() 就能搞定岂不是皆大欢喜。

因此 Service 是很有必要的,一般在业务系统中,Service 层的类不仅仅是简单的调用 Dao,而是作为外观,给 Controller 提供了更方便好用的接口。

不过无论多复杂的系统,总会有 Service 直接调用 Dao 的 getUserById() 的情况 ,我们是否可以偷懒直接在 Controller 调用 Dao 呢?理论上是没问题的,但是强烈建议不要这么干,因为这样会导致层侵入,三层结构的层级混乱。

除非你的业务真的简单到极致,那么干脆直接舍弃 Service 层。只要你有Service 层,就请不要跨层调用。

2. Tomcat 中的外观模式

在做 Servlet 开发时,我们经常用的两个对象就是 HttpServletRequest 和 HttpServletResponse ,但我们拿到的这两个对象其实是被 Tomcat 经过了外观包裹的对象,那么 Tomcat 为什么要这么做呢?

首先我们先来了解一下 HttpServletRequest ,通过源码可以发现,HttpServletRequest 是一个接口,有两个类 RequestFacade 和 Request 实现了 HttpServletRequest :

以 Facade 命名的,毫无疑问是用了外观模式,下面给出一部分源码:

Request 类,继承 HttpServletRequest :

代码语言:javascript
复制
public class Request implements HttpServletRequest {

}

当我们需要使用 Request 对象时,Tomcat 传给我们的其实并不是 Request 对象,而是 RequestFacade 。

RequestFacade 类:

代码语言:javascript
复制
package org.apache.catalina.connector;

public class RequestFacade implements HttpServletRequest {

    protected Request request = null;

    public RequestFacade(Request request) {
        this.request = request;
    }

    public Object getAttribute(String name) {
        if (this.request == null) {
            throw new IllegalStateException(sm.getString("requestFacade.nullRequest"));
        } else {
            return this.request.getAttribute(name);
        }
    }


    public String getProtocol() {
        if (this.request == null) {
            throw new IllegalStateException(sm.getString("requestFacade.nullRequest"));
        } else {
            return this.request.getProtocol();
        }
    }

}

从上面 RequestFacade 源码中可以看到,当调用 getAttribute() , getProtocol() 等方法时,其实还是调用了 Request 对象的 getAttribute() 方法。

既然如此,为什么要多此一举弄个 RequestFacade 呢 ,其实是为了安全,Tomcat 不想把过多的方法暴露给别人。

Tomcat 内部有很多组件,组件之间经常需要通讯,有些方法不得不定义为 Public ,这样才能被其他组件所调用。

但是有些方法只希望内部通讯用,并不想暴露给 Web 开发者,否则会有安全问题。所以定义一个外观类,只实现想要暴露给外部的方法。

所以 Tomcat 要传 Request 给我们的时候,其实是这么做的:

代码语言:javascript
复制
return new RequestFacade(request);

从这个案例中可以看出外观模式不仅仅用于将复杂的接口包装为一个简单的接口,也可以用于隐藏一些不想暴露给别人的方法或接口。

总结

外观模式主要使用场景:

  • 当要为访问一系列复杂的子系统提供一个简单入口时可以使用外观模式。
  • 客户端程序与多个子系统之间存在很大的依赖性。引入外观类可以将子系统与客户端解耦,从而提高子系统的独立性和可移植性。
  • 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。
优点
  • 对客户端屏蔽了子系统组件,减少了客户端处理的对象数量,也减少了客户端的代码量。
  • 实现了客户端和子系统的松散耦合,使得子系统个变化不会影响到调用它的客户端,只需要改变外观类即可。
  • 一个子系统的变化不会影响到另一个子系统,子系统内部变化也不会影响到外观对象。
缺点
  • 不能很好地限制客户端直接使用子系统类,如果对客户端访问子系统类做太多的限制则减少了可变性和灵活性。
  • 如果设计不当,增加新的子系统可能需要修改外观类的源代码,违背了开闭原则。

GOF23种设计模式类型、描述和类图(上)

GOF23种设计模式类型、描述和类图(中)

GOF23种设计模式类型、描述和类图(下)

【文章汇总】设计模式篇

Java中的门面设计模式及如何用代码实现

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-05-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java后端技术栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述
    • 外观模式的抽象结构图
      • 例子2
        • 外观模式中的个角色
          • 代码示例
            • 迪米特法则( 最少知道原则 )
            • 实际案例
              • 1. JAVA 三层结构
                • 2. Tomcat 中的外观模式
                • 总结
                  • 优点
                    • 缺点
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档