六、门面模式与装饰器模式详解

8.门面模式

8.1.课程目标

1、掌握门面模式和装饰器模式的特征和应用场景

2、理解装饰器模式和代理模式的根本区别。

3、了解门面模式的优、缺点。

4、了解装饰器模式的优、缺点。

8.2.内容定位

1、定位高级课程,不太适合接触业务场景比较单一的人群。

2、深刻了解门面模式和装饰器模式的应用场景。

8.3.门面模式定义

门面模式(Facade Pattern)又叫外观模式,提供了一个统一的接口,用来访问子系统中的一群接
口。其主要特征是定义了一个高层接口,让子系统更容易使用,属于结构性模式。

原文: Provide a unified interface to a set of interfaces in a subsystem.Facade defines a higher-level interface that makes the subsystem easier to use.

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

其实,在我们日常的编码工作中,我们都在有意无意地大量使用门面模式,但凡只要高层模块需要
调度多个子系统(2个以上类对象),我们都会自觉地创建一个新类封装这些子系统,提供精简接口,
让高层模块可以更加容易间接调用这些子系统的功能。尤其是现阶段各种第三方SDK,各种开源类库,
很大概率都会使用门面模式。尤其是你觉得调用越方便的,门面模式使用的一般更多。

8.4.门面模式的应用场景

1、子系统越来越复杂,增加门面模式提供简单接口

2、构建多层系统结构,利用门面对象作为每层的入口,简化层间调用。

8.5.门面模式的通用写法

首先来看门面模式的UML类图:

<img src="https://miion.me/image-hosting/image-20200306150145819.png" alt="image-20200306150145819" style="zoom:50%;" />

门面模式主要包含2种角色:

外观角色(Facade):也称门面角色,系统对外的统一接口;

子系统角色(SubSystem):可以同时有一个或多个 SubSystem。每个 SubSytem 都不是一个单独
的类,而是一个类的集合。 SubSystem 并不知道 Facade 的存在,对于 SubSystem 而言, Facade 只
是另一个客户端而已(即 Facade 对 SubSystem 透明)。

下面是门面模式的通用代码,首先分别创建3个子系统的业务逻辑SubSystemA、SubSystemB、
SubSystemC,代码很简单:

// 子系统
public class SubSystemA {
    public void doA() {
        System.out.println("doing A stuff");
    }
}
// 子系统
public class SubSystemB {
    public void doB() {
        System.out.println("doing B stuff");
    }
}
// 子系统
public class SubSystemC {
    public void doC() {
        System.out.println("doing C stuff");
    }
}

来看客户端代码:

// 外观角色 Facade
public class Facade {
    private SubSystemA a = new SubSystemA();
    private SubSystemB b = new SubSystemB();
    private SubSystemC c = new SubSystemC();

    // 对外接口
    public void doA() {
        this.a.doA();
    }

    // 对外接口
    public void doB() {
        this.b.doB();
    }

    // 对外接口
    public void doC() {
        this.c.doC();
    }
}

8.6.门面模式业务场景实例

Gper社区上线了一个积分兑换礼品的商城,这礼品商城中的大部分功能并不是全部重新开发的,而是要去对接已有的各个子系统(如下图所示):

<img src="https://miion.me/image-hosting/image-20200306163027892.png" alt="image-20200306163027892" style="zoom:50%;" />

这些子系统可能涉及到积分系统、支付系统、物流系统的接口调用。如果所有的接口调用全部由前
端发送网络请求去调用现有接口的话,一则会增加前端开发人员的难度,二则会增加一些网络请求影响
页面性能。这个时候就可以发挥门面模式的优势了。将所有现成的接口全部整合到一个类中,由后端提
供统一的接口给前端调用,这样前端开发人员就不需要关心各接口的业务关系,只需要把精力集中在页
面交互上。下面我们用代码来模拟一下这个场景。

首先,创建礼品的实体类GiftInfo:

public class GiftInfo {

    private String name;

    public GiftInfo(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

然后,编写各个子系统的业务逻辑代码,分别创建积分系统QualifyService类:

public class QualifyService {

    public boolean isAvailable(GiftInfo giftInfo){
        System.out.println("校验" +giftInfo.getName() + "积分通过,库存通过。");
        return true;
    }
}

支付系统PaymentService类:

public class PaymentService {

    public boolean pay(GiftInfo giftInfo) {
        System.out.println("扣减" + giftInfo.getName() + " 积分成功");
        return true;
    }
}

物流系统ShippingService类:

public class ShippingService {
    public String delivery(GiftInfo giftInfo){
        System.out.println(giftInfo.getName() + "进入物流系统");
        String shippingNo = "666";
        return shippingNo;
    }
}

然后创建外观角色GiftFacdeService 类,对外只开放一个兑换礼物的exchange()方法,在 exchange() 方法内部整合3个子系统的所有功能。

public class FacadeService {
    private QualifyService qualifyService = new QualifyService();
    private PaymentService paymentService = new PaymentService();
    private ShippingService shippingService = new ShippingService();

    //兑换
    public void exchange(GiftInfo giftInfo){
        //资格校验通过
        if(qualifyService.isAvailable(giftInfo)){
            //如果支付积分成功
            if(paymentService.pay(giftInfo)){
                String shippingNo = shippingService.delivery(giftInfo);
                System.out.println("物流系统下单成功,物流单号是:" + shippingNo);
            }
        }
    }
}

最后,来看客户端代码:

public class Test {
    public static void main(String[] args) {
        FacadeService facadeService = new FacadeService();
        GiftInfo giftInfo = new GiftInfo("《Spring 5核心原理》");
        facadeService.exchange(giftInfo);
    }
}

运行结果如下:

校验《Spring 5核心原理》积分通过,库存通过。
扣减《Spring 5核心原理》 积分成功
《Spring 5核心原理》进入物流系统
物流系统下单成功,物流单号是:666

通过这样一个案例对比之后,相信大家对门面模式的印象非常深刻了。

8.7.门面模式在源码中的应用

下面我们来门面模式在源码中的应用,先来看Spring JDBC模块下的JdbcUtils类,它封装了和
JDBC相关的所有操作,它一个代码片段:

public abstract class JdbcUtils {

    /**
     * Constant that indicates an unknown (or unspecified) SQL type.
     * @see java.sql.Types
     */
    public static final int TYPE_UNKNOWN = Integer.MIN_VALUE;


    private static final Log logger = LogFactory.getLog(JdbcUtils.class);


    /**
     * Close the given JDBC Connection and ignore any thrown exception.
     * This is useful for typical finally blocks in manual JDBC code.
     * @param con the JDBC Connection to close (may be {@code null})
     */
    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);
            }
        }
    }

    /**
     * Close the given JDBC Statement and ignore any thrown exception.
     * This is useful for typical finally blocks in manual JDBC code.
     * @param stmt the JDBC Statement to close (may be {@code null})
     */
    public static void closeStatement(Statement stmt) {
        if (stmt != null) {
            try {
                stmt.close();
            }
            catch (SQLException ex) {
                logger.trace("Could not close JDBC Statement", ex);
            }
            catch (Throwable ex) {
                // We don't trust the JDBC driver: It might throw RuntimeException or Error.
                logger.trace("Unexpected exception on closing JDBC Statement", ex);
            }
        }
    }

    /**
     * Close the given JDBC ResultSet and ignore any thrown exception.
     * This is useful for typical finally blocks in manual JDBC code.
     * @param rs the JDBC ResultSet to close (may be {@code null})
     */
    public static void closeResultSet(ResultSet rs) {
        if (rs != null) {
            try {
                rs.close();
            }
            catch (SQLException ex) {
                logger.trace("Could not close JDBC ResultSet", ex);
            }
            catch (Throwable ex) {
                // We don't trust the JDBC driver: It might throw RuntimeException or Error.
                logger.trace("Unexpected exception on closing JDBC ResultSet", ex);
            }
        }
    }
    ...
}

其他更多的操作,看它的结构就非常清楚了:

<img src="https://miion.me/image-hosting/image-20200306175133730.png" alt="image-20200306175133730" style="zoom:50%;" />

再来看一个MyBatis中的Configuration类。它其中有很多new开头的方法,来看一下源代码:

public MetaObject newMetaObject(Object object) {
  return MetaObject.forObject(object, objectFactory, objectWrapperFactory, reflectorFactory);
}

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
  ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
  parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
  return parameterHandler;
}

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;
}

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
  statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
  return statementHandler;
}

public Executor newExecutor(Transaction transaction) {
  return newExecutor(transaction, defaultExecutorType);
}

上面的这些方法都是对JDBC中关键组件操作的封装。另外地在Tomcat的源码中也有体现,也非常的
有意思。举个例子RequestFacade类,来看源码:

@SuppressWarnings("deprecation")
public class RequestFacade implements HttpServletRequest {
    ...
    @Override
    public String getContentType() {

        if (request == null) {
            throw new IllegalStateException(
                            sm.getString("requestFacade.nullRequest"));
        }

        return request.getContentType();
    }


    @Override
    public ServletInputStream getInputStream() throws IOException {

        if (request == null) {
            throw new IllegalStateException(
                            sm.getString("requestFacade.nullRequest"));
        }

        return request.getInputStream();
    }


    @Override
    public String getParameter(String name) {

        if (request == null) {
            throw new IllegalStateException(
                            sm.getString("requestFacade.nullRequest"));
        }

        if (Globals.IS_SECURITY_ENABLED){
            return AccessController.doPrivileged(
                new GetParameterPrivilegedAction(name));
        } else {
            return request.getParameter(name);
        }
    }
    ...
}

我们看名字就知道它用了门面模式。它封装了非常多的request的操作,也整合了很多servlet-api以
外的一些内容,给用户使用提供了很大便捷。同样,Tomcat对Response和Session当也封装了
ResponseFacade和StandardSessionFacade类,感兴趣的小伙伴可以去深入了解一下。

8.8.门面模式的优缺点

优点:

1、简化了调用过程,无需深入了解子系统,以防给子系统带来风险。

2、减少系统依赖、松散耦合

3、更好地划分访问层次,提高了安全性

4、遵循迪米特法则,即最少知道原则。

缺点:

1、当增加子系统和扩展子系统行为时,可能容易带来未知风险

2、不符合开闭原则

3、某些情况下可能违背单一职责原则。

9.装饰器模式

9.1.装饰器模式定义

装饰器模式(Decorator Pattern),也称为包装模式(Wrapper Pattern)是指在不改变原有对象
的基础之上,将功能附加到对象上,提供了比继承更有弹性的替代方案(扩展原有对象的功能),属于
结构型模式。

原文:Attach additional responsibilities to an object dynamically keeping the same interface. Decorators provide a flexible alternative to subclassing for extending functionality.
解释:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。

装饰器模式的核心是功能扩展。使用装饰器模式可以透明且动态地扩展类的功能。

装饰器模式主要用于透明且动态地扩展类的功能。其实现原理为:让装饰器实现被包装类(Concrete
Component)相同的接口(Component)(使得装饰器与被扩展类类型一致),并在构造函数中传入
该接口(Component)对象,然后就可以在接口需要实现的方法中在被包装类对象的现有功能上添加
新功能了。而且由于装饰器与被包装类属于同一类型(均为Component),且构造函数的参数为其实
现接口类(Component),因此装饰器模式具备嵌套扩展功能,这样我们就能使用装饰器模式一层一
层的对最底层被包装类进行功能扩展了。

首先看下装饰器模式的通用UML类图:

从 UML 类图中,我们可以看到,装饰器模式 主要包含四种角色:

<img src="https://miion.me/image-hosting/image-20200306204330480.png" alt="image-20200306204330480" style="zoom:50%;" />

抽象组件(Component):可以是一个接口或者抽象类,其充当被装饰类的原始对象,规定了被
装饰对象的行为;

具体组件(ConcreteComponent):实现/继承Component的一个具体对象,也即被装饰对象;

抽象装饰器(Decorator):通用的装饰ConcreteComponent的装饰器,其内部必然有一个属性
指向 Component抽象组件;其实现一般是一个抽象类,主要是为了让其子类按照其构造形式传入一
个 Component 抽象组件,这是强制的通用行为(当然,如果系统中装饰逻辑单一,并不需要实现许
多装饰器,那么我们可以直接省略该类,而直接实现一个具体装饰器(ConcreteDecorator)即可);

具体装饰器(ConcreteDecorator):Decorator 的具体实现类,理论上,每个 ConcreteDecorator
都扩展了Component对象的一种功能;

总结:装饰器模式角色分配符合设计模式里氏替换原则,依赖倒置原则,从而使得其具备很强的扩展性,最终满足开 闭原则。

9.2.装饰器模式的应用场景

装饰器模式在我们生活中应用也比较多如给煎饼加鸡蛋;给蛋糕加上一些水果;给房子装修等,为对象扩展一些额外的职责。装饰器在代码程序中适用于以下场景:

1、用于扩展一个类的功能或给一个类添加附加职责。

2、动态的给一个对象添加功能,这些功能可以再动态的撤销。

3、需要为一批的兄弟类进行改装或加装功能。

来看一个这样的场景,上班族白领其实大多有睡懒觉的习惯,每天早上上班都是踩点,于是很多小
伙伴为了多赖一会儿床都不吃早餐。那么,也有些小伙伴可能在上班路上碰到卖煎饼的路边摊,都会顺
带一个到公司茶水间吃早餐。卖煎饼的大姐可以给你的煎饼加鸡蛋,也可以加香肠(如下图,PS:我买
煎饼一般都要求不加生菜)。

<img src="https://miion.me/image-hosting/image-20200306205431629.png" alt="image-20200306205431629" style="zoom:50%;" />

下面我们用代码还原一下码农的生活。首先创建一个煎饼Battercake类:

public class Battercake {

    protected String getMsg(){ return "煎饼";}

    public int getPrice(){ return 5;}
}

创建一个加鸡蛋的煎饼BattercakeWithEgg类:

public class BattercakeWithEgg extends Battercake {
    @Override 
    protected String getMsg(){ return super.getMsg() + "+1个鸡蛋";}

    @Override 
    //加一个鸡蛋加 1 块钱 
    public int getPrice(){ return super.getPrice() + 1;}
}

再创建一个既加鸡蛋又加香肠的BattercakeWithEggAndSausage类:

public class BattercakeWithEggAndSauage extends BattercakeWithEgg {
    @Override
    protected String getMsg(){ return super.getMsg() + "+1根香肠";}

    @Override
    //加一个香肠加 2 块钱
    public int getPrice(){ return super.getPrice() + 2;}
}

编写客户端测试代码:

public class Test {
    public static void main(String[] args) {
        Battercake battercake = new Battercake();
        System.out.println(battercake.getMsg() + ",总价:" + battercake.getPrice());

        BattercakeWithEgg battercakeWithEgg = new BattercakeWithEgg();
        System.out.println(battercakeWithEgg.getMsg() + ",总价:" + battercakeWithEgg.getPrice());

        BattercakeWithEggAndSauage battercakeWithEggAndSauage = new BattercakeWithEggAndSauage();
        System.out.println(battercakeWithEggAndSauage.getMsg() + ",总价:" + battercakeWithEggAndSauage.getPrice());
    }
}

运行结果:

煎饼,总价:5
煎饼+1个鸡蛋,总价:6
煎饼+1个鸡蛋+1根香肠,总价:8

运行结果没有问题。

但是,如果用户需要一个加2个鸡蛋加1根香肠的煎饼,那么用我们现在的类
结构是创建不出来的,也无法自动计算出价格,除非再创建一个类做定制。如果需求再变,一直加定制
显然是不科学的。那么下面我们就用装饰器模式来解决上面的问题。

首先创建一个建煎饼的抽象
Battercake类:

public abstract class Battercake {

    protected abstract String getMsg();

    protected abstract int getPrice();
}

创建一个基本的煎饼(或者叫基础套餐)BaseBattercake:

public class BaseBattercake extends Battercake{

    protected String getMsg(){ return "煎饼";}

    public int getPrice(){ return 5;}
}

然后,再创建一个扩展套餐的抽象装饰器BattercakeDecotator类:

public class BattercakeDecorator extends Battercake{
    //静态代理,委派 
    private Battercake battercake;

    public BattercakeDecorator(Battercake battercake) {
        this.battercake = battercake;
    }
    
    @Override 
    protected String getMsg(){ return this.battercake.getMsg();}
    
    @Override 
    public int getPrice(){ return this.battercake.getPrice();}
}

然后,创建鸡蛋装饰器EggDecorator类:

public class EggDecorator extends BattercakeDecorator{

    public EggDecorator(Battercake battercake) {
        super(battercake);
    }

    @Override
    protected String getMsg(){ return super.getMsg() + "1个鸡蛋";}

    @Override
    public int getPrice(){ return super.getPrice() + 1;}
}

创建香肠装饰器SausageDecorator类:

public class SauageDecorator extends BattercakeDecorator{

    public SauageDecorator(Battercake battercake) {
        super(battercake);
    }

    protected String getMsg(){ return super.getMsg() + "1根香肠";}

    public int getPrice(){ return super.getPrice() + 2;}
}

编写客户端测试代码:

public class Test {
    public static void main(String[] args) {
        //路边摊买一个煎饼
        Battercake battercake = new BaseBattercake();
        //煎饼有点小,想再加一个鸡蛋
        battercake = new EggDecorator(battercake);
        //再加一个鸡蛋
        battercake = new EggDecorator(battercake);
        //很饿,再加根香肠
        battercake = new SauageDecorator(battercake);
        //跟静态代理最大区别就是职责不同 
        //静态代理不一定要满足 is-a 的关系 
        //静态代理会做功能增强,同一个职责变得不一样
        //装饰器更多考虑是扩展 
        System.out.println(battercake.getMsg() + ",总价" + battercake.getPrice());
    }
}

运行结果:

煎饼,总价:5
煎饼+1个鸡蛋,总价:6
煎饼+1个鸡蛋+1根香肠,总价:8

来看一下类图:

<img src="https://miion.me/image-hosting/image-20200306213549376.png" alt="image-20200306213549376" style="zoom:50%;" />

为了加深印象,我们再来看一个应用场景。需求大致是这样,系统采用的是sls服务监控项目日志,
以Json的格式解析,所以需要将项目中的日志封装成json格式再打印。现有的日志体系采用了log4j+
slf4j框架搭建而成。客户端调用是这样的:

private static final Logger logger = LoggerFactory.getLogger(Component.class); 
logger.error(string);

这样打印出来的是毫无规则的一行行字符串。在考虑将其转换成json格式时,我采用了装饰器模式。
目前有的是统一接口 Logger 和其具体实现类,我要加的就是一个装饰类和真正封装成 Json格式的装
饰产品类。创建装饰器类LoggerDecorator:

public class LoggerDecorator implements Logger {

    protected Logger logger;

    public LoggerDecorator(Logger logger) {
        this.logger = logger;
    }

    public void error(String s) {
    }

    public void error(String s, Object o) {
    }
    //省略其他默认实现
}

创建具体组件JasonLogger类实现代码如下:

public class JsonLogger extends LoggerDecorator {
    public JsonLogger(Logger logger) {
        super(logger);
    }

    @Override
    public void info(String s) {
        JSONObject result = newJsonObject();
        result.put("message",s);
        logger.info(result.toString());
    }

    @Override
    public void error(String s) {
        JSONObject result = newJsonObject();
        result.put("message",s);
        logger.info(result.toString());
    }

    public void error(Exception e){
        JSONObject result = newJsonObject();
        result.put("exception",e.getClass().getName());
        String trace = Arrays.toString(e.getStackTrace());
        result.put("starckTrace",trace);
        logger.info(result.toString());
    }

    private JSONObject newJsonObject(){
        //拼装了一些运行时信息
        return new JSONObject();
    }
}

可以看到,在JsonLogger中,对于Logger的各种接口,我都用JsonObject对象进行一层封装。
在打印的时候,最终还是调用原生接口 logger.error(string),只是这个 string 参数已经被我们装饰过
了。如果有额外的需求,我们也可以再写一个函数去实现。比如 error(Exception e),只传入一个异常
对象,这样在调用时就非常方便了。

另外,为了在新老交替的过程中尽量不改变太多的代码和使用方式。我又在JsonLogger 中加入了
一个内部的工厂类JsonLoggerFactory(这个类转移到DecoratorLogger 中可能更好一些),他包含
一个静态方法,用于提供对应的JsonLogger实例。最终在新的日志体系中,使用方式如下:

public class Test {
    private static final Logger logger = JsonLoggerFactory.getLogger(Test.class);

    public static void main(String[] args) {
        logger.error("系统错误");
    }
}

对于客户端而言,唯一与原先不同的地方就是将 LoggerFactory 改为JsonLoggerFactory 即可,
这样的实现,也会被更快更方便的被其他开发者接受和习惯。最后看一下类图:

<img src="https://miion.me/image-hosting/image-20200306215623864.png" alt="image-20200306215623864" style="zoom:50%;" />

装饰器模式最本质的特征是讲原有类的附加功能抽离出来,简化原有类的逻辑。通过这样两个案例,我们可以总结出来,其实抽象的装饰器是可有可无的,具体可以根据业务模型来选择。

9.3.装饰器模式在源码中的应用

装饰器模式在源码中也应用得非常多,在 JDK 中体现最明显的类就是 IO 相关的类,如
BufferedReader、InputStream、OutputStream,看一下常用的InputStream的类结构图:

<img src="https://miion.me/image-hosting/image-20200306221314640.png" alt="image-20200306221314640" style="zoom: 50%;" />

在Spring 中的 TransactionAwareCacheDecorator类我们也可以来尝试理解一下,这个类主要是用来处理事务缓存的,来看一下代码:

public class TransactionAwareCacheDecorator implements Cache {
    private final Cache targetCache;

    public TransactionAwareCacheDecorator(Cache targetCache) {
        Assert.notNull(targetCache, "Target Cache must not be null");
        this.targetCache = targetCache;
    }

    public Cache getTargetCache() {
        return this.targetCache;
    }
    ...
}

TransactionAwareCacheDecorator就是对Cache的一个包装。再来看一个 MVC中的装饰器模式
HttpHeadResponseDecorator类:

public class HttpHeadResponseDecorator extends ServerHttpResponseDecorator {
    public HttpHeadResponseDecorator(ServerHttpResponse delegate) {
        super(delegate);
    }
    ...
}

最后,看看 MyBatis中的一段处理缓存的设计 org.apache.ibatis.cache.Cache 类,找到它的包定
位:

image-20200306222504353

从名字上来看其实更容易理解了。比如 FifoCache先入先出算法的缓存;LruCache最近最少使用
的缓存;TransactionlCache事务相关的缓存,都是采用装饰器模式。MyBatis源码在我们后续的课程
也会深入讲解,感兴趣的小伙伴可以详细看看这块的源码,也可以好好学习一下MyBatis的命名方式,
今天我们还是把重点放到设计模式上。

9.4.装饰器模式和代理模式对比

从代理模式的 UML类图和通用代码实现上看,代理模式与装饰器模式几乎一模一样。代理模式的
Subject 对 应 装 饰 器 模 式 的 Component , 代 理 模 式 的 RealSubject 对 应 装 饰 器 模 式 的
ConcreteComponent,代理模式的 Proxy对应装饰器模式的Decorator。确实,从代码实现上看,代
理模式的确与装饰器模式是一样的(其实装饰器模式就是代理模式的一个特殊应用),但是这两种设计
模式所面向的功能扩展面是不一样的:

装饰器模式强调自身功能的扩展。Decorator所做的就是增强ConcreteComponent的功能(也有
可能减弱功能),主体对象为ConcreteComponent,着重类功能的变化;

代理模式强调对代理过程的控制。Proxy 完全掌握对 RealSubject的访问控制,因此,Proxy 可以决定对RealSubject 进行功能扩展,功能缩减甚至功能散失(不调用RealSubject方法),主体对象为
Proxy;

简单来讲,假设现在小明想租房,那么势必会有一些事务发生:房源搜索,联系房东谈价格····

假设我们按照代理模式进行思考,那么小明只需找到一个房产中介,让他去干房源搜索,联系房东
谈价格这些事情,小明只需等待通知然后付点中介费就行了;

而如果采用装饰器模式进行思考,因为装饰器模式强调的是自身功能扩展,也就是说,如果要找房
子,小明自身就要增加房源搜索能力扩展,联系房东谈价格能力扩展,通过相应的装饰器,提升自身能
力,一个人做满所有的事情。

9.5.装饰器模式的优缺点

优点:

1、装饰器是继承的有力补充,比继承灵活,不改变原有对象的情况下动态地给一个对象扩展功能,即插即用。

2、通过使用不同装饰类以及这些装饰类的排列组合,可以实现不同效果。

3、装饰器完全遵守开闭原则。

缺点:

1、会出现更多的代码,更多的类,增加程序复杂性。

2、动态装饰时,多层装饰时会更复杂。

那么装饰器模式我们就讲解到这里,希望小伙伴们认真体会,加深理解。

9.6.作业

1、请举例3-5个使用门面模式的应用场景。

1.解决易用性问题

门面模式可以用来封装系统的底层实现,隐藏系统的复杂性,提供一组更加简单易用、更高层的接口。

比如,Linux 系统调用函数就可以看作一种“门面”。它是 Linux 操作系统暴露给开发者的一组“特殊”的编程接口,它封装了底层更基础的 Linux 内核调用。

再比如,Linux 的 Shell 命令,实际上也可以看作一种门面模式的应用。它继续封装系统调用,提供更加友好、简单的命令,让我们可以直接通过执行命令来跟操作系统交互。

2.解决性能问题

我们通过将多个接口调用替换为一个门面接口调用,减少网络通信成本,提高 App 客户端的响应速度。

3.解决分布式事务问题

在用户注册的时候,我们不仅会创建用户(在数据库 User 表中),还会给用户创建一个钱包(在数据库的 Wallet 表中)。用户注册需要支持事务,也就是说,创建用户和钱包的两个操作,要么都成功,要么都失败,不能一个成功、一个失败。

最简单的解决方案是,利用数据库事务或者 Spring 框架提供的事务(如果是 Java 语言的话),在一个事务中,执行创建用户和创建钱包这两个 SQL 操作。这就要求两个 SQL 操作要在一个接口中完成,所以,我们可以借鉴门面模式的思想,再设计一个包裹这两个操作的新接口,让新接口在一个事务中执行两个 SQL 操作。

2、使用装饰器模式实现一个可根据权限动态扩展功能的导航条。

例如:GPer社区未登录状态下的导航条

image-20200307180017756

GPer社区登录状态下的导航条

image-20200307180102423

整体思路:用户分为未登录会员、VIP会员、管理员分别展示不同的导航条。

1.创建抽象导航条类,抽象方法展示导航条。

public abstract class AbstractNavDecorator{
    /**
     * 展示导航条
     * @return 导航条
     */
    protected abstract String showNavs();
}

2.创建基础导航条类

public class BaseNavDecorator extends AbstractNavDecorator {
    @Override
    public String showNavs() {
        return "问答-文章-精品课-冒泡-商城";
    }
}

3.创建导航条装饰器

public class NavDecorator extends AbstractNav {

    private AbstractNav abstractNav;

    public NavDecorator(AbstractNav abstractNav) {
        this.abstractNav = abstractNav;
    }

    @Override
    protected String showNavs() {
        return this.abstractNav.showNavs();
    }
}

4.创建作业装饰器

public class HomeworkNavDecorator extends NavDecorator {

    public HomeworkNavDecorator(AbstractNav abstractNav) {
        super(abstractNav);
    }

    @Override
    public String showNavs() {
        return super.showNavs() + "-作业";
    }
}

5.创建题库装饰器

public class QuizNavDecorator extends NavDecorator {

    public QuizNavDecorator(AbstractNav abstractNav) {
        super(abstractNav);
    }

    @Override
    public String showNavs() {
        return super.showNavs() + "-题库";
    }
}

6.创建成长墙装饰器

public class WallNavDecorator extends NavDecorator {

    public WallNavDecorator(AbstractNav abstractNav) {
        super(abstractNav);
    }

    @Override
    public String showNavs() {
        return super.showNavs() + "-成长墙";
    }
}

7.创建管理装饰器

public class AdminNavDecorator extends NavDecorator {

    public AdminNavDecorator(AbstractNav abstractNav) {
        super(abstractNav);
    }

    @Override
    protected String showNavs() {
        return super.showNavs() + "-管理";
    }
}

8.创建用户枚举

public enum User {
    NOT_LOGIN(1, "未登录用户"),
    VIP(2, "会员"),
    ADMIN(3, "管理员")
    ;

    User(int code, String name) {
        this.code = code;
        this.type = name;
    }

    /**
     * 编码
     */
    private int code;

    /**
     * 用户类型
     */
    private String type;
}

9.创建根据用户创建不同导航条简单工厂与测试类

public class PermissionFactory {

    String showPermission(User user) {
        AbstractNav baseNavDecorator = new BaseNav();
        switch (user) {
            case NOT_LOGIN:
                return baseNavDecorator.showNavs();
            case VIP:
                return new WallNavDecorator(new QuizNavDecorator(new HomeworkNavDecorator(baseNavDecorator))).showNavs();
            case ADMIN:
                return new AdminNavDecorator(new WallNavDecorator(new QuizNavDecorator(new HomeworkNavDecorator(baseNavDecorator)))).showNavs();
            default:
                return null;
        }
    }

    public static void main(String[] args){
        PermissionFactory permissionFactory = new PermissionFactory();
        System.out.println("未登录用户导航条展示:");
        System.out.println(permissionFactory.showPermission(NOT_LOGIN));
        System.out.println("VIP用户导航条展示:");
        System.out.println(permissionFactory.showPermission(VIP));
        System.out.println("管理员导航条展示:");
        System.out.println(permissionFactory.showPermission(ADMIN));
    }
}

10.运行效果如下:

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

推荐阅读更多精彩内容