重學 Java 設計模式:實戰抽象工廠模式

小傅哥發表於2020-05-25

作者:小傅哥
部落格:https://bugstack.cn - 本文章已收錄到系列原創專題

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

一、前言

程式碼一把梭,兄弟來背鍋。

大部分做開發的小夥伴初心都希望把程式碼寫好,除了把程式設計當作工作以外他們還是具備工匠精神的從業者。但很多時候又很難讓你把初心堅持下去,就像;接了個爛手的專案、產品功能要的急、個人能力不足,等等原因導致工程程式碼臃腫不堪,線上頻出事故,最終離職走人。

看了很多書、學了很多知識,多執行緒能玩出花,可最後我還是寫不好程式碼!

這就有點像家裡裝修完了買物件,我幾十萬的實木沙發,怎麼放這裡就不好看。同樣程式碼寫的不好並不一定是基礎技術不足,也不一定是產品要得急 怎麼實現我不管明天上線。而很多時候是我們對編碼的經驗的不足和對架構的把控能力不到位,我相信產品的第一個需求往往都不復雜,甚至所見所得。但如果你不考慮後續的是否會擴充,將來會在哪些模組繼續新增功能,那麼後續的程式碼就會隨著你種下的第一顆惡性的種子開始蔓延。

學習設計模式的心得有哪些,怎麼學才會用!

設計模式書籍,有點像考駕駛證的科一、家裡裝修時的手冊、或者單身狗的戀愛寶典。但!你只要不實操,一定能搞的七糟。因為這些指導思想都是從實際經驗中提煉的,沒有經過提煉的小白,很難駕馭這樣的知識。所以在學習的過程中首先要有案例,之後再結合案例與自己實際的業務,嘗試重構改造,慢慢體會其中的感受,從而也就學會了如果搭建出優秀的程式碼。

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程三個,可以通過關注公眾號bugstack蟲洞棧,回覆原始碼下載獲取
工程 描述
itstack-demo-design-2-00 場景模擬工程,模擬出使用Redis升級為叢集時類改造
itstack-demo-design-2-01 使用一坨程式碼實現業務需求,也是對ifelse的使用
itstack-demo-design-2-02 通過設計模式優化改造程式碼,產生對比性從而學習

三、抽象工廠模式介紹

抽象工廠模式,圖片來自 refactoringguru.cn

抽象工廠模式與工廠方法模式雖然主要意圖都是為了解決,介面選擇問題。但在實現上,抽象工廠是一箇中心工廠,建立其他工廠的模式。

可能在平常的業務開發中很少關注這樣的設計模式或者類似的程式碼結構,但是這種場景確一直在我們身邊,例如;

  1. 不同系統內的回車換行

    1. Unix系統裡,每行結尾只有 <換行>,即 \n
    2. Windows系統裡面,每行結尾是 <換行><回車>,即 \n\r
    3. Mac系統裡,每行結尾是 <回車>
  2. IDEA 開發工具的差異展示(WinMac)

    不同系統下,IDEA 開發工具的展示差異點

除了這樣顯而易見的例子外,我們的業務開發中時常也會遇到類似的問題,需要相容做處理但大部分經驗不足的開發人員,常常直接通過新增ifelse方式進行處理了。

四、案例場景模擬

模擬企業級雙套Redis叢集升級

很多時候初期業務的蠻荒發展,也會牽動著研發對系統的建設。

預估QPS較低系統壓力較小併發訪問不大近一年沒有大動作等等,在考慮時間投入成本的前提前,並不會投入特別多的人力去構建非常完善的系統。就像對 Redis 的使用,往往可能只要是單機的就可以滿足現狀。

不吹牛的講百度首頁我上學時候一天就能寫完,等畢業工作了就算給我一年都完成不了!

但隨著業務超過預期的快速發展,系統的負載能力也要隨著跟上。原有的單機 Redis 已經滿足不了系統需求。這時候就需要更換為更為健壯的Redis叢集服務,雖然需要修改但是不能影響目前系統的執行,還要平滑過渡過去。

隨著這次的升級,可以預見的問題會有;

  1. 很多服務用到了Redis需要一起升級到叢集。
  2. 需要相容叢集A和叢集B,便於後續的災備。
  3. 兩套叢集提供的介面和方法各有差異,需要做適配。
  4. 不能影響到目前正常執行的系統。

1. 場景模擬工程

itstack-demo-design-2-00
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── matter
                │   ├── EGM.java
                │   └── IIR.java
                └── RedisUtils.java

工程中的所有程式碼可以通過關注公眾號:bugstack蟲洞棧,回覆原始碼下載進行獲取。

2. 場景簡述

2.1 模擬單機服務 RedisUtils

Redis單機服務

  • 模擬Redis功能,也就是假定目前所有的系統都在使用的服務
  • 類和方法名次都固定寫死到各個業務系統中,改動略微麻煩

2.2 模擬叢集 EGM

模擬叢集 EGM

  • 模擬一個叢集服務,但是方法名與各業務系統中使用的方法名不同。有點像你mac,我用win。做一樣的事,但有不同的操作。

2.3 模擬叢集 IIR

模擬叢集 IIR

  • 這是另外一套叢集服務,有時候在企業開發中就很有可能出現兩套服務,這裡我們也是為了做模擬案例,所以新增兩套實現同樣功能的不同服務,來學習抽象工廠模式。

綜上可以看到,我們目前的系統中已經在大量的使用redis服務,但是因為系統不能滿足業務的快速發展,因此需要遷移到叢集服務中。而這時有兩套叢集服務需要相容使用,又要滿足所有的業務系統改造的同時不影響線上使用。

3. 單叢集程式碼使用

以下是案例模擬中原有的單叢集Redis使用方式,後續會通過對這裡的程式碼進行改造。

當前功能的類圖結構

3.1 定義使用介面

public interface CacheService {

    String get(final String key);

    void set(String key, String value);

    void set(String key, String value, long timeout, TimeUnit timeUnit);

    void del(String key);

}

3.2 實現呼叫程式碼

public class CacheServiceImpl implements CacheService {

    private RedisUtils redisUtils = new RedisUtils();

    public String get(String key) {
        return redisUtils.get(key);
    }

    public void set(String key, String value) {
        redisUtils.set(key, value);
    }

    public void set(String key, String value, long timeout, TimeUnit timeUnit) {
        redisUtils.set(key, value, timeout, timeUnit);
    }

    public void del(String key) {
        redisUtils.del(key);
    }

}
  • 目前的程式碼對於當前場景下的使用沒有什麼問題,也比較簡單。但是所有的業務系統都在使用同時,需要改造就不那麼容易了。這裡可以思考下,看如何改造才是合理的。

五、用一坨坨程式碼實現

講道理沒有ifelse解決不了的邏輯,不行就在加一行!

此時的實現方式並不會修改類結構圖,也就是與上面給出的類層級關係一致。通過在介面中新增型別欄位區分當前使用的是哪個叢集,來作為使用的判斷。可以說目前的方式非常難用,其他使用方改動頗多,這裡只是做為例子。

1. 工程結構

itstack-demo-design-2-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                ├── impl
                │   └── CacheServiceImpl.java
                └── CacheService.java
  • 此時的只有兩個類,類結構非常簡單。而我們需要的補充擴充套件功能也只是在 CacheServiceImpl 中實現。

2. ifelse實現需求

public class CacheServiceImpl implements CacheService {

    private RedisUtils redisUtils = new RedisUtils();

    private EGM egm = new EGM();

    private IIR iir = new IIR();

    public String get(String key, int redisType) {

        if (1 == redisType) {
            return egm.gain(key);
        }

        if (2 == redisType) {
            return iir.get(key);
        }

        return redisUtils.get(key);
    }

    public void set(String key, String value, int redisType) {

        if (1 == redisType) {
            egm.set(key, value);
            return;
        }

        if (2 == redisType) {
            iir.set(key, value);
            return;
        }

        redisUtils.set(key, value);
    }

    //... 同類不做太多展示,可以下載原始碼進行參考

}
  • 這裡的實現過程非常簡單,主要根據型別判斷是哪個Redis叢集。
  • 雖然實現是簡單了,但是對使用者來說就麻煩了,並且也很難應對後期的擴充和不停的維護。

3. 測試驗證

接下來我們通過junit單元測試的方式驗證介面服務,強調日常編寫好單測可以更好的提高系統的健壯度。

編寫測試類:

@Test
public void test_CacheService() {
    CacheService cacheService = new CacheServiceImpl();
    cacheService.set("user_name_01", "小傅哥", 1);
    String val01 = cacheService.get("user_name_01",1);
    System.out.println(val01);
}

結果:

22:26:24.591 [main] INFO  org.itstack.demo.design.matter.EGM - EGM寫入資料 key:user_name_01 val:小傅哥
22:26:24.593 [main] INFO  org.itstack.demo.design.matter.EGM - EGM獲取資料 key:user_name_01
測試結果:小傅哥

Process finished with exit code 0
  • 從結果上看執行正常,並沒有什麼問題。但這樣的程式碼只要到生成執行起來以後,想再改就真的難了!

六、抽象工廠模式重構程式碼

接下來使用抽象工廠模式來進行程式碼優化,也算是一次很小的重構。

這裡的抽象工廠的建立和獲取方式,會採用代理類的方式進行實現。所被代理的類就是目前的Redis操作方法類,讓這個類在不需要任何修改下,就可以實現呼叫叢集A和叢集B的資料服務。

並且這裡還有一點非常重要,由於叢集A和叢集B在部分方法提供上是不同的,因此需要做一個介面適配,而這個適配類就相當於工廠中的工廠,用於建立把不同的服務抽象為統一的介面做相同的業務。這一塊與我們上一章節中的工廠方法模型型別,可以翻閱參考。

1. 工程結構

itstack-demo-design-2-02
└── src
    ├── main
    │   └── java
    │       └── org.itstack.demo.design
    │           ├── factory    
    │           │   ├── impl
    │           │   │   ├── EGMCacheAdapter.java 
    │           │   │   └── IIRCacheAdapter.java
    │           │   ├── ICacheAdapter.java
    │           │   ├── JDKInvocationHandler.java
    │           │   └── JDKProxy.java
    │           ├── impl
    │           │   └── CacheServiceImpl.java    
    │           └── CacheService.java 
    └── test
         └── java
             └── org.itstack.demo.design.test
                 └── ApiTest.java

抽象工廠模型結構

抽象工廠模型結構

  • 工程中涉及的部分核心功能程式碼,如下;

    • ICacheAdapter,定義了適配介面,分別包裝兩個叢集中差異化的介面名稱。EGMCacheAdapterIIRCacheAdapter
    • JDKProxyJDKInvocationHandler,是代理類的定義和實現,這部分也就是抽象工廠的另外一種實現方式。通過這樣的方式可以很好的把原有操作Redis的方法進行代理操作,通過控制不同的入參物件,控制快取的使用。

,那麼接下來會分別講解幾個類的具體實現。

2. 程式碼實現

2.1 定義適配介面

public interface ICacheAdapter {

    String get(String key);

    void set(String key, String value);

    void set(String key, String value, long timeout, TimeUnit timeUnit);

    void del(String key);

}
  • 這個類的主要作用是讓所有叢集的提供方,能在統一的方法名稱下進行操作。也方面後續的擴充。

2.2 實現叢集使用服務

EGMCacheAdapter

public class EGMCacheAdapter implements ICacheAdapter {

    private EGM egm = new EGM();

    public String get(String key) {
        return egm.gain(key);
    }

    public void set(String key, String value) {
        egm.set(key, value);
    }

    public void set(String key, String value, long timeout, TimeUnit timeUnit) {
        egm.setEx(key, value, timeout, timeUnit);
    }

    public void del(String key) {
        egm.delete(key);
    }
}

IIRCacheAdapter

public class IIRCacheAdapter implements ICacheAdapter {

    private IIR iir = new IIR();

    public String get(String key) {
        return iir.get(key);
    }

    public void set(String key, String value) {
        iir.set(key, value);
    }

    public void set(String key, String value, long timeout, TimeUnit timeUnit) {
        iir.setExpire(key, value, timeout, timeUnit);
    }

    public void del(String key) {
        iir.del(key);
    }

}
  • 以上兩個實現都非常容易,在統一方法名下進行包裝。

2.3 定義抽象工程代理類和實現

JDKProxy

public static <T> T getProxy(Class<T> interfaceClass, ICacheAdapter cacheAdapter) throws Exception {
    InvocationHandler handler = new JDKInvocationHandler(cacheAdapter);
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    Class<?>[] classes = interfaceClass.getInterfaces();
    return (T) Proxy.newProxyInstance(classLoader, new Class[]{classes[0]}, handler);
}
  • 這裡主要的作用就是完成代理類,同時對於使用哪個叢集有外部通過入參進行傳遞。

JDKInvocationHandler

public class JDKInvocationHandler implements InvocationHandler {

    private ICacheAdapter cacheAdapter;

    public JDKInvocationHandler(ICacheAdapter cacheAdapter) {
        this.cacheAdapter = cacheAdapter;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return ICacheAdapter.class.getMethod(method.getName(), ClassLoaderUtils.getClazzByArgs(args)).invoke(cacheAdapter, args);
    }

}
  • 在代理類的實現中其實也非常簡單,通過穿透進來的叢集服務進行方法操作。
  • 另外在invoke中通過使用獲取方法名稱反射方式,呼叫對應的方法功能,也就簡化了整體的使用。
  • 到這我們就已經將整體的功能實現完成了,關於抽象工廠這部分也可以使用非代理的方式進行實現。

2. 測試驗證

編寫測試類:

@Test
public void test_CacheService() throws Exception {
    CacheService proxy_EGM = JDKProxy.getProxy(CacheServiceImpl.class, new EGMCacheAdapter());
    proxy_EGM.set("user_name_01","小傅哥");
    String val01 = proxy_EGM.get("user_name_01");
    System.out.println(val01);
    
    CacheService proxy_IIR = JDKProxy.getProxy(CacheServiceImpl.class, new IIRCacheAdapter());
    proxy_IIR.set("user_name_01","小傅哥");
    String val02 = proxy_IIR.get("user_name_01");
    System.out.println(val02);
}
  • 在測試的程式碼中通過傳入不同的叢集型別,就可以呼叫不同的叢集下的方法。JDKProxy.getProxy(CacheServiceImpl.class, new EGMCacheAdapter());
  • 如果後續有擴充套件的需求,也可以按照這樣的型別方式進行補充,同時對於改造上來說並沒有改動原來的方法,降低了修改成本。

結果:

23:07:06.953 [main] INFO  org.itstack.demo.design.matter.EGM - EGM寫入資料 key:user_name_01 val:小傅哥
23:07:06.956 [main] INFO  org.itstack.demo.design.matter.EGM - EGM獲取資料 key:user_name_01
測試結果:小傅哥
23:07:06.957 [main] INFO  org.itstack.demo.design.matter.IIR - IIR寫入資料 key:user_name_01 val:小傅哥
23:07:06.957 [main] INFO  org.itstack.demo.design.matter.IIR - IIR獲取資料 key:user_name_01
測試結果:小傅哥

Process finished with exit code 0
  • 執行結果正常,這樣的程式碼滿足了這次擴充的需求,同時你的技術能力也給老闆留下了深刻的印象。
  • 研發自我能力的提升遠不是外接的壓力就是編寫一坨坨程式碼的介面,如果你已經熟練了很多技能,那麼可以在即使緊急的情況下,也能做出完善的方案。

七、總結

  • 抽象工廠模式,所要解決的問題就是在一個產品族,存在多個不同型別的產品(Redis叢集、作業系統)情況下,介面選擇的問題。而這種場景在業務開發中也是非常多見的,只不過可能有時候沒有將它們抽象化出來。
  • 你的程式碼只是被ifelse埋上了!當你知道什麼場景下何時可以被抽象工程優化程式碼,那麼你的程式碼層級結構以及滿足業務需求上,都可以得到很好的完成功能實現並提升擴充套件性和優雅度。
  • 那麼這個設計模式滿足了;單一職責、開閉原則、解耦等優點,但如果說隨著業務的不斷擴充,可能會造成類實現上的複雜度。但也可以說算不上缺點,因為可以隨著其他設計方式的引入和代理類以及自動生成載入的方式降低此項缺點。

八、推薦閱讀

九、彩蛋

CodeGuide | 程式設計師編碼指南 Go!
本程式碼庫是作者小傅哥多年從事一線網際網路 Java 開發的學習歷程技術彙總,旨在為大家提供一個清晰詳細的學習教程,側重點更傾向編寫Java核心內容。如果本倉庫能為您提供幫助,請給予支援(關注、點贊、分享)!

CodeGuide | 程式設計師編碼指南

相關文章