閱讀《重學(xué)設(shè)計(jì)模式》筆記2 - 抽象工廠模式

一丛版、抽象工廠模式介紹

抽象工廠

抽象工廠模式與工廠方法模式雖然主要意圖都是為了解決乞旦,接口選擇問題。但在實(shí)現(xiàn)上捅位,抽象工廠是一個(gè)中心工廠轧葛,創(chuàng)建其他工廠的模式搂抒。

可能在平常的業(yè)務(wù)開發(fā)中很少關(guān)注這樣的設(shè)計(jì)模式或者類似的代碼結(jié)構(gòu),但是這種場(chǎng)景確一直在我們身邊尿扯,例如求晶;

  1. 不同系統(tǒng)內(nèi)的回車換行

    1. Unix系統(tǒng)里,每行結(jié)尾只有 <換行>衷笋,即 \n芳杏;
    2. Windows系統(tǒng)里面,每行結(jié)尾是 <換行><回車>辟宗,即 \n\r爵赵;
    3. Mac系統(tǒng)里,每行結(jié)尾是 <回車>
  2. 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ì)有;

  1. 很多服務(wù)用到了Redis需要一起升級(jí)到集群阳掐。
  2. 需要兼容集群A和集群B始衅,便于后續(xù)的災(zāi)備冷蚂。
  3. 兩套集群提供的接口和方法各有差異,需要做適配汛闸。
  4. 不能影響到目前正常運(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
    • JDKProxyJDKInvocationHandler柴钻,是代理類的定義和實(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)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末某宪,一起剝皮案震驚了整個(gè)濱河市仿村,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌兴喂,老刑警劉巖蔼囊,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異衣迷,居然都是意外死亡畏鼓,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門壶谒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來云矫,“玉大人,你說我怎么就攤上這事汗菜∪觅鳎” “怎么了?”我有些...
    開封第一講書人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵陨界,是天一觀的道長(zhǎng)巡揍。 經(jīng)常有香客問我,道長(zhǎng)菌瘪,這世上最難降的妖魔是什么腮敌? 我笑而不...
    開封第一講書人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上缀皱,老公的妹妹穿的比我還像新娘斗这。我一直安慰自己,他們只是感情好啤斗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著赁咙,像睡著了一般钮莲。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上彼水,一...
    開封第一講書人閱讀 51,708評(píng)論 1 305
  • 那天崔拥,我揣著相機(jī)與錄音,去河邊找鬼凤覆。 笑死链瓦,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的盯桦。 我是一名探鬼主播慈俯,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼拥峦!你這毒婦竟也來了贴膘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤略号,失蹤者是張志新(化名)和其女友劉穎刑峡,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體玄柠,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡突梦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了羽利。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宫患。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖铐伴,靈堂內(nèi)的尸體忽然破棺而出撮奏,到底是詐尸還是另有隱情,我是刑警寧澤当宴,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布畜吊,位于F島的核電站,受9級(jí)特大地震影響户矢,放射性物質(zhì)發(fā)生泄漏玲献。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望捌年。 院中可真熱鬧瓢娜,春花似錦、人聲如沸礼预。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽托酸。三九已至褒颈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間励堡,已是汗流浹背谷丸。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留应结,地道東北人刨疼。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像鹅龄,于是被迫代替她去往敵國(guó)和親揩慕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容