詳解Java實踐之抽象工廠模式
一、前言
代碼一把梭,兄弟來背鍋。
大部分做開發的小夥伴初心都希望把代碼寫好,除瞭把編程當作工作以外他們還是具備工匠精神的從業者。但很多時候又很難讓你把初心堅持下去,就像;接瞭個爛手的項目、產品功能要的急、個人能力不足,等等原因導致工程代碼臃腫不堪,線上頻出事故,最終離職走人。
看瞭很多書、學瞭很多知識,多線程能玩出花,可最後我還是寫不好代碼!
這就有點像傢裡裝修完瞭買物件,我幾十萬的實木沙發,怎麼放這裡就不好看。同樣代碼寫的不好並不一定是基礎技術不足,也不一定是產品要得急 怎麼實現我不管明天上線
。而很多時候是我們對編碼的經驗的不足和對架構的把控能力不到位,我相信產品的第一個需求往往都不復雜,甚至所見所得。但如果你不考慮後續的是否會拓展,將來會在哪些模塊繼續添加功能,那麼後續的代碼就會隨著你種下的第一顆惡性的種子開始蔓延。
學習設計模式的心得有哪些,怎麼學才會用!
設計模式書籍,有點像考駕駛證的科一、傢裡裝修時的手冊、或者單身狗的戀愛寶典。但!你隻要不實操,一定能搞的亂碼
七糟。因為這些指導思想都是從實際經驗中提煉的,沒有經過提煉的小白,很難駕馭這樣的知識。所以在學習的過程中首先要有案例,之後再結合案例與自己實際的業務,嘗試重構改造,慢慢體會其中的感受,從而也就學會瞭如果搭建出優秀的代碼。
二、開發環境
JDK 1.8
Idea + Maven
工程 | 描述 |
---|---|
itstack-demo-design-2-00 | 場景模擬工程,模擬出使用Redis升級為集群時類改造 |
itstack-demo-design-2-01 | 使用一坨代碼實現業務需求,也是對ifelse的使用 |
itstack-demo-design-2-02 | 通過設計模式優化改造代碼,產生對比性從而學習 |
三、抽象工廠模式介紹
抽象工廠模式與工廠方法模式雖然主要意圖都是為瞭解決,接口選擇問題。但在實現上,抽象工廠是一個中心工廠,創建其他工廠的模式。
可能在平常的業務開發中很少關註這樣的設計模式或者類似的代碼結構,但是這種場景確一直在我們身邊,例如;
1.不同系統內的回車換行
- Unix系統裡,每行結尾隻有 <換行>,即
\n
; - Windows系統裡面,每行結尾是 <換行><回車>,即
\n\r
; - Mac系統裡,每行結尾是 <回車>
2.IDEA 開發工具的差異展示(Win\Mac)
除瞭這樣顯而易見的例子外,我們的業務開發中時常也會遇到類似的問題,需要兼容做處理但大部分經驗不足的開發人員,常常直接通過添加ifelse
方式進行處理瞭。
四、案例場景模擬
很多時候初期業務的蠻荒發展,也會牽動著研發對系統的建設。
預估QPS較低
、系統壓力較小
、並發訪問不大
、近一年沒有大動作
等等,在考慮時間投入成本的前提前,並不會投入特別多的人力去構建非常完善的系統。就像對 Redis
的使用,往往可能隻要是單機的就可以滿足現狀。
不吹牛的講百度首頁我上學時候一天就能寫完,等畢業工作瞭就算給我一年都完成不瞭!
但隨著業務超過預期的快速發展,系統的負載能力也要隨著跟上。原有的單機 Redis
已經滿足不瞭系統需求。這時候就需要更換為更為健壯的Redis集群服務,雖然需要修改但是不能影響目前系統的運行,還要平滑過渡過去。
隨著這次的升級,可以預見的問題會有;
- 很多服務用到瞭Redis需要一起升級到集群。
- 需要兼容集群A和集群B,便於後續的災備。
- 兩套集群提供的接口和方法各有差異,需要做適配。
- 不能影響到目前正常運行的系統。
4.1、場景模擬工程
itstack-demo-design-2-00
└── src
└── main
└── java
└── org.itstack.demo.design
├── matter
│ ├── EGM.java
│ └── IIR.java
└── RedisUtils.java
4.2、場景簡述
4.2.1、模擬單機服務 RedisUtils
- 模擬Redis功能,也就是假定目前所有的系統都在使用的服務
- 類和方法名次都固定寫死到各個業務系統中,改動略微麻煩
4.2.2、模擬集群 EGM
模擬一個集群服務,但是方法名與各業務系統中使用的方法名不同。有點像你mac,我用win。做一樣的事,但有不同的操作。
4.2.3、模擬集群 IIR
這是另外一套集群服務,有時候在企業開發中就很有可能出現兩套服務,這裡我們也是為瞭做模擬案例,所以添加兩套實現同樣功能的不同服務,來學習抽象工廠模式。
綜上可以看到,我們目前的系統中已經在大量的使用redis服務,但是因為系統不能滿足業務的快速發展,因此需要遷移到集群服務中。而這時有兩套集群服務需要兼容使用,又要滿足所有的業務系統改造的同時不影響線上使用。
4.3、單集群代碼使用
以下是案例模擬中原有的單集群Redis使用方式,後續會通過對這裡的代碼進行改造。
4.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); }
4.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解決不瞭的邏輯,不行就在加一行!
此時的實現方式並不會修改類結構圖,也就是與上面給出的類層級關系一致。通過在接口中添加類型字段區分當前使用的是哪個集群,來作為使用的判斷。可以說目前的方式非常難用,其他使用方改動頗多,這裡隻是做為例子。
5.1、工程結構
itstack-demo-design-2-01
└── src
└── main
└── java
└── org.itstack.demo.design
├── impl
│ └── CacheServiceImpl.java
└── CacheService.java
此時的隻有兩個類,類結構非常簡單。而我們需要的補充擴展功能也隻是在 CacheServiceImpl
中實現。
5.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集群。
- 雖然實現是簡單瞭,但是對使用者來說就麻煩瞭,並且也很難應對後期的拓展和不停的維護。
5.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在部分方法提供上是不同的,因此需要做一個接口適配,而這個適配類就相當於工廠中的工廠,用於創建把不同的服務抽象為統一的接口做相同的業務。這一塊與我們上一章節中的工廠方法模型
類型,可以翻閱參考。
6.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
,定義瞭適配接口,分別包裝兩個集群中差異化的接口名稱。EGMCacheAdapter
、IIRCacheAdapter
JDKProxy
、JDKInvocationHandler
,是代理類的定義和實現,這部分也就是抽象工廠的另外一種實現方式。通過這樣的方式可以很好的把原有操作Redis的方法進行代理操作,通過控制不同的入參對象,控制緩存的使用。
好,那麼接下來會分別講解幾個類的具體實現。
6.2、代碼實現
6.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); }
這個類的主要作用是讓所有集群的提供方,能在統一的方法名稱下進行操作。也方面後續的拓展。
6.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); } }
以上兩個實現都非常容易,在統一方法名下進行包裝。
6.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
中通過使用獲取方法名稱反射方式,調用對應的方法功能,也就簡化瞭整體的使用。 - 到這我們就已經將整體的功能實現完成瞭,關於抽象工廠這部分也可以使用非代理的方式進行實現。
6.3、測試驗證
編寫測試類:
@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埋上瞭!
當你知道什麼場景下何時可以被抽象工程優化代碼,那麼你的代碼層級結構以及滿足業務需求上,都可以得到很好的完成功能實現並提升擴展性和優雅度。
那麼這個設計模式滿足瞭;單一職責、開閉原則、解耦等優點,但如果說隨著業務的不斷拓展,可能會造成類實現上的復雜度。但也可以說算不上缺點,因為可以隨著其他設計方式的引入和代理類以及自動生成加載的方式降低此項缺點。
以上就是詳解Java實踐之抽象工廠模式的詳細內容,更多關於Java抽象工廠模式的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- 詳解Java實踐之建造者模式
- SpringBoot整合Redis將對象寫入redis的實現
- Java經典面試題最全匯總208道(六)
- 詳解Java實踐之適配器模式
- Springboot整合Redis與數據持久化