六、門面模式與裝飾器模式詳解

8.門面模式

8.1.課程目標(biāo)

1、掌握門面模式和裝飾器模式的特征和應(yīng)用場(chǎng)景

2、理解裝飾器模式和代理模式的根本區(qū)別。

3、了解門面模式的優(yōu)、缺點(diǎn)。

4、了解裝飾器模式的優(yōu)、缺點(diǎn)。

8.2.內(nèi)容定位

1、定位高級(jí)課程,不太適合接觸業(yè)務(wù)場(chǎng)景比較單一的人群。

2、深刻了解門面模式和裝飾器模式的應(yīng)用場(chǎng)景。

8.3.門面模式定義

門面模式(Facade Pattern)又叫外觀模式,提供了一個(gè)統(tǒng)一的接口,用來(lái)訪問(wèn)子系統(tǒng)中的一群接
口。其主要特征是定義了一個(gè)高層接口,讓子系統(tǒng)更容易使用,屬于結(jié)構(gòu)性模式。

原文: 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.

解釋:要求一個(gè)子系統(tǒng)的外部與其內(nèi)部的通信必須通過(guò)一個(gè)同一的對(duì)象進(jìn)行。門面模式提供一個(gè)高層次的接口,使得 子系統(tǒng)更易于使用。

其實(shí),在我們?nèi)粘5木幋a工作中,我們都在有意無(wú)意地大量使用門面模式,但凡只要高層模塊需要
調(diào)度多個(gè)子系統(tǒng)(2個(gè)以上類對(duì)象),我們都會(huì)自覺(jué)地創(chuàng)建一個(gè)新類封裝這些子系統(tǒng),提供精簡(jiǎn)接口,
讓高層模塊可以更加容易間接調(diào)用這些子系統(tǒng)的功能。尤其是現(xiàn)階段各種第三方SDK,各種開(kāi)源類庫(kù),
很大概率都會(huì)使用門面模式。尤其是你覺(jué)得調(diào)用越方便的,門面模式使用的一般更多。

8.4.門面模式的應(yīng)用場(chǎng)景

1、子系統(tǒng)越來(lái)越復(fù)雜,增加門面模式提供簡(jiǎn)單接口

2、構(gòu)建多層系統(tǒng)結(jié)構(gòu),利用門面對(duì)象作為每層的入口,簡(jiǎn)化層間調(diào)用。

8.5.門面模式的通用寫(xiě)法

首先來(lái)看門面模式的UML類圖:

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

門面模式主要包含2種角色:

外觀角色(Facade):也稱門面角色,系統(tǒng)對(duì)外的統(tǒng)一接口;

子系統(tǒng)角色(SubSystem):可以同時(shí)有一個(gè)或多個(gè) SubSystem。每個(gè) SubSytem 都不是一個(gè)單獨(dú)
的類,而是一個(gè)類的集合。 SubSystem 并不知道 Facade 的存在,對(duì)于 SubSystem 而言, Facade 只
是另一個(gè)客戶端而已(即 Facade 對(duì) SubSystem 透明)。

下面是門面模式的通用代碼,首先分別創(chuàng)建3個(gè)子系統(tǒng)的業(yè)務(wù)邏輯SubSystemA、SubSystemB、
SubSystemC,代碼很簡(jiǎn)單:

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

來(lái)看客戶端代碼:

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

    // 對(duì)外接口
    public void doA() {
        this.a.doA();
    }

    // 對(duì)外接口
    public void doB() {
        this.b.doB();
    }

    // 對(duì)外接口
    public void doC() {
        this.c.doC();
    }
}

8.6.門面模式業(yè)務(wù)場(chǎng)景實(shí)例

Gper社區(qū)上線了一個(gè)積分兌換禮品的商城,這禮品商城中的大部分功能并不是全部重新開(kāi)發(fā)的,而是要去對(duì)接已有的各個(gè)子系統(tǒng)(如下圖所示):

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

這些子系統(tǒng)可能涉及到積分系統(tǒng)、支付系統(tǒng)、物流系統(tǒng)的接口調(diào)用。如果所有的接口調(diào)用全部由前
端發(fā)送網(wǎng)絡(luò)請(qǐng)求去調(diào)用現(xiàn)有接口的話,一則會(huì)增加前端開(kāi)發(fā)人員的難度,二則會(huì)增加一些網(wǎng)絡(luò)請(qǐng)求影響
頁(yè)面性能。這個(gè)時(shí)候就可以發(fā)揮門面模式的優(yōu)勢(shì)了。將所有現(xiàn)成的接口全部整合到一個(gè)類中,由后端提
供統(tǒng)一的接口給前端調(diào)用,這樣前端開(kāi)發(fā)人員就不需要關(guān)心各接口的業(yè)務(wù)關(guān)系,只需要把精力集中在頁(yè)
面交互上。下面我們用代碼來(lái)模擬一下這個(gè)場(chǎng)景。

首先,創(chuàng)建禮品的實(shí)體類GiftInfo:

public class GiftInfo {

    private String name;

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

    public String getName() {
        return name;
    }
}

然后,編寫(xiě)各個(gè)子系統(tǒng)的業(yè)務(wù)邏輯代碼,分別創(chuàng)建積分系統(tǒng)QualifyService類:

public class QualifyService {

    public boolean isAvailable(GiftInfo giftInfo){
        System.out.println("校驗(yàn)" +giftInfo.getName() + "積分通過(guò),庫(kù)存通過(guò)。");
        return true;
    }
}

支付系統(tǒng)PaymentService類:

public class PaymentService {

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

物流系統(tǒng)ShippingService類:

public class ShippingService {
    public String delivery(GiftInfo giftInfo){
        System.out.println(giftInfo.getName() + "進(jìn)入物流系統(tǒng)");
        String shippingNo = "666";
        return shippingNo;
    }
}

然后創(chuàng)建外觀角色GiftFacdeService 類,對(duì)外只開(kāi)放一個(gè)兌換禮物的exchange()方法,在 exchange() 方法內(nèi)部整合3個(gè)子系統(tǒng)的所有功能。

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

    //兌換
    public void exchange(GiftInfo giftInfo){
        //資格校驗(yàn)通過(guò)
        if(qualifyService.isAvailable(giftInfo)){
            //如果支付積分成功
            if(paymentService.pay(giftInfo)){
                String shippingNo = shippingService.delivery(giftInfo);
                System.out.println("物流系統(tǒng)下單成功,物流單號(hào)是:" + shippingNo);
            }
        }
    }
}

最后,來(lái)看客戶端代碼:

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

運(yùn)行結(jié)果如下:

校驗(yàn)《Spring 5核心原理》積分通過(guò),庫(kù)存通過(guò)。
扣減《Spring 5核心原理》 積分成功
《Spring 5核心原理》進(jìn)入物流系統(tǒng)
物流系統(tǒng)下單成功,物流單號(hào)是:666

通過(guò)這樣一個(gè)案例對(duì)比之后,相信大家對(duì)門面模式的印象非常深刻了。

8.7.門面模式在源碼中的應(yīng)用

下面我們來(lái)門面模式在源碼中的應(yīng)用,先來(lái)看Spring JDBC模塊下的JdbcUtils類,它封裝了和
JDBC相關(guān)的所有操作,它一個(gè)代碼片段:

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

其他更多的操作,看它的結(jié)構(gòu)就非常清楚了:

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

再來(lái)看一個(gè)MyBatis中的Configuration類。它其中有很多new開(kāi)頭的方法,來(lái)看一下源代碼:

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

上面的這些方法都是對(duì)JDBC中關(guān)鍵組件操作的封裝。另外地在Tomcat的源碼中也有體現(xiàn),也非常的
有意思。舉個(gè)例子RequestFacade類,來(lái)看源碼:

@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以
外的一些內(nèi)容,給用戶使用提供了很大便捷。同樣,Tomcat對(duì)Response和Session當(dāng)也封裝了
ResponseFacade和StandardSessionFacade類,感興趣的小伙伴可以去深入了解一下。

8.8.門面模式的優(yōu)缺點(diǎn)

優(yōu)點(diǎn):

1、簡(jiǎn)化了調(diào)用過(guò)程,無(wú)需深入了解子系統(tǒng),以防給子系統(tǒng)帶來(lái)風(fēng)險(xiǎn)。

2、減少系統(tǒng)依賴、松散耦合

3、更好地劃分訪問(wèn)層次,提高了安全性

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

缺點(diǎn):

1、當(dāng)增加子系統(tǒng)和擴(kuò)展子系統(tǒng)行為時(shí),可能容易帶來(lái)未知風(fēng)險(xiǎn)

2、不符合開(kāi)閉原則

3、某些情況下可能違背單一職責(zé)原則。

9.裝飾器模式

9.1.裝飾器模式定義

裝飾器模式(Decorator Pattern),也稱為包裝模式(Wrapper Pattern)是指在不改變?cè)袑?duì)象
的基礎(chǔ)之上,將功能附加到對(duì)象上,提供了比繼承更有彈性的替代方案(擴(kuò)展原有對(duì)象的功能),屬于
結(jié)構(gòu)型模式。

原文:Attach additional responsibilities to an object dynamically keeping the same interface. Decorators provide a flexible alternative to subclassing for extending functionality.
解釋:動(dòng)態(tài)地給一個(gè)對(duì)象添加一些額外的職責(zé)。就增加功能來(lái)說(shuō),裝飾器模式相比生成子類更為靈活。

裝飾器模式的核心是功能擴(kuò)展。使用裝飾器模式可以透明且動(dòng)態(tài)地?cái)U(kuò)展類的功能。

裝飾器模式主要用于透明且動(dòng)態(tài)地?cái)U(kuò)展類的功能。其實(shí)現(xiàn)原理為:讓裝飾器實(shí)現(xiàn)被包裝類(Concrete
Component)相同的接口(Component)(使得裝飾器與被擴(kuò)展類類型一致),并在構(gòu)造函數(shù)中傳入
該接口(Component)對(duì)象,然后就可以在接口需要實(shí)現(xiàn)的方法中在被包裝類對(duì)象的現(xiàn)有功能上添加
新功能了。而且由于裝飾器與被包裝類屬于同一類型(均為Component),且構(gòu)造函數(shù)的參數(shù)為其實(shí)
現(xiàn)接口類(Component),因此裝飾器模式具備嵌套擴(kuò)展功能,這樣我們就能使用裝飾器模式一層一
層的對(duì)最底層被包裝類進(jìn)行功能擴(kuò)展了。

首先看下裝飾器模式的通用UML類圖:

從 UML 類圖中,我們可以看到,裝飾器模式 主要包含四種角色:

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

抽象組件(Component):可以是一個(gè)接口或者抽象類,其充當(dāng)被裝飾類的原始對(duì)象,規(guī)定了被
裝飾對(duì)象的行為;

具體組件(ConcreteComponent):實(shí)現(xiàn)/繼承Component的一個(gè)具體對(duì)象,也即被裝飾對(duì)象;

抽象裝飾器(Decorator):通用的裝飾ConcreteComponent的裝飾器,其內(nèi)部必然有一個(gè)屬性
指向 Component抽象組件;其實(shí)現(xiàn)一般是一個(gè)抽象類,主要是為了讓其子類按照其構(gòu)造形式傳入一
個(gè) Component 抽象組件,這是強(qiáng)制的通用行為(當(dāng)然,如果系統(tǒng)中裝飾邏輯單一,并不需要實(shí)現(xiàn)許
多裝飾器,那么我們可以直接省略該類,而直接實(shí)現(xiàn)一個(gè)具體裝飾器(ConcreteDecorator)即可);

具體裝飾器(ConcreteDecorator):Decorator 的具體實(shí)現(xiàn)類,理論上,每個(gè) ConcreteDecorator
都擴(kuò)展了Component對(duì)象的一種功能;

總結(jié):裝飾器模式角色分配符合設(shè)計(jì)模式里氏替換原則,依賴倒置原則,從而使得其具備很強(qiáng)的擴(kuò)展性,最終滿足開(kāi) 閉原則。

9.2.裝飾器模式的應(yīng)用場(chǎng)景

裝飾器模式在我們生活中應(yīng)用也比較多如給煎餅加雞蛋;給蛋糕加上一些水果;給房子裝修等,為對(duì)象擴(kuò)展一些額外的職責(zé)。裝飾器在代碼程序中適用于以下場(chǎng)景:

1、用于擴(kuò)展一個(gè)類的功能或給一個(gè)類添加附加職責(zé)。

2、動(dòng)態(tài)的給一個(gè)對(duì)象添加功能,這些功能可以再動(dòng)態(tài)的撤銷。

3、需要為一批的兄弟類進(jìn)行改裝或加裝功能。

來(lái)看一個(gè)這樣的場(chǎng)景,上班族白領(lǐng)其實(shí)大多有睡懶覺(jué)的習(xí)慣,每天早上上班都是踩點(diǎn),于是很多小
伙伴為了多賴一會(huì)兒床都不吃早餐。那么,也有些小伙伴可能在上班路上碰到賣煎餅的路邊攤,都會(huì)順
帶一個(gè)到公司茶水間吃早餐。賣煎餅的大姐可以給你的煎餅加雞蛋,也可以加香腸(如下圖,PS:我買
煎餅一般都要求不加生菜)。

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

下面我們用代碼還原一下碼農(nóng)的生活。首先創(chuàng)建一個(gè)煎餅Battercake類:

public class Battercake {

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

    public int getPrice(){ return 5;}
}

創(chuàng)建一個(gè)加雞蛋的煎餅BattercakeWithEgg類:

public class BattercakeWithEgg extends Battercake {
    @Override 
    protected String getMsg(){ return super.getMsg() + "+1個(gè)雞蛋";}

    @Override 
    //加一個(gè)雞蛋加 1 塊錢 
    public int getPrice(){ return super.getPrice() + 1;}
}

再創(chuàng)建一個(gè)既加雞蛋又加香腸的BattercakeWithEggAndSausage類:

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

    @Override
    //加一個(gè)香腸加 2 塊錢
    public int getPrice(){ return super.getPrice() + 2;}
}

編寫(xiě)客戶端測(cè)試代碼:

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

        BattercakeWithEgg battercakeWithEgg = new BattercakeWithEgg();
        System.out.println(battercakeWithEgg.getMsg() + ",總價(jià):" + battercakeWithEgg.getPrice());

        BattercakeWithEggAndSauage battercakeWithEggAndSauage = new BattercakeWithEggAndSauage();
        System.out.println(battercakeWithEggAndSauage.getMsg() + ",總價(jià):" + battercakeWithEggAndSauage.getPrice());
    }
}

運(yùn)行結(jié)果:

煎餅,總價(jià):5
煎餅+1個(gè)雞蛋,總價(jià):6
煎餅+1個(gè)雞蛋+1根香腸,總價(jià):8

運(yùn)行結(jié)果沒(méi)有問(wèn)題。

但是,如果用戶需要一個(gè)加2個(gè)雞蛋加1根香腸的煎餅,那么用我們現(xiàn)在的類
結(jié)構(gòu)是創(chuàng)建不出來(lái)的,也無(wú)法自動(dòng)計(jì)算出價(jià)格,除非再創(chuàng)建一個(gè)類做定制。如果需求再變,一直加定制
顯然是不科學(xué)的。那么下面我們就用裝飾器模式來(lái)解決上面的問(wèn)題。

首先創(chuàng)建一個(gè)建煎餅的抽象
Battercake類:

public abstract class Battercake {

    protected abstract String getMsg();

    protected abstract int getPrice();
}

創(chuàng)建一個(gè)基本的煎餅(或者叫基礎(chǔ)套餐)BaseBattercake:

public class BaseBattercake extends Battercake{

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

    public int getPrice(){ return 5;}
}

然后,再創(chuàng)建一個(gè)擴(kuò)展套餐的抽象裝飾器BattercakeDecotator類:

public class BattercakeDecorator extends Battercake{
    //靜態(tài)代理,委派 
    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();}
}

然后,創(chuàng)建雞蛋裝飾器EggDecorator類:

public class EggDecorator extends BattercakeDecorator{

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

    @Override
    protected String getMsg(){ return super.getMsg() + "1個(gè)雞蛋";}

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

創(chuàng)建香腸裝飾器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;}
}

編寫(xiě)客戶端測(cè)試代碼:

public class Test {
    public static void main(String[] args) {
        //路邊攤買一個(gè)煎餅
        Battercake battercake = new BaseBattercake();
        //煎餅有點(diǎn)小,想再加一個(gè)雞蛋
        battercake = new EggDecorator(battercake);
        //再加一個(gè)雞蛋
        battercake = new EggDecorator(battercake);
        //很餓,再加根香腸
        battercake = new SauageDecorator(battercake);
        //跟靜態(tài)代理最大區(qū)別就是職責(zé)不同 
        //靜態(tài)代理不一定要滿足 is-a 的關(guān)系 
        //靜態(tài)代理會(huì)做功能增強(qiáng),同一個(gè)職責(zé)變得不一樣
        //裝飾器更多考慮是擴(kuò)展 
        System.out.println(battercake.getMsg() + ",總價(jià)" + battercake.getPrice());
    }
}

運(yùn)行結(jié)果:

煎餅,總價(jià):5
煎餅+1個(gè)雞蛋,總價(jià):6
煎餅+1個(gè)雞蛋+1根香腸,總價(jià):8

來(lái)看一下類圖:

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

為了加深印象,我們?cè)賮?lái)看一個(gè)應(yīng)用場(chǎng)景。需求大致是這樣,系統(tǒng)采用的是sls服務(wù)監(jiān)控項(xiàng)目日志,
以Json的格式解析,所以需要將項(xiàng)目中的日志封裝成json格式再打印?,F(xiàn)有的日志體系采用了log4j+
slf4j框架搭建而成??蛻舳苏{(diào)用是這樣的:

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

這樣打印出來(lái)的是毫無(wú)規(guī)則的一行行字符串。在考慮將其轉(zhuǎn)換成json格式時(shí),我采用了裝飾器模式。
目前有的是統(tǒng)一接口 Logger 和其具體實(shí)現(xiàn)類,我要加的就是一個(gè)裝飾類和真正封裝成 Json格式的裝
飾產(chǎn)品類。創(chuàng)建裝飾器類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) {
    }
    //省略其他默認(rèn)實(shí)現(xiàn)
}

創(chuàng)建具體組件JasonLogger類實(shí)現(xiàn)代碼如下:

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(){
        //拼裝了一些運(yùn)行時(shí)信息
        return new JSONObject();
    }
}

可以看到,在JsonLogger中,對(duì)于Logger的各種接口,我都用JsonObject對(duì)象進(jìn)行一層封裝。
在打印的時(shí)候,最終還是調(diào)用原生接口 logger.error(string),只是這個(gè) string 參數(shù)已經(jīng)被我們裝飾過(guò)
了。如果有額外的需求,我們也可以再寫(xiě)一個(gè)函數(shù)去實(shí)現(xiàn)。比如 error(Exception e),只傳入一個(gè)異常
對(duì)象,這樣在調(diào)用時(shí)就非常方便了。

另外,為了在新老交替的過(guò)程中盡量不改變太多的代碼和使用方式。我又在JsonLogger 中加入了
一個(gè)內(nèi)部的工廠類JsonLoggerFactory(這個(gè)類轉(zhuǎn)移到DecoratorLogger 中可能更好一些),他包含
一個(gè)靜態(tài)方法,用于提供對(duì)應(yīng)的JsonLogger實(shí)例。最終在新的日志體系中,使用方式如下:

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

    public static void main(String[] args) {
        logger.error("系統(tǒng)錯(cuò)誤");
    }
}

對(duì)于客戶端而言,唯一與原先不同的地方就是將 LoggerFactory 改為JsonLoggerFactory 即可,
這樣的實(shí)現(xiàn),也會(huì)被更快更方便的被其他開(kāi)發(fā)者接受和習(xí)慣。最后看一下類圖:

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

裝飾器模式最本質(zhì)的特征是講原有類的附加功能抽離出來(lái),簡(jiǎn)化原有類的邏輯。通過(guò)這樣兩個(gè)案例,我們可以總結(jié)出來(lái),其實(shí)抽象的裝飾器是可有可無(wú)的,具體可以根據(jù)業(yè)務(wù)模型來(lái)選擇。

9.3.裝飾器模式在源碼中的應(yīng)用

裝飾器模式在源碼中也應(yīng)用得非常多,在 JDK 中體現(xiàn)最明顯的類就是 IO 相關(guān)的類,如
BufferedReader、InputStream、OutputStream,看一下常用的InputStream的類結(jié)構(gòu)圖:

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

在Spring 中的 TransactionAwareCacheDecorator類我們也可以來(lái)嘗試?yán)斫庖幌?,這個(gè)類主要是用來(lái)處理事務(wù)緩存的,來(lái)看一下代碼:

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就是對(duì)Cache的一個(gè)包裝。再來(lái)看一個(gè) MVC中的裝飾器模式
HttpHeadResponseDecorator類:

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

最后,看看 MyBatis中的一段處理緩存的設(shè)計(jì) org.apache.ibatis.cache.Cache 類,找到它的包定
位:

image-20200306222504353

從名字上來(lái)看其實(shí)更容易理解了。比如 FifoCache先入先出算法的緩存;LruCache最近最少使用
的緩存;TransactionlCache事務(wù)相關(guān)的緩存,都是采用裝飾器模式。MyBatis源碼在我們后續(xù)的課程
也會(huì)深入講解,感興趣的小伙伴可以詳細(xì)看看這塊的源碼,也可以好好學(xué)習(xí)一下MyBatis的命名方式,
今天我們還是把重點(diǎn)放到設(shè)計(jì)模式上。

9.4.裝飾器模式和代理模式對(duì)比

從代理模式的 UML類圖和通用代碼實(shí)現(xiàn)上看,代理模式與裝飾器模式幾乎一模一樣。代理模式的
Subject 對(duì) 應(yīng) 裝 飾 器 模 式 的 Component , 代 理 模 式 的 RealSubject 對(duì) 應(yīng) 裝 飾 器 模 式 的
ConcreteComponent,代理模式的 Proxy對(duì)應(yīng)裝飾器模式的Decorator。確實(shí),從代碼實(shí)現(xiàn)上看,代
理模式的確與裝飾器模式是一樣的(其實(shí)裝飾器模式就是代理模式的一個(gè)特殊應(yīng)用),但是這兩種設(shè)計(jì)
模式所面向的功能擴(kuò)展面是不一樣的:

裝飾器模式強(qiáng)調(diào)自身功能的擴(kuò)展。Decorator所做的就是增強(qiáng)ConcreteComponent的功能(也有
可能減弱功能),主體對(duì)象為ConcreteComponent,著重類功能的變化;

代理模式強(qiáng)調(diào)對(duì)代理過(guò)程的控制。Proxy 完全掌握對(duì) RealSubject的訪問(wèn)控制,因此,Proxy 可以決定對(duì)RealSubject 進(jìn)行功能擴(kuò)展,功能縮減甚至功能散失(不調(diào)用RealSubject方法),主體對(duì)象為
Proxy;

簡(jiǎn)單來(lái)講,假設(shè)現(xiàn)在小明想租房,那么勢(shì)必會(huì)有一些事務(wù)發(fā)生:房源搜索,聯(lián)系房東談價(jià)格····

假設(shè)我們按照代理模式進(jìn)行思考,那么小明只需找到一個(gè)房產(chǎn)中介,讓他去干房源搜索,聯(lián)系房東
談價(jià)格這些事情,小明只需等待通知然后付點(diǎn)中介費(fèi)就行了;

而如果采用裝飾器模式進(jìn)行思考,因?yàn)檠b飾器模式強(qiáng)調(diào)的是自身功能擴(kuò)展,也就是說(shuō),如果要找房
子,小明自身就要增加房源搜索能力擴(kuò)展,聯(lián)系房東談價(jià)格能力擴(kuò)展,通過(guò)相應(yīng)的裝飾器,提升自身能
力,一個(gè)人做滿所有的事情。

9.5.裝飾器模式的優(yōu)缺點(diǎn)

優(yōu)點(diǎn):

1、裝飾器是繼承的有力補(bǔ)充,比繼承靈活,不改變?cè)袑?duì)象的情況下動(dòng)態(tài)地給一個(gè)對(duì)象擴(kuò)展功能,即插即用。

2、通過(guò)使用不同裝飾類以及這些裝飾類的排列組合,可以實(shí)現(xiàn)不同效果。

3、裝飾器完全遵守開(kāi)閉原則。

缺點(diǎn):

1、會(huì)出現(xiàn)更多的代碼,更多的類,增加程序復(fù)雜性。

2、動(dòng)態(tài)裝飾時(shí),多層裝飾時(shí)會(huì)更復(fù)雜。

那么裝飾器模式我們就講解到這里,希望小伙伴們認(rèn)真體會(huì),加深理解。

9.6.作業(yè)

1、請(qǐng)舉例3-5個(gè)使用門面模式的應(yīng)用場(chǎng)景。

1.解決易用性問(wèn)題

門面模式可以用來(lái)封裝系統(tǒng)的底層實(shí)現(xiàn),隱藏系統(tǒng)的復(fù)雜性,提供一組更加簡(jiǎn)單易用、更高層的接口。

比如,Linux 系統(tǒng)調(diào)用函數(shù)就可以看作一種“門面”。它是 Linux 操作系統(tǒng)暴露給開(kāi)發(fā)者的一組“特殊”的編程接口,它封裝了底層更基礎(chǔ)的 Linux 內(nèi)核調(diào)用。

再比如,Linux 的 Shell 命令,實(shí)際上也可以看作一種門面模式的應(yīng)用。它繼續(xù)封裝系統(tǒng)調(diào)用,提供更加友好、簡(jiǎn)單的命令,讓我們可以直接通過(guò)執(zhí)行命令來(lái)跟操作系統(tǒng)交互。

2.解決性能問(wèn)題

我們通過(guò)將多個(gè)接口調(diào)用替換為一個(gè)門面接口調(diào)用,減少網(wǎng)絡(luò)通信成本,提高 App 客戶端的響應(yīng)速度。

3.解決分布式事務(wù)問(wèn)題

在用戶注冊(cè)的時(shí)候,我們不僅會(huì)創(chuàng)建用戶(在數(shù)據(jù)庫(kù) User 表中),還會(huì)給用戶創(chuàng)建一個(gè)錢包(在數(shù)據(jù)庫(kù)的 Wallet 表中)。用戶注冊(cè)需要支持事務(wù),也就是說(shuō),創(chuàng)建用戶和錢包的兩個(gè)操作,要么都成功,要么都失敗,不能一個(gè)成功、一個(gè)失敗。

最簡(jiǎn)單的解決方案是,利用數(shù)據(jù)庫(kù)事務(wù)或者 Spring 框架提供的事務(wù)(如果是 Java 語(yǔ)言的話),在一個(gè)事務(wù)中,執(zhí)行創(chuàng)建用戶和創(chuàng)建錢包這兩個(gè) SQL 操作。這就要求兩個(gè) SQL 操作要在一個(gè)接口中完成,所以,我們可以借鑒門面模式的思想,再設(shè)計(jì)一個(gè)包裹這兩個(gè)操作的新接口,讓新接口在一個(gè)事務(wù)中執(zhí)行兩個(gè) SQL 操作。

2、使用裝飾器模式實(shí)現(xiàn)一個(gè)可根據(jù)權(quán)限動(dòng)態(tài)擴(kuò)展功能的導(dǎo)航條。

例如:GPer社區(qū)未登錄狀態(tài)下的導(dǎo)航條

image-20200307180017756

GPer社區(qū)登錄狀態(tài)下的導(dǎo)航條

image-20200307180102423

整體思路:用戶分為未登錄會(huì)員、VIP會(huì)員、管理員分別展示不同的導(dǎo)航條。

1.創(chuàng)建抽象導(dǎo)航條類,抽象方法展示導(dǎo)航條。

public abstract class AbstractNavDecorator{
    /**
     * 展示導(dǎo)航條
     * @return 導(dǎo)航條
     */
    protected abstract String showNavs();
}

2.創(chuàng)建基礎(chǔ)導(dǎo)航條類

public class BaseNavDecorator extends AbstractNavDecorator {
    @Override
    public String showNavs() {
        return "問(wèn)答-文章-精品課-冒泡-商城";
    }
}

3.創(chuàng)建導(dǎo)航條裝飾器

public class NavDecorator extends AbstractNav {

    private AbstractNav abstractNav;

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

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

4.創(chuàng)建作業(yè)裝飾器

public class HomeworkNavDecorator extends NavDecorator {

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

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

5.創(chuàng)建題庫(kù)裝飾器

public class QuizNavDecorator extends NavDecorator {

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

    @Override
    public String showNavs() {
        return super.showNavs() + "-題庫(kù)";
    }
}

6.創(chuàng)建成長(zhǎng)墻裝飾器

public class WallNavDecorator extends NavDecorator {

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

    @Override
    public String showNavs() {
        return super.showNavs() + "-成長(zhǎng)墻";
    }
}

7.創(chuàng)建管理裝飾器

public class AdminNavDecorator extends NavDecorator {

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

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

8.創(chuàng)建用戶枚舉

public enum User {
    NOT_LOGIN(1, "未登錄用戶"),
    VIP(2, "會(huì)員"),
    ADMIN(3, "管理員")
    ;

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

    /**
     * 編碼
     */
    private int code;

    /**
     * 用戶類型
     */
    private String type;
}

9.創(chuàng)建根據(jù)用戶創(chuàng)建不同導(dǎo)航條簡(jiǎn)單工廠與測(cè)試類

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("未登錄用戶導(dǎo)航條展示:");
        System.out.println(permissionFactory.showPermission(NOT_LOGIN));
        System.out.println("VIP用戶導(dǎo)航條展示:");
        System.out.println(permissionFactory.showPermission(VIP));
        System.out.println("管理員導(dǎo)航條展示:");
        System.out.println(permissionFactory.showPermission(ADMIN));
    }
}

10.運(yùn)行效果如下:

未登錄用戶導(dǎo)航條展示:
問(wèn)答-文章-精品課-冒泡-商城
VIP用戶導(dǎo)航條展示:
問(wèn)答-文章-精品課-冒泡-商城-作業(yè)-題庫(kù)-成長(zhǎng)墻
管理員導(dǎo)航條展示:
問(wèn)答-文章-精品課-冒泡-商城-作業(yè)-題庫(kù)-成長(zhǎng)墻-管理
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容