百度工程師教你玩轉設計模式(裝飾器模式)

語言: CN / TW / HK

作者 | 北極星小組

想要寫好程式碼,設計模式(Design Pattern)是必不可少的基本功,設計模式是對面向物件設計(Object Oriented Design)中反覆出現的一類問題的一種解決方案,本篇介紹裝飾器模式(Decorator Pattern)。

在我們日常的開發過程中,一個最常見的場景就是在已有的基礎上新增功能,常規的做法有以下幾種:

  • 修改已有的類:違背開閉原則。

  • 增加新的子類:每次都得新增大量對應的類,隨著功能的增加,子類越來越膨脹。

在此場景下,裝飾器模式就可以體現出它的優勢了,它允許在不修改原有物件的前提下,靈活的擴充套件已有類的功能。下面是裝飾器模式的一個通用的類圖:

圖片 △UML

其中的各個類的作用如下:

  • 抽象元件(Component): 可以是介面或者抽象類,它定義了具體類以及裝飾器所擁有的方法。

  • 具體元件(ComponentA, ComponentB):具體的元件,實現或者繼承自抽象元件。可以理解成上述場景中已存在的類。

  • 抽象裝飾器(Decorator): 通常為抽象類,持有一個被裝飾的物件,定義了具體裝飾器的方法。此類非必須也可以沒有,具體裝飾器也可直接繼承或者實現抽象元件。

  • 具體裝飾器(DecoratorX, DecoratorY): 具體的裝飾器,繼承自抽象裝飾器(也可直接繼承自抽象元件),擴充套件了抽象元件的某些功能。

下面,將通過3個具體的案例的講解裝飾器的使用方式,方便大家進一步的理解。

一、裝飾器在任務處理場景的應用

在實際的開發中,我們經常需要定義不同的類來處理各種不同的任務。假設一個這樣的場景,我們的系統有多個具體的類,用來處理不同型別的任務。現在需要新增一個功能,就是在處理完任務後發出一條訊息。針對這個場景,使用裝飾器模式的實現思路如下:

  • 抽象元件(TaskProcessor):處理任務的抽象類(亦可通過介面實現),定義一個通用的任務處理方法process()。

  • 具體元件(TaskProcessorA, TaskProcessorB): 負責實現具體的任務處理邏輯

  • 抽象裝飾器(TaskProcessDecorator):持有一個任務處理物件例項

  • 具體裝飾器(AfterTaskProcessDecorator):實現具體的任務處理完成後的訊息通知擴充套件能力

具體的程式碼如下:

package com.baidu.demo;
public class Decorator {
    // 抽象元件
    static abstract class TaskProcessor {
        abstract void process();
    }
    // 具體元件
    static class TaskProcessorA extends TaskProcessor {
        @Override
        void process() {
            System.out.println("TaskProcessorA處理完成");
        }
    }
    // 具體元件
    static class TaskProcessorB extends TaskProcessor {
        @Override
        void process() {
            System.out.println("TaskProcessorB處理完成");
        }
    }
    // 抽象裝飾器
    static abstract class TaskProcessDecorator extends TaskProcessor {
        protected TaskProcessor processor;
        public TaskProcessDecorator(TaskProcessor processor) {
            this.processor = processor;
        }
        abstract void process();
    }
    // 具體裝飾器
    static class AfterTaskProcessDecorator extends TaskProcessDecorator {
        public AfterTaskProcessDecorator(TaskProcessor processor) {
            super(processor);
        }

        @Override
        void process() {
            processor.process();
            afterProcess();
        }

        void afterProcess() {
            System.out.println("任務處理完畢,傳送訊息...");
        }
    }

    public static void main(String[] args) {
        // 擴充套件之前
        System.out.println("==========before==========");
        TaskProcessor processorA = new TaskProcessorA();
        processorA.process();
        TaskProcessor processorB = new TaskProcessorB();
        processorB.process();

        // 裝飾器擴充套件之後:TaskProcessorA TaskProcessorB並未做任何修改,即可實現功能的擴充套件
        System.out.println("==========after==========");
        TaskProcessor decoratorA = new AfterTaskProcessDecorator(processorA);
        decoratorA.process();
        TaskProcessor decoratorB = new AfterTaskProcessDecorator(processorB);
        decoratorB.process();
    }
}

// 輸出結果如下
==========before==========
TaskProcessorA處理完成
TaskProcessorB處理完成
==========after==========
TaskProcessorA處理完成
任務處理完畢,傳送訊息...
TaskProcessorB處理完成
任務處理完畢,傳送訊息...

二、裝飾器在檔案IO場景的應用

裝飾器模式,一個典型的應用就是檔案IO操作,最基礎的類實現位元組流讀取類,使用裝飾器模式可以封裝檔案位元組流讀取類,然後可以繼續封裝可快取的檔案位元組流讀取類,在專案中按需使用。具體實現如下:

  • InputStream:具體元件,實現讀取位元組流。

  • FileInputStream:具體裝飾器,作為InputStream的子類,擴充套件檔案操作。

  • BufferedInputStream:具體裝飾器,作為FileInputStream的子類,擴充套件快取操作。

具體程式碼如下:

//具體元件,實現讀取位元組流
public abstract class InputStream {
    public int read(byte b[], int off, int len) {}
}

//具體裝飾器,作為InputStream的子類,擴充套件檔案操作
public class FileInputStream extends InputStream {
    protected InputStream in;
    
    public FileInputStream(String name) {
        InputStream in = ... //此處省略,通過檔名建立物件
        this.in = in;
    }
    
    public int read(byte b[], int off, int len) {
        return this.in.read(b, off, len);
    }
}

//具體裝飾器,作為FileInputStream的子類,擴充套件快取操作
public class BufferedInputStream extends FileInputStream {
    protected FileInputStream in;
    protected byte[] buffer;
    
    public BufferedInputStream(FileInputStream in) {
        this.in = in;
    }
    
    public int read(byte b[], int off, int len) {
        if (this.buffer == null || this.buffer.length == 0) {
            this.in.read(this.buffer, 0, in.lenght());
        }
        
        System.arraycopy(this.buffer, off, b, 0, len);
        ...
    }
}

public static void main(String[] args) {
    FileInputStream fs = new FileInputStream('./test.log');
    BufferedInputStream bs = new BufferedInputStream(fs);
    
    byte[] b;
    bs.read(b, 0, 1);
}

三、裝飾器在日誌系統場景的應用

在日誌系統中,一般常用日誌的級別分別為 DEBUG(除錯)、INFO(執行資訊)、WARN(警告)、ERROR(錯誤),一旦發生錯誤級別的日誌後,則需要觸發報警通知相關人員及時進行跟進,報警方式一般有:郵件、簡訊、如流等,通常我們會根據業務場景以組合的方式進行報警通知,使用裝飾器模式則能很好實現組合報警這一功能。

  • 抽象元件:Log介面抽象

  • 具體元件:Slf4j 具體日誌類的實現

  • 抽象裝飾器:LogDecorator 日誌裝飾器的基類

  • 具體裝飾器:MailLogDecorator、SMSLogDecorator、InfoFlowLogDecorator具體裝飾類

/**
 * 日誌介面
 */
public interface Log {
    void debug(String message);
    void info(String message);
    void warn(String message);
    void error(String message);
}

/**
 * Slf4j 日誌
 */
public class Slf4jLog implements Log {

    //日誌記錄物件
    private final Logger log = LoggerFactory.getLogger("system_log");

    @Override
    public void debug(String message) {
        if (log.isDebugEnabled()) {
             log.debug(message);
        }
    }

    @Override
    public void info(String message) {
        if (log.isInfoEnabled()) {
              log.info(message);
        }
    }

    @Override
    public void warn(String message) {
        if (log.isWarnEnabled()) {
            log.warn(message);
        }
    }

    @Override
    public void error(String message) {
        if (log.isErrorEnabled()) {
            log.error(message);
        }
    }
}

/**
 * 日誌裝飾器
 */
public class LogDecorator implements Log {
    protected Log log;

    public LogDecorator(Log log) {
        this.log = log;
    }

    @Override
    public void debug(String message) {
        log.debug(message);
    }

    @Override
    public void info(String message) {
        log.info(message);
    }

    @Override
    public void warn(String message) {
        log.warn(message);
    }

    @Override
    public void error(String message) {
        log.error(message);
    }
}

/**
 * 郵件日誌裝飾器
 */
public class MailLogDecorator extends LogDecorator {
    public MailLogDecorator(Log log) {
        super(log);
    }

    @Override
    public void warn(String message) {
        log.warn(message);
        mail(message);
    }

    @Override
    public void error(String message) {
        log.error(message);
        mail(message);
    }
    
    public void mail(String message) {
        //模擬郵件傳送
        log.info("郵件已傳送,資訊:" + message);
    }
}

/**
 * 簡訊日誌裝飾器
 */
public class SMSLogDecorator extends LogDecorator {
    public SMSLogDecorator(Log log) {
        super(log);
    }
    
    @Override
    public void error(String message) {
        log.error(message);
        send(message);
    }

    public void send(String message) {
        //模擬簡訊傳送
        log.info("簡訊已傳送,資訊:" + message);
    }
}

/**
 * 如流日誌裝飾器
 */
public class InfoflowLogDecorator extends LogDecorator {
    public InfoflowLogDecorator(Log log) {
        super(log);
    }

    @Override
    public void warn(String message) {
        log.warn(message);
        send(message);
    }
    
    @Override
    public void error(String message) {
        log.error(message);
        send(message);
    }

    
    public void send(String message) {
        //模擬如流傳送
        log.info("如流訊息已傳送,資訊:" + message);
    }
}

/**
 * 日誌測試類
 */
public class LogTest {

    /**
     * 測試日誌裝飾器
     */
    @Test
    public void testLogDecorator() {
        Log log = new SMSLogDecorator(new InfoFlowLogDecorator(new MailLogDecorator(new Slf4jLog())));
        log.debug("系統除錯開啟");
        log.info("系統正常執行");
        log.warn("資料為空警告");
        log.error("db 連線錯誤");
    }
}
===========output=========
15:16:56.564 [main] DEBUG system_log - 系統除錯開啟
15:16:56.566 [main] INFO  system_log - 系統正常執行
15:16:56.566 [main] WARN  system_log - 資料為空警告
15:16:56.566 [main] INFO  system_log - 郵件已傳送,資訊:資料為空警告
15:16:56.566 [main] INFO  system_log - 如流訊息已傳送,資訊:資料為空警告
15:16:56.566 [main] ERROR system_log - db 連線錯誤
15:16:56.566 [main] INFO  system_log - 郵件已傳送,資訊:db 連線錯誤
15:16:56.566 [main] INFO  system_log - 如流訊息已傳送,資訊:db 連線錯誤
15:16:56.566 [main] INFO  system_log - 簡訊已傳送,資訊:db 連線錯誤

Process finished with exit code 0

四、總結

如上幾個案例,裝飾器的最大作用就是在不修改原有類的基礎上擴充套件已有的功能,它符合開閉原則,而且實現也比較靈活。

---------- END ----------

推薦閱讀【技術加油站】系列:

百度工程師教你玩轉設計模式(工廠模式)

百度工程師教你玩轉設計模式(介面卡模式)

百度工程師教你玩轉設計模式(單例模式)

圖片