兩萬字盤點那些被玩爛了的設計模式

大家好,我是三友~~

之前有小夥伴私信我說看源碼的時候感覺源碼很難,不知道該怎麼看,其實這有部分原因是因爲沒有弄懂一些源碼實現的套路,也就是設計模式,所以本文我就總結了 9 種在源碼中非常常見的設計模式,並列舉了很多源碼的實現例子,希望對你看源碼和日常工作中有所幫助。

單例模式

單例模式是指一個類在一個進程中只有一個實例對象(但也不一定,比如 Spring 中的 Bean 的單例是指在一個容器中是單例的)

單例模式創建分爲餓漢式和懶漢式,總共大概有 8 種寫法。但是在開源項目中使用最多的主要有兩種寫法:

1、靜態常量

靜態常量方式屬於餓漢式,以靜態變量的方式聲明對象。這種單例模式在 Spring 中使用的比較多,舉個例子,在 Spring 中對於 Bean 的名稱生成有個類 AnnotationBeanNameGenerator 就是單例的。

2、雙重檢查機制

除了上面一種,還有一種雙重檢查機制在開源項目中也使用的比較多,而且在面試中也比較喜歡問。雙重檢查機制方式屬於懶漢式,代碼如下:

public class Singleton {

    private volatile static Singleton INSTANCE;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }

}

之所以這種方式叫雙重檢查機制,主要是在創建對象的時候進行了兩次 INSTANCE == null 的判斷。

疑問講解

這裏解釋一下雙重檢查機制的三個疑問:

外層判斷 null 的作用:其實就是爲了減少進入同步代碼塊的次數,提高效率。你想一下,其實去了外層的判斷其實是可以的,但是每次獲取對象都需要進入同步代碼塊,實在是沒有必要。

內層判斷 null 的作用:防止多次創建對象。假設 AB 同時走到同步代碼塊,A 先搶到鎖,進入代碼,創建了對象,釋放鎖,此時 B 進入代碼塊,如果沒有判斷 null,那麼就會直接再次創建對象,那麼就不是單例的了,所以需要進行判斷 null,防止重複創建單例對象。

volatile 關鍵字的作用:防止重排序。因爲創建對象的過程不是原子,大概會分爲三個步驟

假設沒有使用 volatile 關鍵字發生了重排序,第二步和第三步執行過程被調換了,也就是先將 INSTANCE 變量指向 Singleton 這個對象內存地址,再初始化對象。這樣在發生併發的情況下,另一個線程經過第一個 if 非空判斷時,發現已經爲不爲空,就直接返回了這個對象,但是此時這個對象還未初始化,內部的屬性可能都是空值,一旦被使用的話,就很有可能出現空指針這些問題。

雙重檢查機制在 dubbo 中的應用

在 dubbo 的 spi 機制中獲取對象的時候有這樣一段代碼:

雖然這段代碼跟上面的單例的寫法有點不同,但是不難看出其實是使用了雙重檢查機制來創建對象,保證對象單例。

建造者模式

將一個複雜對象的構造與它的表示分離,使同樣的構建過程可以創建不同的表示,這樣的設計模式被稱爲建造者模式。它是將一個複雜的對象分解爲多個簡單的對象,然後一步一步構建而成。

上面的意思看起來很繞,其實在實際開發中,其實建造者模式使用的還是比較多的,比如有時在創建一個 pojo 對象時,就可以使用建造者模式來創建:

PersonDTO personDTO = PersonDTO.builder()
        .name("三友的java日記")
        .age(18)
        .sex(1)
        .phone("188****9527")
        .build();

上面這段代碼就是通過建造者模式構建了一個 PersonDTO 對象,所以建造者模式又被稱爲 Budiler 模式。

這種模式在創建對象的時候看起來比較優雅,當構造參數比較多的時候,適合使用建造者模式。

接下來就來看看建造者模式在開源項目中是如何運用的

1、在 Spring 中的運用

我們都知道,Spring 在創建 Bean 之前,會將每個 Bean 的聲明封裝成對應的一個 BeanDefinition,而 BeanDefinition 會封裝很多屬性,所以 Spring 爲了更加優雅地創建 BeanDefinition,就提供了 BeanDefinitionBuilder 這個建造者類。

2、在 Guava 中的運用

在項目中,如果我們需要使用本地緩存,會使用本地緩存的實現的框架來創建一個,比如在使用 Guava 來創建本地緩存時,就會這麼寫

Cache<String, String> cache = CacheBuilder.newBuilder()
         .expireAfterAccess(1, TimeUnit.MINUTES)
         .maximumSize(200)
         .build();

這其實也就是建造者模式。

建造者模式不僅在開源項目中有所使用,在 JDK 源碼中也有使用到,比如 StringBuilder 類。

最後上面說的建造者模式其實算是在 Java 中一種簡化的方式,如果想了解一下傳統的建造者模式,可以看一下這篇文章

https://m.runoob.com/design-pattern/builder-pattern.html?ivk_sa=1024320u

工廠模式

工廠模式在開源項目中也使用的非常多,具體的實現大概可以細分爲三種:

簡單工廠模式

簡單工廠模式,就跟名字一樣,的確很簡單。比如說,現在有個動物接口 Animal,具體的實現有貓 Cat、狗 Dog 等等,而每個具體的動物對象創建過程很複雜,有各種各樣地步驟,此時就可以使用簡單工廠來封裝對象的創建過程,調用者不需要關心對象是如何具體創建的。

public class SimpleAnimalFactory {

    public Animal createAnimal(String animalType) {
        if ("cat".equals(animalType)) {
            Cat cat = new Cat();
            //一系列複雜操作
            return cat;
        } else if ("dog".equals(animalType)) {
            Dog dog = new Dog();
            //一系列複雜操作
            return dog;
        } else {
            throw new RuntimeException("animalType=" + animalType + "無法創建對應對象");
        }
    }

}

當需要使用這些對象,調用者就可以直接通過簡單工廠創建就行。

SimpleAnimalFactory animalFactory = new SimpleAnimalFactory();
Animal cat = animalFactory.createAnimal("cat");

需要注意的是,一般來說如果每個動物對象的創建只需要簡單地 new 一下就行了,那麼其實就無需使用工廠模式,工廠模式適合對象創建過程複雜的場景。

工廠方法模式

上面說的簡單工廠模式看起來沒啥問題,但是還是違反了七大設計原則的 OCP 原則,也就是開閉原則。所謂的開閉原則就是對修改關閉,對擴展開放。

什麼叫對修改關閉?就是儘可能不修改的意思。就拿上面的例子來說,如果現在新增了一種動物兔子,那麼 createAnimal 方法就得修改,增加一種類型的判斷,那麼就此時就出現了修改代碼的行爲,也就違反了對修改關閉的原則。

所以解決簡單工廠模式違反開閉原則的問題,就可以使用工廠方法模式來解決。

/**
 * 工廠接口
 */
public interface AnimalFactory {
    Animal createAnimal();
}

/**
 * 小貓實現
 */
public class CatFactory implements AnimalFactory {
    @Override
    public Animal createAnimal() {
        Cat cat = new Cat();
        //一系列複雜操作
        return cat;
    }
}

/**
 * 小狗實現
 */
public class DogFactory implements AnimalFactory {
    @Override
    public Animal createAnimal() {
        Dog dog = new Dog();
        //一系列複雜操作
        return dog;
    }
}

這種方式就是工廠方法模式。他將動物工廠提取成一個接口 AnimalFactory,具體每個動物都各自實現這個接口,每種動物都有各自的創建工廠,如果調用者需要創建動物,就可以通過各自的工廠來實現。

AnimalFactory animalFactory = new CatFactory();
Animal cat = animalFactory.createAnimal();

此時假設需要新增一個動物兔子,那麼只需要實現 AnimalFactory 接口就行,對於原來的貓和狗的實現,其實代碼是不需要修改的,遵守了對修改關閉的原則,同時由於是對擴展開放,實現接口就是擴展的意思,那麼也就符合擴展開放的原則。

抽象工廠模式

工廠方法模式其實是創建一個產品的工廠,比如上面的例子中,AnimalFactory 其實只創建動物這一個產品。而抽象工廠模式特點就是創建一系列產品,比如說,不同的動物喫的東西是不一樣的,那麼就可以加入食物這個產品,通過抽象工廠模式來實現。

public interface AnimalFactory {

    Animal createAnimal();

    Food createFood();
        
}

在動物工廠中,新增了創建食物的接口,小狗小貓的工廠去實現這個接口,創建狗糧和貓糧,這裏就不去寫了。

1、工廠模式在 Mybatis 的運用

在 Mybatis 中,當需要調用 Mapper 接口執行 sql 的時候,需要先獲取到 SqlSession,通過 SqlSession 再獲取到 Mapper 接口的動態代理對象,而 SqlSession 的構造過程比較複雜,所以就提供了 SqlSessionFactory 工廠類來封裝 SqlSession 的創建過程。

SqlSessionFactory 及默認實現 DefaultSqlSessionFactory

對於使用者來說,只需要通過 SqlSessionFactory 來獲取到 SqlSession,而無需關心 SqlSession 是如何創建的。

2、工廠模式在 Spring 中的運用

我們知道 Spring 中的 Bean 是通過 BeanFactory 創建的。

BeanFactory 就是 Bean 生成的工廠。一個 Spring Bean 在生成過程中會經歷複雜的一個生命週期,而這些生命週期對於使用者來說是無需關心的,所以就可以將 Bean 創建過程的邏輯給封裝起來,提取出一個 Bean 的工廠。

策略模式

策略模式也比較常見,就比如說在 Spring 源碼中就有很多地方都使用到了策略模式。

在講策略模式是什麼之前先來舉個例子,這個例子我在之前的《寫出漂亮代碼的 45 個小技巧》文章提到過。

假設現在有一個需求,需要將消息推送到不同的平臺。

最簡單的做法其實就是使用 if else 來做判斷就行了。

public void notifyMessage(User user, String content, int notifyType) {
    if (notifyType == 0) {
        //調用短信通知的api發送短信
    } else if (notifyType == 1) {
        //調用app通知的api發送消息
    }
}

根據不同的平臺類型進行判斷,調用對應的 api 發送消息。

雖然這樣能實現功能,但是跟上面的提到的簡單工廠的問題是一樣的,同樣違反了開閉原則。當需要增加一種平臺類型,比如郵件通知,那麼就得修改 notifyMessage 的方法,再次進行 else if 的判斷,然後調用發送郵件的郵件發送消息。

此時就可以使用策略模式來優化了。

首先設計一個策略接口:

public interface MessageNotifier {

    /**
     * 是否支持改類型的通知的方式
     *
     * @param notifyType 0:短信 1:app
     * @return
     */
    boolean support(int notifyType);

    /**
     * 通知
     *
     * @param user
     * @param content
     */
    void notify(User user, String content);

}

短信通知實現:

@Component
public class SMSMessageNotifier implements MessageNotifier {
    @Override
    public boolean support(int notifyType) {
        return notifyType == 0;
    }

    @Override
    public void notify(User user, String content) {
        //調用短信通知的api發送短信
    }
}

app 通知實現:

public class AppMessageNotifier implements MessageNotifier {
    @Override
    public boolean support(int notifyType) {
        return notifyType == 1;
    }

    @Override
    public void notify(User user, String content) {
       //調用通知app通知的api
    }
}

最後 notifyMessage 的實現只需要要循環調用所有的 MessageNotifier 的 support 方法,一旦 support 方法返回 true,說明當前 MessageNotifier 支持該類的消息發送,最後再調用 notify 發送消息就可以了。

@Resource
private List<MessageNotifier> messageNotifiers;

public void notifyMessage(User user, String content, int notifyType) {
    for (MessageNotifier messageNotifier : messageNotifiers) {
        if (messageNotifier.support(notifyType)) {
            messageNotifier.notify(user, content);
        }
    }
}

那麼如果現在需要支持通過郵件通知,只需要實現 MessageNotifier 接口,注入到 Spring 容器就行,其餘的代碼根本不需要有任何變動。

到這其實可以更好的理解策略模式了。就拿上面舉的例子來說,短信通知,app 通知等其實都是發送消息一種策略,而策略模式就是需要將這些策略進行封裝,抽取共性,使這些策略之間相互替換。

策略模式在 SpringMVC 中的運用

1、對接口方法參數的處理

比如說,我們經常在寫接口的時候,會使用到了 @PathVariable、@RequestParam、@RequestBody 等註解,一旦我們使用了註解,SpringMVC 會處理註解,從請求中獲取到參數,然後再調用接口傳遞過來,而這個過程,就使用到了策略模式。

對於這類參數的解析,SpringMVC 提供了一個策略接口 HandlerMethodArgumentResolver

這個接口的定義就跟我們上面定義的差不多,不同的參數處理只需要實現這個解決就行,比如上面提到的幾個註解,都有對應的實現。

比如處理 @RequestParam 註解的 RequestParamMethodArgumentResolver 的實現。

當然還有其它很多的實現,如果想知道各種註解處理的過程,只需要找到對應的實現類就行了。

2、對接口返回值的處理

同樣,SpringMVC 對於返回值的處理也是基於策略模式來實現的。

HandlerMethodReturnValueHandler 接口定義跟上面都是同一種套路。

比如說,常見的對於 @ResponseBody 註解處理的實現 RequestResponseBodyMethodProcessor。

ResponseBody 註解處理的實現 RequestResponseBodyMethodProcessor

同樣,HandlerMethodReturnValueHandler 的實現也有很多,這裏就不再舉例了。

策略模式在 Spring 的運用遠不止這兩處,就比如我在《三萬字盤點 Spring/Boot 的那些常用擴展點》文章提到過對於配置文件的加載 PropertySourceLoader 也是策略模式的運用。

模板方法模式

模板方法模式是指,在父類中定義一個操作中的框架,而操作步驟的具體實現交由子類做。其核心思想就是,對於功能實現的順序步驟是一定的,但是具體每一步如何實現交由子類決定。

比如說,對於旅遊來說,一般有以下幾個步驟:

但是對於去哪,收拾什麼東西都,乘坐什麼交通工具,都是由具體某個旅行來決定。

那麼對於旅遊這個過程使用模板方法模式翻譯成代碼如下:

public abstract class Travel {

    public void travel() {
        //做攻略
        makePlan();

        //收拾行李
        packUp();

        //去目的地
        toDestination();

        //玩耍、拍照
        play();

        //乘坐交通工具去返回
        backHome();
    }

    protected abstract void makePlan();

    protected abstract void packUp();

    protected abstract void toDestination();

    protected abstract void play();

    protected abstract void backHome();

}

對於某次旅行來說,只需要重寫每個步驟該做的事就行,比如說這次可以選擇去杭州西湖,下次可以去長城,但是對於旅行過程來說是不變了,對於調用者來說,只需要調用暴露的 travel 方法就行。

可能這說的還是比較抽象,我再舉兩個模板方法模式在源碼中實現的例子。

模板方法模式在源碼中的使用

1、模板方法模式在 HashMap 中的使用

HashMap 我們都很熟悉,可以通過 put 方法存元素,並且在元素添加成功之後,會調用一下 afterNodeInsertion 方法。

而 afterNodeInsertion 其實是在 HashMap 中是空實現,什麼事都沒幹。

這其實就是模板方法模式。HashMap 定義了一個流程,那就是當元素成功添加之後會調用 afterNodeInsertion,子類如果需要在元素添加之後做什麼事,那麼重寫 afterNodeInsertion 就行。

正巧,JDK 中的 LinkedHashMap 重寫了這個方法。

而這段代碼主要乾的一件事就是可能會移除最老的元素,至於到底會不會移除,得看 if 是否成立。

添加元素移除最老的元素,基於這種特性其實可以實現 LRU 算法,比如 Mybatis 的 LruCache 就是基於 LinkedHashMap 實現的,有興趣的可以扒扒源碼,這裏就不再展開講了。

2、模板方法模式在 Spring 中的運用

我們都知道,在 Spring 中,ApplicationContext 在使用之前需要調用一下 refresh 方法,而 refresh 方法就定義了整個容器刷新的執行流程代碼。

refresh 方法部分截圖

在整個刷新過程有一個 onRefresh 方法

onRefresh 方法

而 onRefresh 方法默認是沒有做任何事,並且在註釋上有清楚兩個單詞 Template method,翻譯過來就是模板方法的意思,所以 onRefresh 就是一個模板方法,並且方法內部的註釋也表明了,這個方法是爲了子類提供的。

在 Web 環境下,子類會重寫這個方法,然後創建一個 Web 服務器。

3、模板方法模式在 Mybatis 中的使用

在 Mybatis 中,是使用 Executor 執行 Sql 的。

Executor

而 Mybatis 一級緩存就在 Executor 的抽象實現中 BaseExecutor 實現的。如圖所示,紅圈就是一級緩存

BaseExecutor

比如在查詢的時候,如果一級緩存有,那麼就處理緩存的數據,沒有的話就調用 queryFromDatabase 從數據庫查

queryFromDatabase 會調用 doQuery 方法從數據庫查數據,然後放入一級緩存中。

而 doQuery 是個抽象方法

所以 doQuery 其實就是一個模板方法,需要子類真正實現從數據庫中查詢數據,所以這裏就使用了模板方法模式。

責任鏈模式

在責任鏈模式裏,很多對象由每一個對象對其下家的引用而連接起來形成一條鏈。請求在這個鏈上傳遞,由該鏈上的某一個對象或者某幾個對象決定處理此請求,每個對象在整個處理過程中值扮演一個小小的角色。

舉個例子,現在有個請假的審批流程,根據請假的人的級別審批到的領導不同,比如有有組長、主管、HR、分管經理等等。

先需要定義一個處理抽象類,抽象類有個下一個處理對象的引用,提供了抽象處理方法,還有一個對下一個處理對象的調用方法。

public abstract class ApprovalHandler {

    /**
     * 責任鏈中的下一個處理對象
     */
    protected ApprovalHandler next;

    /**
     * 設置下一個處理對象
     *
     * @param approvalHandler
     */
    public void nextHandler(ApprovalHandler approvalHandler) {
        this.next = approvalHandler;
    }

    /**
     * 處理
     *
     * @param approvalContext
     */
    public abstract void approval(ApprovalContext approvalContext);

    /**
     * 調用下一個處理對象
     *
     * @param approvalContext
     */
    protected void invokeNext(ApprovalContext approvalContext) {
        if (next != null) {
            next.approval(approvalContext);
        }
    }

}

幾種審批人的實現

//組長審批實現
public class GroupLeaderApprovalHandler extends ApprovalHandler {
    @Override
    public void approval(ApprovalContext approvalContext) {
        System.out.println("組長審批");
        //調用下一個處理對象進行處理
        invokeNext(approvalContext);
    }
}

//主管審批實現
public class DirectorApprovalHandler extends ApprovalHandler {
    @Override
    public void approval(ApprovalContext approvalContext) {
        System.out.println("主管審批");
        //調用下一個處理對象進行處理
        invokeNext(approvalContext);
    }
}

//hr審批實現
public class HrApprovalHandler extends ApprovalHandler {
    @Override
    public void approval(ApprovalContext approvalContext) {
        System.out.println("hr審批");
        //調用下一個處理對象進行處理
        invokeNext(approvalContext);
    }
}

有了這幾個實現之後,接下來就需要對對象進行組裝,組成一個鏈條,比如在 Spring 中就可以這麼玩。

@Component
public class ApprovalHandlerChain {

    @Autowired
    private GroupLeaderApprovalHandler groupLeaderApprovalHandler;
    @Autowired
    private DirectorApprovalHandler directorApprovalHandler;
    @Autowired
    private HrApprovalHandler hrApprovalHandler;

    public ApprovalHandler getChain() {
        //組長處理完下一個處理對象是主管
        groupLeaderApprovalHandler.nextHandler(directorApprovalHandler);
        //主管處理完下一個處理對象是hr
        directorApprovalHandler.nextHandler(hrApprovalHandler);
        
        //返回組長,這樣就從組長開始審批,一條鏈就完成了
        return groupLeaderApprovalHandler;
    }

}

之後對於調用方而言,只需要獲取到鏈條,開始處理就行。

一旦後面出現需要增加或者減少審批人,只需要調整鏈條中的節點就行,對於調用者來說是無感知的。

責任鏈模式在開源項目中的使用

1、在 SpringMVC 中的使用

在 SpringMVC 中,可以通過使用 HandlerInterceptor 對每個請求進行攔截。

HandlerInterceptor

而 HandlerInterceptor 其實就使用到了責任鏈模式,但是這種責任鏈模式的寫法跟上面舉的例子寫法不太一樣。

對於 HandlerInterceptor 的調用是在 HandlerExecutionChain 中完成的。

HandlerExecutionChain

比如說,對於請求處理前的攔截,就在是這樣調用的。

其實就是循環遍歷每個 HandlerInterceptor,調用 preHandle 方法。

2、在 Sentinel 中的使用

Sentinel 是阿里開源的一個流量治理組件,而 Sentinel 核心邏輯的執行其實就是一條責任鏈。

在 Sentinel 中,有個核心抽象類 AbstractLinkedProcessorSlot

AbstractLinkedProcessorSlot

這個組件內部也維護了下一個節點對象,這個類扮演的角色跟例子中的 ApprovalHandler 類是一樣的,寫法也比較相似。這個組件有很多實現

比如有比較核心的幾個實現

整個鏈條的組裝的實現是由 DefaultSlotChainBuilder 實現的

DefaultSlotChainBuilder

並且內部是使用了 SPI 機制來加載每個處理節點

所以,如果你想自定一些處理邏輯,就可以基於 SPI 機制來擴展。

除了上面的例子,比如 Gateway 網關、Dubbo、MyBatis 等等框架中都有責任鏈模式的身影,所以責任鏈模式使用的還是比較多的。

代理模式

代理模式也是開源項目中很常見的使用的一種設計模式,這種模式可以在不改變原有代碼的情況下增加功能。

舉個例子,比如現在有個 PersonService 接口和它的實現類 PersonServiceImpl

//接口
public interface PersonService {

    void savePerson(PersonDTO person);
    
}

//實現
public class PersonServiceImpl implements PersonService{
    @Override
    public void savePerson(PersonDTO person) {
        //保存人員信息
    }
}

這個類剛開始運行的好好的,但是突然之前不知道咋回事了,有報錯,需要追尋入參,所以此時就可以這麼寫。

public class PersonServiceImpl implements PersonService {
    @Override
    public void savePerson(PersonDTO person) {
        log.info("savePerson接口入參:{}", JSON.toJSONString(person));
        //保存人員信息
    }
}

這麼寫,就修改了代碼,萬一以後不需要打印日誌了呢,豈不是又要修改代碼,不符和之前說的開閉原則,那麼怎麼寫呢?可以這麼玩。

public class PersonServiceProxy implements PersonService {

    private final PersonService personService = new PersonServiceImpl();

    @Override
    public void savePerson(PersonDTO person) {
        log.info("savePerson接口入參:{}", JSON.toJSONString(person));
        personService.savePerson(person);
    }
}

可以實現一個代理類 PersonServiceProxy,對 PersonServiceImpl 進行代理,這個代理類乾的事就是打印日誌,最後調用 PersonServiceImpl 進行人員信息的保存,這就是代理模式。

當需要打印日誌就使用 PersonServiceProxy,不需要打印日誌就使用 PersonServiceImpl,這樣就行了,不需要改原有代碼的實現。

講到了代理模式,就不得不提一下 Spring AOP,Spring AOP 其實跟靜態代理很像,最終其實也是調用目標對象的方法,只不過是動態生成的,這裏就不展開講解了。

代理模式在 Mybtais 中的使用

前面在說模板方法模式的時候,舉了一個 BaseExecutor 使用到了模板方法模式的例子,並且在 BaseExecutor 這裏面還完成了一級緩存的操作。

其實不光是一級緩存是通過 Executor 實現的,二級緩存其實也是,只不過不在 BaseExecutor 裏面實現,而是在 CachingExecutor 中實現的。

CachingExecutor

CachingExecutor 中內部有一個 Executor 類型的屬性 delegate,delegate 單詞的意思就是代理的意思,所以 CachingExecutor 顯然就是一個代理類,這裏就使用到了代理模式。

CachingExecutor 的實現原理其實很簡單,先從二級緩存查,查不到就通過被代理的對象查找數據,而被代理的 Executor 在 Mybatis 中默認使用的是 SimpleExecutor 實現,SimpleExecutor 繼承自 BaseExecutor。

這裏思考一下二級緩存爲什麼不像一級緩存一樣直接寫到 BaseExecutor 中?

這裏我猜測一下是爲了減少耦合。

我們知道 Mybatis 的一級緩存默認是開啓的,一級緩存寫在 BaseExecutor 中的話,那麼只要是繼承了 BaseExecutor,就擁有了一級緩存的能力。

但二級緩存默認是不開啓的,如果寫在 BaseExecutor 中,講道理也是可以的,但不符和單一職責的原則,類的功能過多,同時會耦合很多判斷代碼,比如開啓二級緩存走什麼邏輯,不開啓二級緩存走什麼邏輯。而使用代理模式很好的解決了這一問題,只需要在創建的 Executor 的時候判斷是否開啓二級緩存,開啓的話就用 CachingExecutor 代理一下,不開啓的話老老實實返回未被代理的對象就行,默認是 SimpleExecutor。

如圖所示,是構建 Executor 對象的源碼,一旦開啓了二級緩存,就會將前面創建的 Executor 進行代理,構建一個 CachingExecutor 返回。

適配器模式

適配器模式使得原本由於接口不兼容而不能一起工作的哪些類可以一起工作,將一個類的接口轉換成客戶希望的另一個接口。

舉個生活中的例子,比如手機充電器接口類型有 USB TypeC 接口和 Micro USB 接口等。現在需要給一個 Micro USB 接口的手機充電,但是現在只有 USB TypeC 接口的充電器,這怎麼辦呢?

其實一般可以弄個一個 USB TypeC 轉 Micro USB 接口的轉接頭,這樣就可以給 Micro USB 接口手機充電了,代碼如下

USBTypeC 接口充電

public class USBTypeC {

    public void chargeTypeC() {
        System.out.println("開啓充電了");
    }

}

MicroUSB 接口

public interface MicroUSB {

    void charge();

}

適配實現,最後是調用 USBTypeC 接口來充電

public class MicroUSBAdapter implements MicroUSB {

    private final USBTypeC usbTypeC = new USBTypeC();

    @Override
    public void charge() {
        //使用usb來充電
        usbTypeC.chargeTypeC();
    }

}

方然除了上面這種寫法,還有一種繼承的寫法。

public class MicroUSBAdapter extends USBTypeC implements MicroUSB {

    @Override
    public void charge() {
        //使用usb來充電
        this.chargeTypeC();
    }

}

這兩種寫法主要是繼承和組合(聚合)的區別。

這樣就可以通過適配器(轉接頭)就可以實現 USBTypeC 給 MicroUSB 接口充電。

適配器模式在日誌中的使用

在日常開發中,日誌是必不可少的,可以幫助我們快速快速定位問題,但是日誌框架比較多,比如 Slf4j、Log4j 等等,一般同一系統都使用一種日誌框架。

但是像 Mybatis 這種框架來說,它本身在運行的過程中也需要產生日誌,但是 Mybatis 框架在設計的時候,無法知道項目中具體使用的是什麼日誌框架,所以只能適配各種日誌框架,項目中使用什麼框架,Mybatis 就使用什麼框架。

爲此 Mybatis 提供一個 Log 接口

而不同的日誌框架,只需要適配這個接口就可以了

Slf4jLoggerImpl

就拿 Slf4j 的實現來看,內部依賴了一個 Slf4j 框架中的 Logger 對象,最後所有日誌的打印都是通過 Slf4j 框架中的 Logger 對象來實現的。

此外,Mybatis 還提供瞭如下的一些實現

這樣,Mybatis 在需要打印日誌的時候,只需要從 Mybatis 自己的 LogFactory 中獲取到 Log 對象就行,至於最終獲取到的是什麼 Log 實現,由最終項目中使用日誌框架來決定。

觀察者模式

當對象間存在一對多關係時,則使用觀察者模式(Observer Pattern)。比如,當一個對象被修改時,則會自動通知依賴它的對象。

這是什麼意思呢,舉個例子來說,假設發生了火災,可能需要打 119、救人,那麼就可以基於觀察者模式來實現,打 119、救人的操作只需要觀察火災的發生,一旦發生,就觸發相應的邏輯。

觀察者的核心優點就是觀察者和被觀察者是解耦合的。就拿上面的例子來說,火災事件(被觀察者)根本不關係有幾個監聽器(觀察者),當以後需要有變動,只需要擴展監聽器就行,對於事件的發佈者和其它監聽器是無需做任何改變的。

觀察者模式實現起來比較複雜,這裏我舉一下 Spring 事件的例子來說明一下。

觀察者模式在 Spring 事件中的運用

Spring 事件,就是 Spring 基於觀察者模式實現的一套 API,如果有不知道不知道 Spring 事件的小夥伴,可以看看《三萬字盤點 Spring/Boot 的那些常用擴展點》這篇文章,裏面有對 Spring 事件的詳細介紹,這裏就不對使用進行介紹了。

Spring 事件的實現比較簡單,其實就是當 Bean 在生成完成之後,會將所有的 ApplicationListener 接口實現(監聽器)添加到 ApplicationEventMulticaster 中。

ApplicationEventMulticaster 可以理解爲一個調度中心的作用,可以將事件通知給監聽器,觸發監聽器的執行。

ApplicationEventMulticaster 可以理解爲一個總線

retrieverCache 中存儲了事件類型和對應監聽器的緩存。當發佈事件的時候,會通過事件的類型找到對應的監聽器,然後循環調用監聽器。

所以,Spring 的觀察者模式實現的其實也不復雜。

總結

本文通過對設計模式的講解加源碼舉例的方式介紹了 9 種在代碼設計中常用的設計模式:

其實這些設計模式不僅在源碼中常見在平時工作中也是可以經常使用到的。

設計模式其實還是一種思想,或者是套路性的東西,至於設計模式具體怎麼用、如何用、代碼如何寫還得依靠具體的場景來進行靈活的判斷。


本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/vWkdyKw3QgE2ABfJDqS7pA