重學 Java 設計模式:實戰裝飾器模式(SSO單點登入功能擴充套件,增加攔截使用者訪問方法範圍場景)

小傅哥發表於2020-06-10

作者:小傅哥
部落格:https://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收穫!?

一、前言

對於程式碼你有程式設計感覺嗎

很多人寫程式碼往往是沒有程式設計感覺的,也就是除了可以把功能按照固定的流程編寫出流水式的程式碼外,很難去思考整套功能服務的擴充套件性和可維護性。尤其是在一些較大型的功能搭建上,比較缺失一些駕馭能力,從而導致最終的程式碼相對來說不能做到盡善盡美。

江洋大盜與江洋大偷

兩個本想描述一樣的意思的詞,只因一字只差就讓人覺得一個是好牛,一個好搞笑。往往我們去開發程式設計寫程式碼時也經常將一些不恰當的用法用於業務需求實現中,當卻不能意識到。一方面是由於編碼不多缺少較大型專案的實踐,另一方面是不思進取的總在以完成需求為目標缺少精益求精的工匠精神。

書從來不是看的而是用的

在這個學習資料幾乎爆炸的時代,甚至你可以輕易就獲取幾個T的視訊,小手輕輕一點就收藏一堆文章,但卻很少去看。學習的過程從不只是簡單的看一遍就可以,對於一些實操性的技術書籍,如果真的希望學習到知識,那麼一定是把這本書用起來而絕對不是看起來。

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程三個,可以通過關注公眾號bugstack蟲洞棧,回覆原始碼下載獲取(開啟獲取的連結,找到序號18)
工程 描述
itstack-demo-design-9-00 場景模擬工程;模擬單點登入類
itstack-demo-design-9-01 使用一坨程式碼實現業務需求
itstack-demo-design-9-02 通過設計模式優化改造程式碼,產生對比性從而學習

三、裝飾器模式介紹

裝飾器模式,圖片來自 refactoringguru.cn

初看上圖感覺裝飾器模式有點像俄羅斯套娃、某眾汽車?,而裝飾器的核心就是再不改原有類的基礎上給類新增功能。不改變原有類,可能有的小夥伴會想到繼承、AOP切面,當然這些方式都可以實現,但是使用裝飾器模式會是另外一種思路更為靈活,可以避免繼承導致的子類過多,也可以避免AOP帶來的複雜性。

你熟悉的場景很多用到裝飾器模式

new BufferedReader(new FileReader(""));,這段程式碼你是否熟悉,相信學習java開發到位元組流、字元流、檔案流的內容時都見到了這樣的程式碼,一層巢狀一層,一層巢狀一層,位元組流轉字元流等等,而這樣方式的使用就是裝飾器模式的一種體現。

四、案例場景模擬

場景模擬;單點登入功能擴充套件

在本案例中我們模擬一個單點登入功能擴充的場景

一般在業務開發的初期,往往內部的ERP使用只需要判斷賬戶驗證即可,驗證通過後即可訪問ERP的所有資源。但隨著業務的不斷髮展,團隊裡開始出現專門的運營人員、營銷人員、資料人員,每個人員對於ERP的使用需求不同,有些需要建立活動,有些只是檢視資料。同時為了保證資料的安全性,不會讓每個使用者都有最高的許可權。

那麼以往使用的SSO是一個元件化通用的服務,不能在裡面新增需要的使用者訪問驗證功能。這個時候我們就可以使用裝飾器模式,擴充原有的單點登入服務。但同時也保證原有功能不受破壞,可以繼續使用。

1. 場景模擬工程

itstack-demo-design-9-00
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── HandlerInterceptor.java
                └── SsoInterceptor.java
  • 這裡模擬的是spring中的類:HandlerInterceptor,實現起介面功能SsoInterceptor模擬的單點登入攔截服務。
  • 為了避免引入太多spring的內容影響對設計模式的閱讀,這裡使用了同名的類和方法,儘可能減少外部的依賴。

2. 場景簡述

2.1 模擬Spring的HandlerInterceptor

public interface HandlerInterceptor {

    boolean preHandle(String request, String response, Object handler);

}
  • 實際的單點登入開發會基於;org.springframework.web.servlet.HandlerInterceptor 實現。

2.2 模擬單點登入功能

public class SsoInterceptor implements HandlerInterceptor{

    public boolean preHandle(String request, String response, Object handler) {
        // 模擬獲取cookie
        String ticket = request.substring(1, 8);
        // 模擬校驗
        return ticket.equals("success");
    }

}
  • 這裡的模擬實現非常簡單只是擷取字串,實際使用需要從HttpServletRequest request物件中獲取cookie資訊,解析ticket值做校驗。
  • 在返回的裡面也非常簡單,只要獲取到了success就認為是允許登入。

五、用一坨坨程式碼實現

此場景大多數實現的方式都會採用繼承類

繼承類的實現方式也是一個比較通用的方式,通過繼承後重寫方法,併發將自己的邏輯覆蓋進去。如果是一些簡單的場景且不需要不斷維護和擴充套件的,此類實現並不會有什麼,也不會導致子類過多。

1. 工程結構

itstack-demo-design-9-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── LoginSsoDecorator.java
  • 以上工程結構非常簡單,只是通過 LoginSsoDecorator 繼承 SsoInterceptor,重寫方法功能。

2. 程式碼實現

public class LoginSsoDecorator extends SsoInterceptor {

    private static Map<String, String> authMap = new ConcurrentHashMap<String, String>();

    static {
        authMap.put("huahua", "queryUserInfo");
        authMap.put("doudou", "queryUserInfo");
    }

    @Override
    public boolean preHandle(String request, String response, Object handler) {
        // 模擬獲取cookie
        String ticket = request.substring(1, 8);
        // 模擬校驗
        boolean success = ticket.equals("success");

        if (!success) return false;

        String userId = request.substring(9);
        String method = authMap.get(userId);

        // 模擬方法校驗
        return "queryUserInfo".equals(method);
    }

}
  • 以上這部分通過繼承重寫方法,將個人可訪問哪些方法的功能新增到方法中。
  • 以上看著程式碼還算比較清晰,但如果是比較複雜的業務流程程式碼,就會很混亂。

3. 測試驗證

3.1 編寫測試類

@Test
public void test_LoginSsoDecorator() {
    LoginSsoDecorator ssoDecorator = new LoginSsoDecorator();
    String request = "1successhuahua";
    boolean success = ssoDecorator.preHandle(request, "ewcdqwt40liuiu", "t");
    System.out.println("登入校驗:" + request + (success ? " 放行" : " 攔截"));
}
  • 這裡模擬的相當於登入過程中的校驗操作,判斷使用者是否可登入以及是否可訪問方法。

3.2 測試結果

登入校驗:1successhuahua 攔截

Process finished with exit code 0
  • 從測試結果來看滿足我們的預期,已經做了攔截。如果你在學習的過程中,可以嘗試模擬單點登入並繼承擴充套件功能。

六、裝飾器模式重構程式碼

接下來使用裝飾器模式來進行程式碼優化,也算是一次很小的重構。

裝飾器主要解決的是直接繼承下因功能的不斷橫向擴充套件導致子類膨脹的問題,而是用裝飾器模式後就會比直接繼承顯得更加靈活同時這樣也就不再需要考慮子類的維護。

在裝飾器模式中有四個比較重要點抽象出來的點;

  1. 抽象構件角色(Component) - 定義抽象介面
  2. 具體構件角色(ConcreteComponent) - 實現抽象介面,可以是一組
  3. 裝飾角色(Decorator) - 定義抽象類並繼承介面中的方法,保證一致性
  4. 具體裝飾角色(ConcreteDecorator) - 擴充套件裝飾具體的實現邏輯

通過以上這四項來實現裝飾器模式,主要核心內容會體現在抽象類的定義和實現上。

1. 工程結構

itstack-demo-design-9-02
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── LoginSsoDecorator.java
                └── SsoDecorator.java

裝飾器模式模型結構

裝飾器模式模型結構

  • 以上是一個裝飾器實現的類圖結構,重點的類是SsoDecorator,這個類是一個抽象類主要完成了對介面HandlerInterceptor繼承。
  • 當裝飾角色繼承介面後會提供建構函式,入參就是繼承的介面實現類即可,這樣就可以很方便的擴充套件出不同功能元件。

2. 程式碼實現

2.1 抽象類裝飾角色

public abstract class SsoDecorator implements HandlerInterceptor {

    private HandlerInterceptor handlerInterceptor;

    private SsoDecorator(){}

    public SsoDecorator(HandlerInterceptor handlerInterceptor) {
        this.handlerInterceptor = handlerInterceptor;
    }

    public boolean preHandle(String request, String response, Object handler) {
        return handlerInterceptor.preHandle(request, response, handler);
    }

}
  • 在裝飾類中有兩個重點的地方是;1)繼承了處理介面、2)提供了建構函式、3)覆蓋了方法preHandle
  • 以上三個點是裝飾器模式的核心處理部分,這樣可以踢掉對子類繼承的方式實現邏輯功能擴充套件。

2.2 裝飾角色邏輯實現

public class LoginSsoDecorator extends SsoDecorator {

    private Logger logger = LoggerFactory.getLogger(LoginSsoDecorator.class);

    private static Map<String, String> authMap = new ConcurrentHashMap<String, String>();

    static {
        authMap.put("huahua", "queryUserInfo");
        authMap.put("doudou", "queryUserInfo");
    }

    public LoginSsoDecorator(HandlerInterceptor handlerInterceptor) {
        super(handlerInterceptor);
    }

    @Override
    public boolean preHandle(String request, String response, Object handler) {
        boolean success = super.preHandle(request, response, handler);
        if (!success) return false;
        String userId = request.substring(8);
        String method = authMap.get(userId);
        logger.info("模擬單點登入方法訪問攔截校驗:{} {}", userId, method);
        // 模擬方法校驗
        return "queryUserInfo".equals(method);
    }
}
  • 在具體的裝飾類實現中,繼承了裝飾類SsoDecorator,那麼現在就可以擴充套件方法;preHandle
  • preHandle的實現中可以看到,這裡只關心擴充套件部分的功能,同時不會影響原有類的核心服務,也不會因為使用繼承方式而導致的多餘子類,增加了整體的靈活性。

3. 測試驗證

3.1 編寫測試類

@Test
public void test_LoginSsoDecorator() {
    LoginSsoDecorator ssoDecorator = new LoginSsoDecorator(new SsoInterceptor());
    String request = "1successhuahua";
    boolean success = ssoDecorator.preHandle(request, "ewcdqwt40liuiu", "t");
    System.out.println("登入校驗:" + request + (success ? " 放行" : " 攔截"));
}
  • 這裡測試了對裝飾器模式的使用,通過透傳原有單點登入類new SsoInterceptor(),傳遞給裝飾器,讓裝飾器可以執行擴充的功能。
  • 同時對於傳遞者和裝飾器都可以是多組的,在一些實際的業務開發中,往往也是由於太多型別的子類實現而導致不易於維護,從而使用裝飾器模式替代。

3.2 測試結果

23:50:50.796 [main] INFO  o.i.demo.design.LoginSsoDecorator - 模擬單點登入方法訪問攔截校驗:huahua queryUserInfo
登入校驗:1successhuahua 放行

Process finished with exit code 0
  • 結果符合預期,擴充套件了對方法攔截的校驗性。
  • 如果你在學習的過程中有用到過單點登陸,那麼可以適當在裡面進行擴充套件裝飾器模式進行學習使用。
  • 另外,還有一種場景也可以使用裝飾器。例如;你之前使用某個實現某個介面接收單個訊息,但由於外部的升級變為傳送list集合訊息,但你又不希望所有的程式碼類都去修改這部分邏輯。那麼可以使用裝飾器模式進行適配list集合,給使用者依然是for迴圈後的單個訊息。

七、總結

  • 使用裝飾器模式滿足單一職責原則,你可以在自己的裝飾類中完成功能邏輯的擴充套件,而不影響主類,同時可以按需在執行時新增和刪除這部分邏輯。另外裝飾器模式與繼承父類重寫方法,在某些時候需要按需選擇,並不一定某一個就是最好。
  • 裝飾器實現的重點是對抽象類繼承介面方式的使用,同時設定被繼承的介面可以通過建構函式傳遞其實現類,由此增加擴充套件性並重寫方法裡可以實現此部分父類實現的功能。
  • 就像夏天熱你穿短褲,冬天冷你穿棉褲,雨天挨澆你穿雨衣一樣,你的根本本身沒有被改變,而你的需求卻被不同的裝飾而實現。生活中往往比比皆是設計,當你可以融合這部分活靈活現的例子到程式碼實現中,往往會創造出更加優雅的實現方式。

八、推薦閱讀

相關文章