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

来源:https://www.cnblogs.com/zzyang/archive/2022/11/16/16895538.html
-Advertisement-
Play Games

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


大家好,我是三友~~

之前有小伙伴私信我說看源碼的時候感覺源碼很難,不知道該怎麼看,其實這有部分原因是因為沒有弄懂一些源碼實現的套路,也就是設計模式,所以本文我就總結了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

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

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1.HTTP是什麼? http是超文本傳輸協議用來在web瀏覽器和網站伺服器之間傳遞數據信息,http以明文的方式發送內容,不提供任何方式的數據加密,如果攻擊者截獲了Web瀏覽器和網站伺服器之間的傳輸報文,就可以直接讀懂其中的信息,因此,HTTP協議不適合傳輸一些敏感信息,比如:信用卡號、密碼等支付 ...
  • 1、使用註解需要導入的依賴 1、1在application.xml文件中加入該約束 xmlns:context=http://www.springframework.org/schema/context http://www.springframework.org/schema/context ht ...
  • 我國目前並未出台專門針對網路爬蟲技術的法律規範,但在司法實踐中,相關判決已屢見不鮮,K 哥特設了“K哥爬蟲普法”專欄,本欄目通過對真實案例的分析,旨在提高廣大爬蟲工程師的法律意識,知曉如何合法合規利用爬蟲技術,警鐘長鳴,做一個守法、護法、有原則的技術人員。 案情介紹 2018年1月至7月期間,咼某興 ...
  • 大家好,我是棧長。 今天給大家通報一則框架更新消息,時隔兩個月,Spring Cloud 2021.0.5 最新版發佈了,來看下最新的 Spring Cloud 版本情況: Spring Cloud 無疑是現在 Java 微服務事實上的標準,完全基於 Spring Boot 構建,依賴 Spring ...
  • 目錄 一.freeglut 簡介 二.freeglut 下載 五.猜你喜歡 零基礎 OpenGL ES 學習路線推薦 : OpenGL ES 學習目錄 >> OpenGL ES 基礎 零基礎 OpenGL ES 學習路線推薦 : OpenGL ES 學習目錄 >> OpenGL ES 特效 零基礎 ...
  • 類模板=>實力化=>模板類 通過類模板實現棧,點擊查看代碼 #include <iostream> #include <cstring> using namespace std; template<typename T> //template<typename T=int> 也可以這樣寫,寫個預設類 ...
  • ##SpringBoot集成JWT(極簡版) ###在WebConfig配置類中設置介面統一首碼 import org.springframework.context.annotation.Configuration; import org.springframework.web.bind.anno ...
  • 這一篇問文章主要介紹元組的相關知識。 元組:不可修改的序列 與列表一樣,元組也是序列,唯一的差別在於元組是不能修改的(同樣的,字元串也不能修改)。 元組的語法很簡單。 >>> >>> 1, 2, 3 (1, 2, 3) >>> (1, 2, 3) (1, 2, 3) >>> >>> () () >> ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...