上一課時我們講了單例模式的 8 種實現方式以及它的優缺點,可見設計模式的內容是非常豐富且非常有趣。我們在一些優秀的框架中都能找到設計模式的具體使用,比如前面 MyBatis 中(第 13 課時)講的那些設計模式以及具體的使用場景,但由于設計模式的內容比較多,有些常用的設計模式在 MyBatis 課時中并沒有講到。因此本課時我們就以全局的視角,來重點學習一下這些常用設計模式。
我們本課時的面試題是,你知道哪些設計模式?它的使用場景有哪些?它們有哪些優缺點?
#### 典型回答
設計模式從大的維度來說,可以分為三大類:創建型模式、結構型模式及行為型模式,這三大類下又有很多小分類。
創建型模式是指提供了一種對象創建的功能,并把對象創建的過程進行封裝隱藏,讓使用者只關注具體的使用而并非對象的創建過程。它包含的設計模式有單例模式、工廠模式、抽象工廠模式、建造者模式及原型模式。
結構型模式關注的是對象的結構,它是使用組合的方式將類結合起來,從而可以用它來實現新的功能。它包含的設計模式是代理模式、組合模式、裝飾模式及外觀模式。
行為型模式關注的是對象的行為,它是把對象之間的關系進行梳理劃分和歸類。它包含的設計模式有模板方法模式、命令模式、策略模式和責任鏈模式。
下面我們來看看那些比較常見的設計模式的定義和具體的應用場景。
* [ ] 1. 單例模式
單例模式是指一個類在運行期間始終只有一個實例,我們把它稱之為單例模式。
單例模式的典型應用場景是 Spring 中 Bean 實例,它默認就是 singleton 單例模式。
單例模式的優點很明顯,可以有效地節約內存,并提高對象的訪問速度,同時避免重復創建和銷毀對象所帶來的性能消耗,尤其是對頻繁創建和銷毀對象的業務場景來說優勢更明顯。然而單例模式一般不會實現接口,因此它的擴展性不是很好,并且單例模式違背了單一職責原則,因為單例類在一個方法中既創建了類又提供類對象的復合操作,這樣就違背了單一職責原則,這也是單例模式的缺點所在。
* [ ] 2. 原型模式
原型模式屬于創建型模式,它是指通過“克隆”來產生一個新的對象。所以它的核心方法是 clone(),我們通過該方法就可以復制出一個新的對象。
在 Java 語言中我們只需要實現 Cloneable 接口,并重寫 clone() 方法就可以實現克隆了,實現代碼如下:
```
public class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
// 創建一個新對象
People p1 = new People();
p1.setId(1);
p1.setName("Java");
// 克隆對象
People p2 = (People) p1.clone();
// 輸出新對象的名稱
System.out.println("People 2:" + p2.getName());
}
static class People implements Cloneable {
private Integer id;
private String name;
/**
* 重寫 clone 方法
*/
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
```
程序的執行結果為:
```
People 2:Java
```
但需要注意的是,以上代碼為淺克隆的實現方式,如果要實現深克隆(對所有屬性無論是基本類型還是引用類型的克隆)可以通過以下手段實現:
*
所有對象都實現克隆方法;
* 通過構造方法實現深克隆;
* 使用 JDK 自帶的字節流實現深克隆;
* 使用第三方工具實現深克隆,比如 Apache Commons Lang;
* 使用 JSON 工具類實現深克隆,比如 Gson、FastJSON 等。
具體的實現代碼可以參考我們第 07 課時的內容。
原型模式的典型使用場景是 Java 語言中的 Object.clone() 方法,它的優點是性能比較高,因為它是通過直接拷貝內存中的二進制流實現的復制,因此具備很好的性能。它的缺點是在對象層級嵌套比較深時,復制的代碼實現難度比較大。
* [ ] 3. 命令模式
命令模式屬于行為模式的一種,它是指將一個請求封裝成一個對象,并且提供命令的撤銷和恢復功能。說得簡單一點就是將發送者、接收者和調用命令封裝成獨立的對象,以供客戶端來調用,它的具體實現代碼如下。
接收者的示例代碼:
```
// 接收者
class Receiver {
public void doSomething() {
System.out.println("執行業務邏輯");
}
}
```
命令對象的示例代碼:
```
// 命令接口
interface Command {
void execute();
}
// 具體命令類
class ConcreteCommand implements Command {
private Receiver receiver;
public ConcreteCommand(Receiver receiver) {
this.receiver = receiver;
}
public void execute() {
this.receiver.doSomething();
}
}
```
請求者的示例代碼:
```
// 請求者類
class Invoker {
// 持有命令對象
private Command command;
public Invoker(Command command) {
this.command = command;
}
// 請求方法
public void action() {
this.command.execute();
}
}
```
客戶端的示例代碼:
```
// 客戶端
class Client {
public static void main(String[] args) {
// 創建接收者
Receiver receiver = new Receiver();
// 創建命令對象,設定接收者
Command command = new ConcreteCommand(receiver);
// 創建請求者,把命令對象設置進去
Invoker invoker = new Invoker(command);
// 執行方法
invoker.action();
}
}
```
Spring 框架中的 JdbcTemplate 使用的就是命令模式,它的優點是降低了系統的耦合度,新增的命令可以很容易地添加到系統中;其缺點是如果命令很多就會造成命令類的代碼很長,增加了維護的復雜性。
考點分析
對于設計模式的掌握程度來說,一般面試官都不會要求你要精通所有的設計模式,但需要對幾個比較常用的設計模式有所理解和掌握才行。本課時介紹了 3 種設計模式加上 MyBatis 那一課時介紹的 7 種設計模式,足以應對日常的工作和一般性面試了。
和此知識點相關的面試題還有,軟件中的六大設計原則是什么?這也是面試中經常會問的面試題,同時也是優秀程序設計的指導思想。
#### 知識擴展:六大設計原則
六大設計原則包括:單一職責原則、里氏替換原則、依賴倒置原則、接口隔離原則、迪米特法則、開閉原則,接下來我們一一來看看它們分別是什么。
* [ ] 1. 單一職責原則
單一職責是指一個類只負責一個職責。比如現在比較流行的微服務,就是將之前很復雜耦合性很高的業務,分成多個獨立的功能單一的簡單接口,然后通過服務編排組裝的方式實現不同的業務需求,而這種細粒度的獨立接口就是符合單一職責原則的具體實踐。
* [ ] 2. 開閉原則
開閉原則指的是對拓展開放、對修改關閉。它是說我們在實現一個新功能時,首先應該想到的是擴展原來的功能,而不是修改之前的功能。
這個設計思想非常重要,也是一名優秀工程師所必備的設計思想。至于為什么要這樣做?其實非常簡單,我們團隊在開發后端接口時遵循的也是這個理念。
隨著軟件越做越大,對應的客戶端版本也越來越多,而這些客戶端都是安裝在用戶的手機上。因此我們不能保證所有用戶手中的 App(客戶端)都一直是最新版本的,并且也不能每次都強制用戶進行升級或者是協助用戶去升級,那么我們在開發新功能時,就強制要求團隊人員不允許直接修改原來的老接口,而是要在原有的接口上進行擴展升級。
因為直接修改老接口帶來的隱患是老版本的 App 將不能使用,這顯然不符合我們的要求。那么此時在老接口上進行擴展無疑是最好的解決方案,因為這樣我們既可以滿足新業務也不用擔心新加的代碼會影響到老版本的使用。
* [ ] 3. 里氏替換原則
里氏替換原則是面向對象(OOP)編程的實現基礎,它指的是所有引用了父類的地方都能被子類所替代,并且使用子類替代不會引發任何異常或者是錯誤的出現。
比如,如果把鴕鳥歸為了“鳥”類,那么鴕鳥就是“鳥”的子類,但是鳥類會飛,而鴕鳥不會飛,那么鴕鳥就違背了里氏替換原則。
* [ ] 4. 依賴倒置原則
依賴倒置原則指的是要針對接口編程,而不是面向具體的實現編程。也就說高層模塊不應該依賴底層模塊,因為底層模塊的職責通常更單一,不足以應對高層模塊的變動,因此我們在實現時,應該依賴高層模塊而非底層模塊。
比如我們要從 A 地點去往 B 地點,此時應該掏出手機預約一個“車”,而這個“車”就是一個頂級的接口,它的實現類可以是各種各樣的車,不同廠商的車甚至是不同顏色的車,而不應該依賴于某一個具體的車。例如,我們依賴某個車牌為 XXX 的車,那么一旦這輛車發生了故障或者這輛車正拉著其他乘客,就會對我的出行帶來不便。所以我們應該依賴是“車”這一個頂級接口,而不是具體的某一輛車。
* [ ] 5. 接口隔離原則
接口隔離原則是指使用多個專門的接口比使用單一的總接口要好,即接口應該是相互隔離的小接口,而不是一個臃腫且龐雜的大接口。
使用接口隔離原則的好處是避免接口的污染,提高了程序的靈活性。
可以看出,接口隔離原則和單一職責原則的概念很像,單一職責原則要求接口的職責要單一,而接口隔離原則要求接口要盡量細化,二者雖然有異曲同工之妙,但可以看出單一職責原則要求的粒度更細。
* [ ] 6. 迪米特法則
迪米特法則又叫最少知識原則,它是指一個類對于其他類知道的越少越好。
迪米特法則設計的初衷是降低類之間的耦合,讓每個類對其他類都不了解,因此每個類都在做自己的事情,這樣就能降低類之間的耦合性。
這就好比我們在一些電視中看到的有些人在遇到強盜時,會選擇閉著眼睛不看強盜,因為知道的信息越少反而對自己就越安全,這就是迪米特法則的基本思想。
#### 小結
本課時我們講了 3 種設計模式:單例模式、原型模式和命令模式,結合 MyBatis 那一課時(第 13 課時)介紹的 7 種設計模式,足以應對日常的工作和一般性的面試了。最后我們還介紹了設計模式中的 6 大設計原則:單一職責原則、開閉原則、里氏替換原則、依賴倒置原則、接口隔離原則和迪米特法則,我們應該結合這些概念對照日常項目中的代碼,看看還有哪些代碼可以進行優化和改進。
- 前言
- 開篇詞
- 開篇詞:大廠技術面試“潛規則”
- 模塊一:Java 基礎
- 第01講:String 的特點是什么?它有哪些重要的方法?
- 第02講:HashMap 底層實現原理是什么?JDK8 做了哪些優化?
- 第03講:線程的狀態有哪些?它是如何工作的?
- 第04講:詳解 ThreadPoolExecutor 的參數含義及源碼執行流程?
- 第05講:synchronized 和 ReentrantLock 的實現原理是什么?它們有什么區別?
- 第06講:談談你對鎖的理解?如何手動模擬一個死鎖?
- 第07講:深克隆和淺克隆有什么區別?它的實現方式有哪些?
- 第08講:動態代理是如何實現的?JDK Proxy 和 CGLib 有什么區別?
- 第09講:如何實現本地緩存和分布式緩存?
- 第10講:如何手寫一個消息隊列和延遲消息隊列?
- 模塊二:熱門框架
- 第11講:底層源碼分析 Spring 的核心功能和執行流程?(上)
- 第12講:底層源碼分析 Spring 的核心功能和執行流程?(下)
- 第13講:MyBatis 使用了哪些設計模式?在源碼中是如何體現的?
- 第14講:SpringBoot 有哪些優點?它和 Spring 有什么區別?
- 第15講:MQ 有什么作用?你都用過哪些 MQ 中間件?
- 模塊三:數據庫相關
- 第16講:MySQL 的運行機制是什么?它有哪些引擎?
- 第17講:MySQL 的優化方案有哪些?
- 第18講:關系型數據和文檔型數據庫有什么區別?
- 第19講:Redis 的過期策略和內存淘汰機制有什么區別?
- 第20講:Redis 怎樣實現的分布式鎖?
- 第21講:Redis 中如何實現的消息隊列?實現的方式有幾種?
- 第22講:Redis 是如何實現高可用的?
- 模塊四:Java 進階
- 第23講:說一下 JVM 的內存布局和運行原理?
- 第24講:垃圾回收算法有哪些?
- 第25講:你用過哪些垃圾回收器?它們有什么區別?
- 第26講:生產環境如何排除和優化 JVM?
- 第27講:單例的實現方式有幾種?它們有什么優缺點?
- 第28講:你知道哪些設計模式?分別對應的應用場景有哪些?
- 第29講:紅黑樹和平衡二叉樹有什么區別?
- 第30講:你知道哪些算法?講一下它的內部實現過程?
- 模塊五:加分項
- 第31講:如何保證接口的冪等性?常見的實現方案有哪些?
- 第32講:TCP 為什么需要三次握手?
- 第33講:Nginx 的負載均衡模式有哪些?它的實現原理是什么?
- 第34講:Docker 有什么優點?使用時需要注意什么問題?
- 彩蛋
- 彩蛋:如何提高面試成功率?