兩萬字盤點被玩爛了的9種設計模式

2022-11-16 15:03:59

大家好,我是三友~~

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

單例模式

單例模式是指一個類在一個程序中只有一個範例物件(但也不一定,比如Spring中的Bean的單例是指在一個容器中是單例的)

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

1、靜態常數

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

AnnotationBeanNameGenerator
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的作用
  • 變數使用volatile關鍵字修飾的作用

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

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

volatile關鍵字的作用:防止重排序。因為建立物件的過程不是原子,大概會分為三個步驟

  • 第一步:分配記憶體空間給Singleton這個物件
  • 第二步:初始化物件
  • 第三步:將INSTANCE變數指向Singleton這個物件記憶體地址

假設沒有使用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這個建造者類。

BeanDefinitionBuilder
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及預設實現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

HandlerMethodArgumentResolver
HandlerMethodArgumentResolver

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

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

RequestParamMethodArgumentResolver
RequestParamMethodArgumentResolver

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

2、對介面返回值的處理

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

HandlerMethodReturnValueHandler
HandlerMethodReturnValueHandler

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

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

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中是空實現,什麼事都沒幹。

afterNodeInsertion
afterNodeInsertion

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

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

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

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

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

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

refresh方法部分截圖
refresh方法部分截圖

在整個重新整理過程有一個onRefresh方法

onRefresh方法
onRefresh方法

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

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

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

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

Executor
Executor

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

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其實就使用到了責任鏈模式,但是這種責任鏈模式的寫法跟上面舉的例子寫法不太一樣。

對於HandlerInterceptor的呼叫是在HandlerExecutionChain中完成的。

HandlerExecutionChain
HandlerExecutionChain

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

其實就是迴圈遍歷每個HandlerInterceptor,呼叫preHandle方法。

2、在Sentinel中的使用

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

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

AbstractLinkedProcessorSlot
AbstractLinkedProcessorSlot

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

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

  • DegradeSlot:熔斷降級的實現
  • FlowSlot:流量控制的實現
  • StatisticSlot:統計的實現,比如統計請求成功的次數、異常次數,為限流提供資料來源
  • SystemSlot:根據系統規則來進行流量控制

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

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

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
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可以理解為一個匯流排
ApplicationEventMulticaster可以理解為一個匯流排

retrieverCache中儲存了事件型別和對應監聽器的快取。當釋出事件的時候,會通過事件的型別找到對應的監聽器,然後迴圈呼叫監聽器。

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

總結

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

  • 單例模式
  • 建造者模式
  • 工廠模式
  • 策略模式
  • 模板方法模式
  • 責任鏈模式
  • 代理模式
  • 介面卡模式
  • 觀察者模式

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

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

最後,本文又是前前後後花了一週多的時間完成,如果對你有點幫助,還請幫忙點贊、在看、轉發、非常感謝。

往期熱門文章推薦

寫出漂亮程式碼的45個小技巧

RocketMQ保姆級教學

擼了一個簡易的設定中心,順帶還給整合到了SpringCloud

三萬字盤點Spring/Boot的那些常用擴充套件點

RocketMQ的push消費方式實現的太聰明瞭

@Async註解的坑,小心

掃碼或者搜尋關注公眾號 三友的java日記 ,及時乾貨不錯過,公眾號致力於通過畫圖加上通俗易懂的語言講解技術,讓技術更加容易學習,回覆 面試 即可獲得一套面試真題。