聊一聊裝飾者模式

2022-11-25 12:02:12

是你,還是你,一切都有你!—— 裝飾者模式

一、概述

裝飾者模式(Decorator Pattern)允許向一個現有的物件擴充套件新的功能,同時不改變其結構。主要解決直接繼承下因功能的不斷橫向擴充套件導致子類膨脹的問題,無需考慮子類的維護。

裝飾者模式有4種角色:

  1. 抽象構件角色(Component):具體構件類和抽象裝飾者類的共同父類別。
  2. 具體構件角色(ConcreteComponent):抽象構件的子類,裝飾者類可以給它增加額外的職責。
  3. 裝飾角色(Decorator):抽象構件的子類,具體裝飾類的父類別,用於給具體構件增加職責,但在子類中實現。
  4. 具體裝飾角色(ConcreteDecorator):具體裝飾類,定義了一些新的行為,向構件類新增新的特性。

二、入門案例

2.1、類圖

2.2、基礎類介紹

// 抽象構件角色
public interface Component {

    void doSomeThing();
}

// 具體構件角色
public class ConcreteComponent implements Component {

    @Override
    public void doSomeThing() {
        System.out.println("處理業務邏輯");
    }
}

// 裝飾者類
public abstract class Decorator implements Component {

    private Component component;

    public Decorator(Component component) {
        this.component = component;
    }

    @Override
    public void doSomeThing() {
        // 呼叫處理業務邏輯
        component.doSomeThing();
    }
}

// 具體裝飾類
public class ConcreteDecorator extends Decorator {

    public ConcreteDecorator(Component component) {
        super(component);
    }

    @Override
    public void doSomeThing() {
        System.out.println("業務邏輯功能擴充套件");
        super.doSomeThing();
    }
}

當然,如果需要擴充套件更多功能的話,可以再定義其他的ConcreteDecorator類,實現其他的擴充套件功能。
如果只有一個ConcreteDecorator類,那麼就沒有必要建立一個單獨的Decorator類,而可以把Decorator和ConcreteDecorator的責任合併成一個類。

三、應用場景

如風之前在一家保險公司幹過一段時間。其中保險業務員也會在自家產品註冊賬號,進行推銷。不過在這之前,他們需要經過培訓,匯入一張展業資格證書。然後再去推銷保險產品供使用者下單,自己則通過推銷產生的業績,參與分潤,拿對應的佣金。

對於上面導證書這個場景,實際上是會根據不同的保險產品,匯入不同的證書的。並且證書的型別也不同,對應的解析、校驗、執行的業務場景都是不同的。如何去實現呢?當然if-else確實也是一種不錯的選擇。下面放一段虛擬碼

/**
 * @author 往事如風
 * @version 1.0
 * @date 2022/11/17 11:32
 * @description
 */
@RestController
@RequestMapping("/certificate")
public class CertificateController {

    @Resource
    private CommonCertificateService certificateService;

    @PostMapping("/import")
    public Result<Integer> importFile(@RequestParam MultipartFile file, @RequestParam String productCode) {
        return Result.success(certificateService.importCertificate(file, productCode));
    }
}

/**
 * @author 往事如風
 * @version 1.0
 * @date 2022/11/17 13:25
 * @description
 */
@Service
public class CommonCertificateService {

    public Integer importCertificate(MultipartFile file, String productCode) {
        // 1、引數非空校驗
        // 2、通過file字尾判斷file型別,支援excel和pdf
        // 3、解析file檔案,獲取資料,統一封裝到定義的CertificatePojo類中
        // 4、根據產品型別判斷匯入之前的業務邏輯
        if (productCode.equals(DecorateConstants.PRODUCT_A)) {
            // 重新計算業績邏輯
            // 重新算業績型別邏輯
            // 一坨坨程式碼去實現....
        }
        else if (productCode.equals(DecorateConstants.PRODUCT_B)) {
            // 匯入證書的代理人自己以及上級身份晉升邏輯
            // 業績計算邏輯
            // 一坨坨程式碼去實現...
        } else if (productCode.equals(DecorateConstants.PRODUCT_C)) {
            // c產品下的業務邏輯
            // 一坨坨程式碼去實現...
        } else {
            // 預設的處理邏輯
            // 一坨坨程式碼去實現...
        }
        // 5、證書資料儲存
        // 6、代理人資訊儲存
        // 7、相關流水資料儲存
        // 返回代理人id
        Integer agentId = Integer.MAX_VALUE;
        return agentId;
    }
}

從上面的虛擬碼看到,所有的業務邏輯是在一起處理的,通過productCode去處理對應產品的相關邏輯。這麼一看,好像也沒毛病,但是還是被技術大佬給否決了。好吧,如風決定重寫。運用裝飾者模式,重新處理下了下這段程式碼。
1、一切再從註解出發,自定義Decorate註解,這裡定義2個屬性,scene和type

  • scene:標記具體的業務場景
  • type:表示在該種業務場景下,定義一種具體的裝飾器類
/**
 * @author 往事如風
 * @version 1.0
 * @date 2022/11/8 17:44
 * @description
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Service
public @interface Decorate {
     /**
      * 具體的業務場景
      * @return
      */
     String scene();
     /**
      * 型別:不同業務場景下,不同的裝飾器型別
      * @return
      */
     String type();
}

2、抽象構件介面,BaseHandler,這個是必須滴

/**
 * @author 往事如風
 * @version 1.0
 * @date 2022/11/8 17:07
 * @description 抽象處理介面
 */
public interface BaseHandler<T, R> {
    /**
     * 統一的處理方法
     * @param t
     * @return
     */
    R handle(T t);
}

3、抽象裝飾器類,AbstractHandler,持有一個被裝飾類的參照,這個參照具體在執行時被指定

/**
 * @author 往事如風
 * @version 1.0
 * @date 2022-11-13 22:10:05
 * @desc 抽象父類別
 */
public abstract class AbstractHandler<T, R> implements BaseHandler<T, R> {
    protected BaseHandler service;

    public void setService(BaseHandler service) {
        this.service = service;
    }
}

4、具體的裝飾器類AProductServiceDecorate,主要負責處理「導師證書」這個業務場景下,A產品相關的匯入邏輯,並且標記了自定義註解Decorate,表示該類是裝飾器類。主要負責對A產品證書匯入之前邏輯的增強,我們這裡稱之為「裝飾」。

/**
 * @author 往事如風
 * @version 1.0
 * @date 2022-11-13 23:11:16
 * @desc
 */
@Decorate(scene = SceneConstants.CERTIFICATE_IMPORT, type = DecorateConstants.PRODUCT_A)
public class AProductServiceDecorate extends AbstractHandler<MultipartFile, Integer> {

    /**
     * 重寫父類別處理資料方法
     * @param file
     * @return
     */
    @Override
    public Integer handle(MultipartFile file) {
        // 解析
        CertificatePojo data = parseData(file);
        // 校驗
        check(data);
        // 業績計算
        calAchievement(data.getMobile());
        return (Integer) service.handle(data);
    }

    public CertificatePojo parseData(MultipartFile file) {
        // file,證書解析
        System.out.println("A產品的證書解析......");
        CertificatePojo certificatePojo = new CertificatePojo();
        certificatePojo.setMobile("12323");
        certificatePojo.setName("張三");
        certificatePojo.setMemberNo("req_343242ds");
        certificatePojo.setEffectDate("2022-10-31:20:20:10");
        return certificatePojo;
    }

    /**
     * 證書資料校驗
     * @param data
     */
    public void check(CertificatePojo data) {
        // 資料規範和重複性校驗
        // .....
        System.out.println("A證書資料校驗......");
    }

    /**
     * 計算業績資訊
     */
    private void calAchievement(String mobile) {
        System.out.println("查詢使用者資訊, 手機號:" + mobile);
        System.out.println("重新計算業績...");
    }
}

當然,還是其他裝飾類,BProductServiceDecorateCProductServiceDecorate等等,負責裝飾其他產品,這裡就不舉例了。
5、當然還有管理裝飾器類的裝飾器類管理器DecorateManager,內部維護一個map,負責存放具體的裝飾器類

/**
 * @author 往事如風
 * @version 1.0
 * @date 2022/11/15 17:18
 * @description 裝飾管理器
 */
public class DecorateManager {

    /**
     * 用於存放裝飾器類
     */
    private Map<String, AbstractHandler> decorateHandleMap = new HashMap<>();

    /**
     * 將具體裝飾器類放在map中
     *
     * @param handlerList
     */
    public void setDecorateHandler(List<AbstractHandler> handlerList) {
        for (AbstractHandler h : handlerList) {
            Decorate annotation = AnnotationUtils.findAnnotation(h.getClass(), Decorate.class);
            decorateHandleMap.put(createKey(annotation.scene(), annotation.type()), h);
        }
    }

    /**
     * 返回具體的裝飾器類
     *
     * @param type
     * @return
     */
    public AbstractHandler selectHandler(String scene, String type) {
        String key = createKey(scene, type);
        return decorateHandleMap.get(key);
    }

    /**
     * 拼接map的key
     * @param scene
     * @param type
     * @return
     */
    private String createKey(String scene, String type) {
        return StrUtil.builder().append(scene).append(":").append(type).toString();
    }
}

6、用了springboot,當然需要將這個管理器交給spring的bean容器去管理,需要建立一個設定類DecorateAutoConfiguration

/**
 * @author 往事如風
 * @version 1.0
 * @date 2022-11-12 19:22:41
 * @desc
 */
@Configuration
public class DecorateAutoConfiguration {

    @Bean
    public DecorateManager handleDecorate(List<AbstractHandler> handlers) {
        DecorateManager manager = new DecorateManager();
        manager.setDecorateHandler(handlers);
        return manager;
    }
}

7、被裝飾的service類,CertificateService,只需要關注自己的核心邏輯就可以

/**
 * @author 往事如風
 * @version 1.0
 * @date 2022/11/8 17:10
 * @description 執行證書匯入的service
 */
@Service
public class CertificateService implements BaseHandler<CertificatePojo, Integer> {

    /**
     * 處理匯入證書的核心邏輯service
     * @param certificate
     * @return
     */
    @Override
    public Integer handle(CertificatePojo certificate) {
        System.out.println("核心業務,證書資料:" + JSONUtil.toJsonStr(certificate));
        // 1、證書資料儲存
        // 2、代理人資訊儲存
        // 3、相關流水資料儲存
        // 其他的一些列核心操作
        Integer agentId = Integer.MAX_VALUE;
        // 返回代理人id
        return agentId;
    }
}

8、在原來的controller中,注入管理器類DecorateManager去呼叫,以及service,也就是被裝飾的類。首先拿到裝飾器,然後再通過setService方法,傳入被裝飾的service。也就是具體裝飾什麼類,需要在執行時才確定。

/**
 * @author 往事如風
 * @version 1.0
 * @date 2022-11-13 23:30:37
 * @desc
 */
@RestController
public class WebController {

    @Resource
    private DecorateManager decorateManager;

    @Resource
    private CertificateService certificateService;

    @PostMapping("/import")
    public Result importFile(@RequestParam MultipartFile file, @RequestParam String productCode) {
        AbstractHandler handler = decorateManager.selectHandler(SceneConstants.CERTIFICATE_IMPORT, productCode);
        if (Objects.isNull(handler)) {
            return Result.fail();
        }
        handler.setService(certificateService);
        return Result.success(handler.handle(file));
    }
}

下面模擬下代理人匯入證書的流程,當選擇A產品,productCode傳A過來,後端的處理流程。

  • 對於A產品下,證書的解析,A產品傳的是excel
  • 然後資料校驗,這個產品下,特有的資料校驗
  • 最後是核心的業績重算,只有A產品才會有這個邏輯

當選擇B產品,productCode傳A過來,後端的處理流程。

  • 對於B產品下,證書的解析,A產品傳的是pdf
  • 然後資料校驗,跟A產品也不同,多了xxx步驟
  • 核心是代理人的晉升處理,這部分是B產品獨有的

最後說一句,既然都用springboot了,這塊可以寫一個starter,做一個公用的裝飾器模式。如果哪個服務需要用到,依賴這個裝飾器的starter,然後標記Decorate註解,定義對應的scene和type屬性,就可以直接使用了。

四、原始碼中運用

4.1、JDK原始碼中的運用

來看下IO流中,InputStreamFilterInputStreamFileInputStreamBufferedInputStream的一段程式碼

public abstract class InputStream implements Closeable {

    public abstract int read() throws IOException;


    public int read(byte b[], int off, int len) throws IOException {
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return 0;
        }

        int c = read();
        if (c == -1) {
            return -1;
        }
        b[off] = (byte)c;

        int i = 1;
        try {
            for (; i < len ; i++) {
                c = read();
                if (c == -1) {
                    break;
                }
                b[off + i] = (byte)c;
            }
        } catch (IOException ee) {
        }
        return i;
    }
}
//--------------------------
public class FilterInputStream extends InputStream {
   
    protected FilterInputStream(InputStream in) {
        this.in = in;
    }
    public int read() throws IOException {
        return in.read();
    }
}

//--------------------------
public class BufferedInputStream extends FilterInputStream {
    public BufferedInputStream(InputStream in, int size) {
        super(in);
        if (size <= 0) {
            throw new IllegalArgumentException("Buffer size <= 0");
        }
        buf = new byte[size];
    }

    public BufferedInputStream(InputStream in) {
        this(in, DEFAULT_BUFFER_SIZE);
    }
    
    public int read() throws IOException {
        return in.read();
    }

    public int read(byte b[], int off, int len) throws IOException {
        return in.read(b, off, len);
    }
}

//--------------------------
public class FileInputStream extends InputStream {
    public int read(byte b[]) throws IOException {
        return readBytes(b, 0, b.length);
    }
}


再來看下這幾個類的類圖

這些類的程式碼有刪改,可以看到BufferedInputStream中定義了很多屬性,這些資料都是為了可緩衝讀取來作準備的,看到其有構造方法會傳入一個InputStream的範例。實際編碼如下

//被裝飾的物件,檔案輸入流
InputStream in=new FileInputStream("/data/log/app.log");
//裝飾物件,可緩衝
InputStream bufferedIn=new BufferedInputStream(in);
bufferedIn.read();

這裡覺得很眼熟吧,其實已經運用了裝飾模式了。

4.2、mybatis原始碼中的運用

在mybatis中,有個介面Executor,顧名思義這個介面是個執行器,它底下有許多實現類,如CachingExecutorSimpleExecutorBaseExecutor等等。類圖如下:

主要看下CachingExecutor類,看著很眼熟,很標準的裝飾器。其中該類中的update是裝飾方法,在呼叫真正update方法之前,會執行重新整理本地快取的方法,對原來的update做增強和擴充套件。

public class CachingExecutor implements Executor {

  private final Executor delegate;
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  public CachingExecutor(Executor delegate) {
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }

  @Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    // 增強內容
    // 修改方法就要清空原生的快取
    flushCacheIfRequired(ms);
    // 呼叫原有的方法
    return delegate.update(ms, parameterObject);
  }
}    

再來看下BaseExecutor類,這裡有一個update方法,這個是原本的被裝飾的update方法。然後再看這個原本的update方法,它呼叫的doUpdate方法是個抽象方法,用protected修飾。咦,這不就是模板方法麼,關於模板方法模式,這裡就不展開贅述了。

public abstract class BaseExecutor implements Executor {
  protected Executor wrapper;
  
  @Override
  public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    clearLocalCache();
    return doUpdate(ms, parameter);
  }
  protected abstract int doUpdate(MappedStatement ms, Object parameter)
      throws SQLException;
}

五、總結

優點

  1. 通過組合而非繼承的方式,動態地擴充套件一個物件的功能,在執行時可以選擇不同的裝飾器從而實現不同的功能。
  2. 有效的避免了使用繼承的方式擴充套件物件功能而帶來的靈活性差、子類無限制擴張的問題。
  3. 具體元件類與具體裝飾類可以獨立變化,使用者可以根據需要新增具體元件類跟裝飾類,在使用時在對其進行組合,原有程式碼無須改變,符合"開閉原則"。

缺點

  1. 這種比繼承更加靈活機動的特性,也同時意味著更加多的複雜性。
  2. 裝飾模式會導致設計中出現許多小類 (I/O 類中就是這樣),如果過度使用,會使程式變得很複雜。
  3. 裝飾模式是針對抽象元件(Component)型別程式設計。但是,如果你要針對具體元件程式設計時,就應該重新思考你的應用架構,以及裝飾者是否合適。

六、參考原始碼

程式設計檔案:
https://gitee.com/cicadasmile/butte-java-note

應用倉庫:
https://gitee.com/cicadasmile/butte-flyer-parent