設計模式是人們為軟件開發中相同表征的問題,抽象出的可重復利用的解決方案。在某種程度上,設計模式已經代表了一些特定情況的最佳實踐,同時也起到了軟件工程師之間溝通的“行話”的作用。理解和掌握典型的設計模式,有利于我們提高溝通、設計的效率和質量。
今天我要問你的問題是,談談你知道的設計模式?請手動實現單例模式,Spring 等框架中使用了哪些模式?
## 典型回答
大致按照模式的應用目標分類,設計模式可以分為創建型模式、結構型模式和行為型模式。
* 創建型模式,是對對象創建過程的各種問題和解決方案的總結,包括各種工廠模式(Factory、Abstract Factory)、單例模式(Singleton)、構建器模式(Builder)、原型模式(ProtoType)。
* 結構型模式,是針對軟件設計結構的總結,關注于類、對象繼承、組合方式的實踐經驗。常見的結構型模式,包括橋接模式(Bridge)、適配器模式(Adapter)、裝飾者模式(Decorator)、代理模式(Proxy)、組合模式(Composite)、外觀模式(Facade)、享元模式(Flyweight)等。
* 行為型模式,是從類或對象之間交互、職責劃分等角度總結的模式。比較常見的行為型模式有策略模式(Strategy)、解釋器模式(Interpreter)、命令模式(Command)、觀察者模式(Observer)、迭代器模式(Iterator)、模板方法模式(Template Method)、訪問者模式(Visitor)。
## 考點分析
這個問題主要是考察你對設計模式的了解和掌握程度,更多相關內容你可以參考:[https://en.wikipedia.org/wiki/Design\_Patterns。](https://en.wikipedia.org/wiki/Design_Patterns)
我建議可以在回答時適當地舉些例子,更加清晰地說明典型模式到底是什么樣子,典型使用場景是怎樣的。這里舉個 Java 基礎類庫中的例子供你參考。
首先,[專欄第 11 講](http://time.geekbang.org/column/article/8369)剛介紹過 IO 框架,我們知道 InputStream 是一個抽象類,標準類庫中提供了 FileInputStream、ByteArrayInputStream 等各種不同的子類,分別從不同角度對 InputStream 進行了功能擴展,這是典型的裝飾器模式應用案例。
識別裝飾器模式,可以通過**識別類設計特征**來進行判斷,也就是其類構造函數以**相同的**抽象類或者接口為輸入參數。
因為裝飾器模式本質上是包裝同類型實例,我們對目標對象的調用,往往會通過包裝類覆蓋過的方法,迂回調用被包裝的實例,這就可以很自然地實現增加額外邏輯的目的,也就是所謂的“裝飾”。
例如,BufferedInputStream 經過包裝,為輸入流過程增加緩存,類似這種裝飾器還可以多次嵌套,不斷地增加不同層次的功能。
~~~
public BufferedInputStream(InputStream in)
~~~
我在下面的類圖里,簡單總結了 InputStream 的裝飾模式實踐。

接下來再看第二個例子。創建型模式尤其是工廠模式,在我們的代碼中隨處可見,我舉個相對不同的 API 設計實踐。比如,JDK 最新版本中 HTTP/2 Client API,下面這個創建 HttpRequest 的過程,就是典型的構建器模式(Builder),通常會被實現成[fluent 風格](https://en.wikipedia.org/wiki/Fluent_interface)的 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 的構造函數(也有建議聲明為枚舉,這是有爭議的,我個人不建議選擇相對復雜的枚舉,畢竟日常開發不是學術研究)。這樣還有什么改進的余地嗎?
[專欄第 10 講](http://time.geekbang.org/column/article/8137)介紹 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](https://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html#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](http://hg.openjdk.java.net/jdk/jdk/file/18fba780c1d1/src/java.base/share/classes/java/lang/Runtime.java),你會發現:
* 它并沒使用復雜的雙檢鎖之類。
* 靜態實例被聲明為 final,這是被通常實踐忽略的,一定程度保證了實例不被篡改([專欄第 6 講](http://time.geekbang.org/column/article/7489)介紹過,反射之類可以繞過私有訪問限制),也有有限的保證執行順序的語義。
~~~
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 設計中使用設計模式。你至少要有個大體的印象,如:
* [BeanFactory](https://github.com/spring-projects/spring-framework/blob/master/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java)和[ApplicationContext](https://github.com/spring-projects/spring-framework/blob/master/spring-context/src/main/java/org/springframework/context/ApplicationContext.java)應用了工廠模式。
* 在 Bean 的創建中,Spring 也為不同 scope 定義的對象,提供了單例和原型等模式實現。
* 我在[專欄第 6 講](http://time.geekbang.org/column/article/7489)介紹的 AOP 領域則是使用了代理模式、裝飾器模式、適配器模式等。
* 各種事件監聽器,是觀察者模式的典型應用。
* 類似 JdbcTemplate 等則是應用了模板模式。
今天,我與你回顧了設計模式的分類和主要類型,并從 Java 核心類庫、開源框架等不同角度分析了其采用的模式,并結合單例的不同實現,分析了如何實現符合線程安全等需求的單例,希望可以對你的工程實踐有所幫助。另外,我想最后補充的是,設計模式也不是銀彈,要避免濫用或者過度設計。
## 一課一練
關于設計模式你做到心中有數了嗎?你可以思考下,在業務代碼中,經常發現大量 XXFacade,外觀模式是解決什么問題?適用于什么場景?
- 前言
- 開篇詞
- 開篇詞 -以面試題為切入點,有效提升你的Java內功
- 模塊一 Java基礎
- 第1講 談談你對Java平臺的理解?
- 第2講 Exception和Error有什么區別?
- 第3講 談談final、finally、 finalize有什么不同?
- 第4講 強引用、軟引用、弱引用、幻象引用有什么區別?
- 第5講 String、StringBuffer、StringBuilder有什么區別?
- 第6講 動態代理是基于什么原理?
- 第7講 int和Integer有什么區別?
- 第8講 對比Vector、ArrayList、LinkedList有何區別?
- 第9講 對比Hashtable、HashMap、TreeMap有什么不同?
- 第10講 如何保證集合是線程安全的? ConcurrentHashMap如何實現高效地線程安全?
- 第11講 Java提供了哪些IO方式? NIO如何實現多路復用?
- 第12講 Java有幾種文件拷貝方式?哪一種最高效?
- 第13講 談談接口和抽象類有什么區別?
- 第14講 談談你知道的設計模式?
- 模塊二 Java進階
- 第15講 synchronized和ReentrantLock有什么區別呢?
- 第16講 synchronized底層如何實現?什么是鎖的升級、降級?
- 第17講 一個線程兩次調用start()方法會出現什么情況?
- 第18講 什么情況下Java程序會產生死鎖?如何定位、修復?
- 第19講 Java并發包提供了哪些并發工具類?
- 第20講 并發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么區別?
- 第21講 Java并發類庫提供的線程池有哪幾種? 分別有什么特點?
- 第22講 AtomicInteger底層實現原理是什么?如何在自己的產品代碼中應用CAS操作?
- 第23講 請介紹類加載過程,什么是雙親委派模型?
- 第24講 有哪些方法可以在運行時動態生成一個Java類?
- 第25講 談談JVM內存區域的劃分,哪些區域可能發生OutOfMemoryError?
- 第26講 如何監控和診斷JVM堆內和堆外內存使用?
- 第27講 Java常見的垃圾收集器有哪些?
- 第28講 談談你的GC調優思路?
- 第29講 Java內存模型中的happen-before是什么?
- 第30講 Java程序運行在Docker等容器環境有哪些新問題?
- 模塊三 Java安全基礎
- 第31講 你了解Java應用開發中的注入攻擊嗎?
- 第32講 如何寫出安全的Java代碼?
- 模塊四 Java性能基礎
- 第33講 后臺服務出現明顯“變慢”,談談你的診斷思路?
- 第34講 有人說“Lambda能讓Java程序慢30倍”,你怎么看?
- 第35講 JVM優化Java代碼時都做了什么?
- 模塊五 Java應用開發擴展
- 第36講 談談MySQL支持的事務隔離級別,以及悲觀鎖和樂觀鎖的原理和應用場景?
- 第37講 談談Spring Bean的生命周期和作用域?
- 第38講 對比Java標準NIO類庫,你知道Netty是如何實現更高性能的嗎?
- 第39講 談談常用的分布式ID的設計方案?Snowflake是否受冬令時切換影響?
- 周末福利
- 周末福利 談談我對Java學習和面試的看法
- 周末福利 一份Java工程師必讀書單
- 結束語
- 結束語 技術沒有終點
- 結課測試 Java核心技術的這些知識,你真的掌握了嗎?