聊一聊裝飾者模式

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

一、概述

裝飾者模式(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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章