14、談談你知道的設計模式?

目錄

談談你知道的設計模式?請手動實現單例模式,Spring 等框架中使用了哪些模式?

典型回答

考點分析

知識擴展

Spring 等如何在 API 設計中使用設計模式


設計模式是人們爲軟件開發中相同表徵的問題,抽象出的可重複利用的解決方案。在某種程度上,設計模式已經代表了一些特定情況的最佳實踐,同時也起到了軟件工程師之間溝通的“行話”的作用。理解和掌握典型的設計模式,有利於我們提高溝通、設計的效率和質量。

 

談談你知道的設計模式?請手動實現單例模式,Spring 等框架中使用了哪些模式?

典型回答

大致按照模式的應用目標分類,設計模式可以分爲創建型模式、結構型模式和行爲型模式。

  •   創建型模式,是對對象創建過程的各種問題和解決方案的總結,包括各種工廠模式(Factory、Abstract Factory)、單例模式(Singleton)、構建器模式(Builder)、原型模式(ProtoType)。
  •   結構型模式,是針對軟件設計結構的總結,關注於類、對象繼承、組合方式的實踐經驗。常見的結構型模式,包括橋接模式(Bridge)、適配器模式(Adapter)、裝飾者模式(Decorator)、代理模式(Proxy)、組合模式(Composite)、外觀模式(Facade)、享元模式(Flyweight)等。
  •   行爲型模式,是從類或對象之間交互、職責劃分等角度總結的模式。比較常見的行爲型模式有策略模式(Strategy)、解釋器模式(Interpreter)、命令模式(Command)、觀察者模式(Observer)、迭代器模式(Iterator)、模板方法模式(Templatey Method)、訪問者模式(Visitor)。


考點分析

這個問題主要是考察你對設計模式的瞭解和掌握程度,更多相關內容你可以參考:

https://en.wikipedia.org/wiki/Design_Patterns。

我建議可以在回答時適當地舉些例子,更加清晰地說明典型模式到底是什麼樣子,典型使用場景是怎樣的。這裏舉個 Java 基礎類庫中的例子供你參考。
 
 IO 框架中,知道 InputStream 是一個抽象類,標準類庫中提供了 FileInputStream、ByteArrayInputStream 等各種不同的子類,分別從不同角度對 InputStream 進行了功能擴展,這是典型的裝飾器模式應用案例。

識別裝飾器模式,可以通過識別類設計特徵來進行判斷,也就是其類構造函數以相同的抽象類或者接口爲輸入參數。

因爲裝飾器模式本質上是包裝同類型實例,我們對目標對象的調用,往往會通過包裝類覆蓋過的方法,迂迴調用被包裝的實例,這就可以很自然地實現增加額外邏輯的目的,也就是所謂的“裝飾”。

例如,BufferedInputStream 經過包裝,爲輸入流過程增加緩存,類似這種裝飾器還可以多次嵌套,不斷地增加不同層次的功能。

public Buffered InputStream(InputStream in)

我在下面的類圖裏,簡單總結了 InputStream 的裝飾模式實踐。

接下來再看第二個例子。創建型模式尤其是工廠模式,在我們的代碼中隨處可見,我舉個相對不同的 API 設計實踐。比如,JDK 最新版本中 HTTP/2 Client API,下面這個創建 HttpRequest 的過程,就是典型的構建器模式(Builder),通常會被實現成fluent 風格的 API,也有人叫它方法鏈。

HttpRequest request = HttpRequest.newBuilder(new URI(uri))
                     .header(headerAlice, valueAlice)
                     .headers(headerBob, value1Bob,
                      headerCarl, valueCarl,
                      headerBob, value2Bob)
                     .GET()
                     .build();

使用構建器模式,可以比較優雅地解決構建複雜對象的麻煩,這裏的“複雜”是指類似需要輸入的參數組合較多,如果用構造函數,我們往往需要爲每一種可能的輸入參數組合實現相應的構造函數,一系列複雜的構造函數會讓代碼閱讀性和可維護性變得很差。

上面的分析也進一步反映了創建型模式的初衷,即,將對象創建過程單獨抽象出來,從結構上把對象使用邏輯和創建邏輯相互獨立,隱藏對象實例的細節,進而爲使用者實現了更加規範、統一的邏輯。

更進一步進行設計模式考察,面試官可能會:

  •   希望你寫一個典型的設計模式實現。這雖然看似簡單,但即使是最簡單的單例,也能夠綜合考察代碼基本功。
  •   考察典型的設計模式使用,尤其是結合標準庫或者主流開源框架,考察你對業界良好實踐的掌握程度。


在面試時如果恰好問到你不熟悉的模式,你可以稍微引導一下,比如介紹你在產品中使用了什麼自己相對熟悉的模式,試圖解決什麼問題,它們的優點和缺點等。

下面,我會針對前面兩點,結合代碼實例進行分析。

 

知識擴展

我們來實現一個日常非常熟悉的單例設計模式。看起來似乎很簡單,那麼下面這個樣例符合基本需求嗎?

 public class Singleton {
       private static Singleton instance = new Singleton();
       public static Singleton getInstance() {
          return instance;
       }
    }

是不是總感覺缺了點什麼?原來,Java 會自動爲沒有明確聲明構造函數的類,定義一個 public 的無參數的構造函數,所以上面的例子並不能保證額外的對象不被創建出來,別人完全可以直接“new Singleton()”,那我們應該怎麼處理呢?

不錯,可以爲單例定義一個 private 的構造函數(也有建議聲明爲枚舉,這是有爭議的,我個人不建議選擇相對複雜的枚舉,畢竟日常開發不是學術研究)。這樣還有什麼改進的餘地嗎?

介紹 ConcurrentHashMap 時,提到過標準類庫中很多地方使用懶加載(lazy-load),改善初始內存開銷,單例同樣適用,下面是修正後的改進版本。

public class Singleton {
        private static Singleton instance;

        private Singleton() {
        }

        public static Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            }
        return instance;
        }
    }

這個實現在單線程環境不存在問題,但是如果處於併發場景,就需要考慮線程安全,最熟悉的就莫過於“雙檢鎖”,其要點在於:

  •   這裏的 volatile 能夠提供可見性,以及保證 getInstance 返回的是初始化完全的對象。
  •   在同步之前進行 null 檢查,以儘量避免進入相對昂貴的同步塊。
  •   直接在 class 級別進行同步,保證線程安全的類方法調用。
public class Singleton {
    private static volatile Singleton singleton = null;

    private Singleton() {
    }

    public static Singleton getSingleton() {
        if (singleton == null) { // 儘量避免重複進入同步塊
            synchronized (Singleton.class) { // 同步.class,意味着對同步類方法調用
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

在這段代碼中,爭論較多的是 volatile 修飾靜態變量,當 Singleton 類本身有多個成員變量時,需要保證初始化過程完成後,才能被 get 到。

在現代 Java 中,內存排序模型(JMM)已經非常完善,通過 volatile 的 write 或者 read,能保證所謂的 happen-before,也就是避免常被提到的指令重排。換句話說,構造對象的 store 指令能夠被保證一定在 volatile read 之前。

當然,也有一些人推薦利用內部類持有靜態對象的方式實現,其理論依據是對象初始化過程中隱含的初始化鎖(有興趣的話你可以參考jls-12.4.2 中對 LC 的說明),這種和前面的雙檢鎖實現都能保證線程安全,不過語法稍顯晦澀,未必有特別的優勢。

public class Singleton {
    private Singleton(){}

    public static Singleton getSingleton(){
        return Holder.singleton;
    }

    private static class Holder {
        private static Singleton singleton = new Singleton();
    }
}

所以,可以看出,即使是看似最簡單的單例模式,在增加各種高標準需求之後,同樣需要非常多的實現考量。

上面是比較學究的考察,其實實踐中未必需要如此複雜,如果我們看 Java 核心類庫自己的單例實現,比如java.lang.Runtime,你會發現:

  •   它並沒使用複雜的雙檢鎖之類。
  •   靜態實例被聲明爲 final,這是被通常實踐忽略的,一定程度保證了實例不被篡改(專欄第 6 講介紹過,反射之類可以繞過私有訪問限制),也有有限的保證執行順序的語義。
private static final Runtime currentRuntime = new Runtime();

private static Version version;
// …
public static Runtime getRuntime() {
    return currentRuntime;
}

/** Don't let anyone else instantiate this class */
private Runtime() {

}

 

Spring 等如何在 API 設計中使用設計模式

前面說了不少代碼實踐,下面一起來簡要看看主流開源框架,如 Spring 等如何在 API 設計中使用設計模式。你至少要有個大體的印象,如:

  •   BeanFactory和ApplicationContext應用了工廠模式。
  •   在 Bean 的創建中,Spring 也爲不同 scope 定義的對象,提供了單例和原型等模式實現。
  •   我在專欄第 6 講介紹的 AOP 領域則是使用了代理模式、裝飾器模式、適配器模式等。
  •   各種事件監聽器,是觀察者模式的典型應用。
  •   類似 JdbcTemplate 等則是應用了模板模式。


今天,我與你回顧了設計模式的分類和主要類型,並從 Java 核心類庫、開源框架等不同角度分析了其採用的模式,並結合單例的不同實現,分析瞭如何實現符合線程安全等需求的單例,希望可以對你的工程實踐有所幫助。另外,我想最後補充的是,設計模式也不是銀彈,要避免濫用或者過度設計。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章