一丛版、抽象工廠模式介紹
抽象工廠模式與工廠方法模式雖然主要意圖都是為了解決乞旦,接口選擇問題。但在實(shí)現(xiàn)上捅位,抽象工廠是一個(gè)中心工廠轧葛,創(chuàng)建其他工廠的模式搂抒。
可能在平常的業(yè)務(wù)開發(fā)中很少關(guān)注這樣的設(shè)計(jì)模式或者類似的代碼結(jié)構(gòu),但是這種場(chǎng)景確一直在我們身邊尿扯,例如求晶;
-
不同系統(tǒng)內(nèi)的回車換行
- Unix系統(tǒng)里,每行結(jié)尾只有 <換行>衷笋,即
\n
芳杏; - Windows系統(tǒng)里面,每行結(jié)尾是 <換行><回車>辟宗,即
\n\r
爵赵; - Mac系統(tǒng)里,每行結(jié)尾是 <回車>
- Unix系統(tǒng)里,每行結(jié)尾只有 <換行>衷笋,即
IDEA 開發(fā)工具的差異展示(Win\Mac)
除了這樣顯而易見的例子外泊脐,我們的業(yè)務(wù)開發(fā)中時(shí)常也會(huì)遇到類似的問題空幻,需要兼容做處理。但大部分經(jīng)驗(yàn)不足的開發(fā)人員容客,常常直接通過添加ifelse
方式進(jìn)行處理了秕铛。
二、案例場(chǎng)景模擬
很多時(shí)候初期業(yè)務(wù)的蠻荒發(fā)展缩挑,也會(huì)牽動(dòng)著研發(fā)對(duì)系統(tǒng)的建設(shè)但两。
預(yù)估QPS較低
、系統(tǒng)壓力較小
供置、并發(fā)訪問不大
谨湘、近一年沒有大動(dòng)作
等等,在考慮時(shí)間投入成本的前提前芥丧,并不會(huì)投入特別多的人力去構(gòu)建非常完善的系統(tǒng)紧阔。就像對(duì) Redis
的使用,往往可能只要是單機(jī)的就可以滿足現(xiàn)狀续担。
不吹牛的講百度首頁我上學(xué)時(shí)候一天就能寫完寓辱,等畢業(yè)工作了就算給我一年都完成不了!
但隨著業(yè)務(wù)超過預(yù)期的快速發(fā)展赤拒,系統(tǒng)的負(fù)載能力也要隨著跟上。原有的單機(jī) Redis
已經(jīng)滿足不了系統(tǒng)需求诱鞠。這時(shí)候就需要更換為更為健壯的Redis集群服務(wù)挎挖,雖然需要修改但是不能影響目前系統(tǒng)的運(yùn)行,還要平滑過渡過去航夺。
隨著這次的升級(jí)蕉朵,可以預(yù)見的問題會(huì)有;
- 很多服務(wù)用到了Redis需要一起升級(jí)到集群阳掐。
- 需要兼容集群A和集群B始衅,便于后續(xù)的災(zāi)備冷蚂。
- 兩套集群提供的接口和方法各有差異,需要做適配汛闸。
- 不能影響到目前正常運(yùn)行的系統(tǒng)蝙茶。
1. 場(chǎng)景模擬工程
└── src
└── main
└── java
└── org.itstack.demo.design
├── matter
│ ├── EGM.java
│ └── IIR.java
└── RedisUtils.java
工程中的所有代碼可以通過關(guān)注公眾號(hào):bugstack蟲洞棧
,回復(fù)源碼下載
進(jìn)行獲取诸老。
2. 場(chǎng)景簡(jiǎn)述
2.1 模擬單機(jī)服務(wù) RedisUtils
- 模擬Redis功能隆夯,也就是假定目前所有的系統(tǒng)都在使用的服務(wù)
- 類和方法名次都固定寫死到各個(gè)業(yè)務(wù)系統(tǒng)中,改動(dòng)略微麻煩
2.2 模擬集群 EGM
- 模擬一個(gè)集群服務(wù)别伏,但是方法名與各業(yè)務(wù)系統(tǒng)中使用的方法名不同蹄衷。有點(diǎn)像你mac,我用win厘肮。做一樣的事愧口,但有不同的操作。
2.3 模擬集群 IIR
- 這是另外一套集群服務(wù)类茂,有時(shí)候在企業(yè)開發(fā)中就很有可能出現(xiàn)兩套服務(wù)耍属,這里我們也是為了做模擬案例,所以添加兩套實(shí)現(xiàn)同樣功能的不同服務(wù)大咱,來學(xué)習(xí)抽象工廠模式恬涧。
綜上可以看到,我們目前的系統(tǒng)中已經(jīng)在大量的使用redis服務(wù)碴巾,但是因?yàn)橄到y(tǒng)不能滿足業(yè)務(wù)的快速發(fā)展溯捆,因此需要遷移到集群服務(wù)中。而這時(shí)有兩套集群服務(wù)需要兼容使用厦瓢,又要滿足所有的業(yè)務(wù)系統(tǒng)改造的同時(shí)不影響線上使用提揍。
3. 單集群代碼使用
以下是案例模擬中原有的單集群Redis使用方式,后續(xù)會(huì)通過對(duì)這里的代碼進(jìn)行改造煮仇。
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 實(shí)現(xiàn)調(diào)用代碼
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);
}
}
- 目前的代碼對(duì)于當(dāng)前場(chǎng)景下的使用沒有什么問題劳跃,也比較簡(jiǎn)單。但是所有的業(yè)務(wù)系統(tǒng)都在使用同時(shí)浙垫,需要改造就不那么容易了刨仑。這里可以思考下,看如何改造才是合理的夹姥。
三杉武、用一坨坨代碼實(shí)現(xiàn)
講道理沒有ifelse解決不了的邏輯,不行就在加一行辙售!
此時(shí)的實(shí)現(xiàn)方式并不會(huì)修改類結(jié)構(gòu)圖轻抱,也就是與上面給出的類層級(jí)關(guān)系一致。通過在接口中添加類型字段區(qū)分當(dāng)前使用的是哪個(gè)集群旦部,來作為使用的判斷祈搜〗系辏可以說目前的方式非常難用,其他使用方改動(dòng)頗多容燕,這里只是做為例子梁呈。
1. 工程結(jié)構(gòu)
└── src
└── main
└── java
└── org.itstack.demo.design
├── impl
│ └── CacheServiceImpl.java
└── CacheService.java
- 此時(shí)的只有兩個(gè)類,類結(jié)構(gòu)非常簡(jiǎn)單缰趋。而我們需要的補(bǔ)充擴(kuò)展功能也只是在
CacheServiceImpl
中實(shí)現(xiàn)捧杉。
2. ifelse實(shí)現(xiàn)需求
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);
}
//... 同類不做太多展示,可以下載源碼進(jìn)行參考
}
- 這里的實(shí)現(xiàn)過程非常簡(jiǎn)單秘血,主要根據(jù)類型判斷是哪個(gè)Redis集群味抖。
- 雖然實(shí)現(xiàn)是簡(jiǎn)單了,但是對(duì)使用者來說就麻煩了灰粮,并且也很難應(yīng)對(duì)后期的拓展和不停的維護(hù)仔涩。
3. 測(cè)試驗(yàn)證
接下來我們通過junit單元測(cè)試的方式驗(yàn)證接口服務(wù),強(qiáng)調(diào)日常編寫好單測(cè)可以更好的提高系統(tǒng)的健壯度粘舟。
編寫測(cè)試類:
@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);
}
結(jié)果:
22:26:24.591 [main] INFO org.itstack.demo.design.matter.EGM - EGM寫入數(shù)據(jù) key:user_name_01 val:小傅哥
22:26:24.593 [main] INFO org.itstack.demo.design.matter.EGM - EGM獲取數(shù)據(jù) key:user_name_01
測(cè)試結(jié)果:小傅哥
Process finished with exit code 0
- 從結(jié)果上看運(yùn)行正常熔脂,并沒有什么問題。但這樣的代碼只要到生成運(yùn)行起來以后柑肴,想再改就真的難了霞揉!
四、抽象工廠模式重構(gòu)代碼
接下來使用抽象工廠模式來進(jìn)行代碼優(yōu)化晰骑,也算是一次很小的重構(gòu)适秩。
這里的抽象工廠的創(chuàng)建和獲取方式,會(huì)采用代理類的方式進(jìn)行實(shí)現(xiàn)硕舆。所被代理的類就是目前的Redis操作方法類秽荞,讓這個(gè)類在不需要任何修改下,就可以實(shí)現(xiàn)調(diào)用集群A和集群B的數(shù)據(jù)服務(wù)抚官。
并且這里還有一點(diǎn)非常重要扬跋,由于集群A和集群B在部分方法提供上是不同的,因此需要做一個(gè)接口適配凌节,而這個(gè)適配類就相當(dāng)于工廠中的工廠钦听,用于創(chuàng)建把不同的服務(wù)抽象為統(tǒng)一的接口做相同的業(yè)務(wù)。這一塊與我們上一章節(jié)中的工廠方法模型
類型倍奢,可以翻閱參考彪见。
1. 工程結(jié)構(gòu)
└── 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
抽象工廠模型結(jié)構(gòu)
- 工程中涉及的部分核心功能代碼,如下娱挨;
-
ICacheAdapter
,定義了適配接口捕犬,分別包裝兩個(gè)集群中差異化的接口名稱跷坝。EGMCacheAdapter
酵镜、IIRCacheAdapter
-
JDKProxy
、JDKInvocationHandler
柴钻,是代理類的定義和實(shí)現(xiàn)淮韭,這部分也就是抽象工廠的另外一種實(shí)現(xiàn)方式。通過這樣的方式可以很好的把原有操作Redis的方法進(jìn)行代理操作贴届,通過控制不同的入?yún)?duì)象靠粪,控制緩存的使用。
-
好毫蚓,那么接下來會(huì)分別講解幾個(gè)類的具體實(shí)現(xiàn)占键。
2. 代碼實(shí)現(xiàn)
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);
}
- 這個(gè)類的主要作用是讓所有集群的提供方,能在統(tǒng)一的方法名稱下進(jìn)行操作元潘。也方面后續(xù)的拓展畔乙。
2.2 實(shí)現(xiàn)集群使用服務(wù)
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);
}
}
- 以上兩個(gè)實(shí)現(xiàn)都非常容易,在統(tǒng)一方法名下進(jìn)行包裝翩概。
2.3 定義抽象工程代理類和實(shí)現(xiàn)
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);
}
- 這里主要的作用就是完成代理類牲距,同時(shí)對(duì)于使用哪個(gè)集群有外部通過入?yún)⑦M(jìn)行傳遞。
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);
}
}
- 在代理類的實(shí)現(xiàn)中其實(shí)也非常簡(jiǎn)單钥庇,通過穿透進(jìn)來的集群服務(wù)進(jìn)行方法操作牍鞠。
- 另外在
invoke
中通過使用獲取方法名稱反射方式,調(diào)用對(duì)應(yīng)的方法功能评姨,也就簡(jiǎn)化了整體的使用难述。 - 到這我們就已經(jīng)將整體的功能實(shí)現(xiàn)完成了,關(guān)于抽象工廠這部分也可以使用非代理的方式進(jìn)行實(shí)現(xiàn)参咙。
3. 測(cè)試驗(yàn)證
編寫測(cè)試類:
@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);
}
- 在測(cè)試的代碼中通過傳入不同的集群類型龄广,就可以調(diào)用不同的集群下的方法。
JDKProxy.getProxy(CacheServiceImpl.class, new EGMCacheAdapter());
- 如果后續(xù)有擴(kuò)展的需求蕴侧,也可以按照這樣的類型方式進(jìn)行補(bǔ)充择同,同時(shí)對(duì)于改造上來說并沒有改動(dòng)原來的方法,降低了修改成本净宵。
結(jié)果:
23:07:06.953 [main] INFO org.itstack.demo.design.matter.EGM - EGM寫入數(shù)據(jù) key:user_name_01 val:小傅哥
23:07:06.956 [main] INFO org.itstack.demo.design.matter.EGM - EGM獲取數(shù)據(jù) key:user_name_01
測(cè)試結(jié)果:小傅哥
23:07:06.957 [main] INFO org.itstack.demo.design.matter.IIR - IIR寫入數(shù)據(jù) key:user_name_01 val:小傅哥
23:07:06.957 [main] INFO org.itstack.demo.design.matter.IIR - IIR獲取數(shù)據(jù) key:user_name_01
測(cè)試結(jié)果:小傅哥
Process finished with exit code 0
- 運(yùn)行結(jié)果正常敲才,這樣的代碼滿足了這次拓展的需求,同時(shí)你的技術(shù)能力也給老板留下了深刻的印象择葡。
- 研發(fā)自我能力的提升遠(yuǎn)不是外接的壓力就是編寫一坨坨代碼的接口紧武,如果你已經(jīng)熟練了很多技能,那么可以在即使緊急的情況下敏储,也能做出完善的方案阻星。
七、總結(jié)
- 抽象工廠模式,所要解決的問題就是在一個(gè)產(chǎn)品族妥箕,存在多個(gè)不同類型的產(chǎn)品(Redis集群滥酥、操作系統(tǒng))情況下,接口選擇的問題畦幢。而這種場(chǎng)景在業(yè)務(wù)開發(fā)中也是非常多見的坎吻,只不過可能有時(shí)候沒有將它們抽象化出來。
-
你的代碼只是被ifelse埋上了宇葱!
當(dāng)你知道什么場(chǎng)景下何時(shí)可以被抽象工程優(yōu)化代碼瘦真,那么你的代碼層級(jí)結(jié)構(gòu)以及滿足業(yè)務(wù)需求上,都可以得到很好的完成功能實(shí)現(xiàn)并提升擴(kuò)展性和優(yōu)雅度黍瞧。 - 那么這個(gè)設(shè)計(jì)模式滿足了诸尽;單一職責(zé)、開閉原則雷逆、解耦等優(yōu)點(diǎn)弦讽,但如果說隨著業(yè)務(wù)的不斷拓展,可能會(huì)造成類實(shí)現(xiàn)上的復(fù)雜度膀哲。但也可以說算不上缺點(diǎn)往产,因?yàn)榭梢噪S著其他設(shè)計(jì)方式的引入和代理類以及自動(dòng)生成加載的方式降低此項(xiàng)缺點(diǎn)。