繼上一篇的物件導向設計的設計原則,本篇是物件導向設計系列的第二個部分:物件導向設計的設計模式的第一篇文章。
最開始說一下什麼是設計模式。關於設計模式的概念,有很多不同的版本,在這裡說一下我個人比較贊同的一個說法:
設計模式用於在特定的條件下為一些重複出現的軟體設計問題提供合理的、有效的解決方案。
去掉一些定語的修飾,這句話精簡為:
設計模式為問題提供方案。
簡單來看,設計模式其實就是針對某些問題的一些方案。在軟體開發中,即使很多人在用不同的語言去開發不同的業務,但是很多時候這些人遇到的問題抽象出來都是相似的。一些卓越的開發者將一些常出現的問題和對應的解決方案彙總起來,總結出了這些設計模式。
因此掌握了這些設計模式,可以讓我們更好地去解決開發過程中遇到的一些常見問題。而且對這些問題的解決方案的掌握程度越好,我們就越能夠打破語言本身的限制去解決問題,也就是增強“軟體開發的內功”。
介紹設計模式最著名的一本書莫屬《設計模式 可複用物件導向軟體的基礎》這本書,書中共介紹了23個設計模式。而這些設計模式分為三大類:
- 建立型設計模式:側重於物件的建立。
- 結構型設計模式:側重於介面的設計和系統的結構。
- 行為型設計模式:側重於類或物件的行為。
而本篇作為該系列的第一篇,講解的是設計模式中的6個建立型設計模式:
- 簡單工廠模式(Simple Factory Pattern)
- 工廠方法模式(Factory Method Pattern)
- 抽象工廠模式(Abstract Factory Pattern)
- 單例模式(Singleton Pattern)
- 生成器模式(Builder Pattern)
- 原型模式(Prototype Pattern)
注意:簡單工廠模式不是 GoF總結出來的23種設計模式之一,不存在於《設計模式 可複用物件導向軟體的基礎》這本書中。
在物件導向設計中,類與物件幾乎是構成所有系統的基本元素,因此我認為學好了建立型模式才是學會設計系統的第一步:因為你應該知道如何去建立一些特定性質的物件,這才是設計好的系統的開始。
在講解這6個設計模式之前先說一下該系列文章的講解方式:
從更多維度來理解一件事物有助於更深刻地理解它,因此每個設計模式我都會從以下這幾點來講解:
- 定義
- 使用場景
- 成員與類圖
- 程式碼示例
- 優點
- 缺點
- iOS SDK 和 JDK 中的應用
最後一項:“iOS SDK 和 JDK中的應用”講解的是該設計模式在Objective-C和java語言(JDK)中的應用。
首先我們看一下簡單工廠模式:
一. 簡單工廠模式
定義
簡單工廠模式(Simple Factory Pattern):專門定義一個類(工廠類)來負責建立其他類的例項。可以根據建立方法的引數來返回不同類的例項,被建立的例項通常都具有共同的父類。
簡單工廠模式又稱為靜態工廠方法(Static Factory Method)模式,它屬於類建立型模式。
適用場景
如果我們希望將一些為數不多的類似的物件的建立和他們的建立細節分離開,也不需要知道物件的具體型別,可以使用簡單工廠模式。
舉個形象點的例子:在前端開發中,常常會使用外觀各式各樣的按鈕:比如有的按鈕有圓角,有的按鈕有陰影,有的按鈕有邊框,有的按鈕無邊框等等。但是因為同一種樣式的按鈕可以出現在專案的很多地方,所以如果在每個地方都把建立按鈕的邏輯寫一遍的話顯然是會造成程式碼的重複(而且由於業務的原因有的按鈕的建立邏輯能比較複雜,程式碼量大)。
那麼為了避免重複程式碼的產生,我們可以將這些建立按鈕的邏輯都放在一個“工廠”裡面,讓這個工廠來根據你的需求(傳入的引數)來建立對應的按鈕並返回給你。這樣一來,同樣型別的按鈕在多個地方使用的時候,就可以只給這個工廠傳入其對應的引數並拿到返回的按鈕即可。
下面來看一下簡單工廠模式的成員和類圖。
成員與類圖
成員
簡單工廠模式的結構比較簡單,一共只有三個成員:
- 工廠(Factory):工廠負責實現建立所有產品例項的邏輯
- 抽象產品(Product):抽象產品是工廠所建立的所有產品物件的父類,負責宣告所有產品例項所共有的公共介面。
- 具體產品(Concrete Product):具體產品是工廠所建立的所有產品物件類,它以自己的方式來實現其共同父類宣告的介面。
下面通過類圖來看一下各個成員之間的關係:
模式類圖
從類圖中可以看出,工廠類提供一個靜態方法:通過傳入的字串來製造其所對應的產品。
程式碼示例
場景概述
舉一個店鋪售賣不同品牌手機的例子:店鋪,即客戶端類向手機工廠購進手機售賣。
場景分析
該場景可以使用簡單工廠的角色來設計:
- 抽象產品:
Phone
,是所有具體產品類的父類,提供一個公共介面packaging
表示手機的裝箱並送到店鋪。 - 具體產品:不同品牌的手機,iPhone手機類(
IPhone
),小米手機類(MIPhone
),華為手機類(HWPhone
)。 - 工廠:
PhoneFactory
根據不同的引數來建立不同的手機。 - 客戶端類:店鋪類
Store
負責售賣手機。
程式碼實現
抽象產品類Phone
:
//================== Phone.h ==================
@interface Phone : NSObject
//package to store
- (void)packaging;
@end
複製程式碼
具體產品類 IPhone
:
//================== IPhone.h ==================
@interface IPhone : Phone
@end
//================== IPhone.m ==================
@implementation IPhone
- (void)packaging{
NSLog(@"IPhone has been packaged");
}
@end
複製程式碼
具體產品類 MIPhone
:
//================== MIPhone.h ==================
@interface MIPhone : Phone
@end
//================== MIPhone.m ==================
@implementation MIPhone
- (void)packaging{
NSLog(@"MIPhone has been packaged");
}
@end
複製程式碼
具體產品類:HWPhone
:
//================== HWPhone.h ==================
@interface HWPhone : Phone
@end
//================== HWPhone.m ==================
@implementation HWPhone
- (void)packaging{
NSLog(@"HUAWEI Phone has been packaged");
}
@end
複製程式碼
以上是抽象產品類以及它的三個子類:蘋果手機類,小米手機類和華為手機類。
下面看一下工廠類 PhoneFactory
:
//================== PhoneFactory.h ==================
@interface PhoneFactory : NSObject
+ (Phone *)createPhoneWithTag:(NSString *)tag;
@end
//================== PhoneFactory.m ==================
#import "IPhone.h"
#import "MIPhone.h"
#import "HWPhone.h"
@implementation PhoneFactory
+ (Phone *)createPhoneWithTag:(NSString *)tag{
if ([tag isEqualToString:@"i"]) {
IPhone *iphone = [[IPhone alloc] init];
return iphone;
}else if ([tag isEqualToString:@"MI"]){
MIPhone *miPhone = [[MIPhone alloc] init];
return miPhone;
}else if ([tag isEqualToString:@"HW"]){
HWPhone *hwPhone = [[HWPhone alloc] init];
return hwPhone;
}else{
return nil;
}
}
@end
複製程式碼
工廠類向外部(客戶端)提供了一個創造手機的介面
createPhoneWithTag:
,根據傳入引數的不同可以返回不同的具體產品類。因此客戶端只需要知道它所需要的產品所對應的引數即可獲得對應的產品了。
在本例中,我們宣告瞭店鋪類 Store
為客戶端類:
//================== Store.h ==================
#import "Phone.h"
@interface Store : NSObject
- (void)sellPhone:(Phone *)phone;
@end
//================== Store.m ==================
@implementation Store
- (void)sellPhone:(Phone *)phone{
NSLog(@"Store begins to sell phone:%@",[phone class]);
}
@end
複製程式碼
客戶端類宣告瞭一個售賣手機的介面
sellPhone:
。表示它可以售賣作為引數所傳入的手機。
最後我們用程式碼模擬一下這個實際場景:
//================== Using by client ==================
//1. A phone store wants to sell iPhone
Store *phoneStore = [[Store alloc] init];
//2. create phone
Phone *iPhone = [PhoneFactory createPhoneWithTag:@"i"];
//3. package phone to store
[iphone packaging];
//4. store sells phone after receving it
[phoneStore sellPhone:iphone];
複製程式碼
上面程式碼的解讀:
- 最開始例項化一個商店,商店打算賣蘋果手機
- 商店委託工廠給他製作一臺iPhone手機,傳入對應的欄位
i
。 - 手機生產好以後打包送到商店
- 商店售賣手機
在這裡我們需要注意的是:商店從工廠拿到手機不需要了解手機制作的過程,只需要知道它要工廠做的是手機(只知道Phone
類即可),和需要給工廠類傳入它所需手機所對應的引數即可(這裡的iPhone手機對應的引數就是i
)。
下面我們看一下該例子對應的 UML類圖,可以更直觀地看一下各個成員之間的關係:
程式碼對應的類圖
優點
- 客戶端只需要給工廠類傳入一個正確的(約定好的)引數,就可以獲取你所需要的物件,而不需要知道其建立細節,一定程度上減少系統的耦合。
- 客戶端無須知道所建立的具體產品類的類名,只需要知道具體產品類所對應的引數即可,減少開發者的記憶成本。
缺點
- 如果業務上新增新產品的話,就需要修改工廠類原有的判斷邏輯,這其實是違背了開閉原則的。
- 在產品型別較多時,有可能造成工廠邏輯過於複雜。所以簡單工廠模式比較適合產品種類比較少而且增多的概率很低的情況。
iOS SDK 和 JDK 中的應用
- Objective-C中的類簇就是簡單工廠設計模式的一個應用。如果給
NSNumber
的工廠方法傳入不同型別的資料,則會返回不同資料所對應的NSNumber
的子類。 - JDK中的
Calendar
類中的私有的createCalendar(TimeZone zone, Locale aLocale)
方法通過不同的入參來返回不同型別的Calendar子類的例項。
二. 工廠方法模式
定義
工廠方法模式(Factory Method Pattern)又稱為工廠模式,工廠父類負責定義建立產品物件的公共介面,而工廠子類則負責生成具體的產品物件,即通過不同的工廠子類來建立不同的產品物件。
適用場景
工廠方法模式的適用場景與簡單工廠類似,都是建立資料和行為比較類似的物件。但是和簡單工廠不同的是:在工廠方法模式中,因為建立物件的責任移交給了抽象工廠的子類,因此客戶端需要知道其所需產品所對應的工廠子類,而不是簡單工廠中的引數。
下面我們看一下工廠方法模式的成員和類圖。
成員與類圖
成員
工廠方法模式包含四個成員:
- 抽象工廠(Abstract Factory):抽象工廠負責宣告具體工廠的建立產品的介面。
- 具體工廠(Concrete Factory):具體工廠負責建立產品。
- 抽象產品(Abstract Product):抽象產品是工廠所建立的所有產品物件的父類,負責宣告所有產品例項所共有的公共介面。
- 具體產品(Concrete Product):具體產品是工廠所建立的所有產品物件類,它以自己的方式來實現其共同父類宣告的介面。
下面通過類圖來看一下各個成員之間的關係:
模式類圖
從類圖中我們可以看到:抽象工廠負責定義具體工廠必須實現的介面,而建立產品物件的任務則交給具體工廠,由特定的子工廠來建立其對應的產品。
這使得工廠方法模式可以允許系統在不修改原有工廠的情況下引進新產品:只需要建立新產品類和其所對應的工廠類即可。
程式碼示例
場景概述
同樣也是模擬上面的簡單工廠例子中的場景(手機商店賣手機),但是由於這次是由工廠方法模式來實現的,因此在程式碼設計上會有變化。
場景分析
與簡單工廠模式不同的是:簡單工廠模式裡面只有一個工廠,而工廠方法模式裡面有一個抽象工廠和繼承於它的具體工廠。
因此同樣的三個品牌的手機,我們可以通過三個不同的具體工廠:蘋果手機工廠(IPhoneFactory
),小米手機工廠 (MIPhoneFactory
),華為手機工廠(HWPhoneFactory
)來生產。而這些具體工廠類都會繼承於抽象手機工廠類:PhoneFactory
,它來宣告生產手機的介面。
下面我們用程式碼來具體來看一下工廠類(抽象工廠和具體工廠)的設計:
程式碼實現
首先我們宣告一個抽象工廠類 PhoneFactory
:
//================== PhoneFactory.h ==================
#import "Phone.h"
@interface PhoneFactory : NSObject
+ (Phone *)createPhone;
@end
//================== PhoneFactory.m ==================
@implementation PhoneFactory
+ (Phone *)createPhone{
//implemented by subclass
return nil;
}
@end
複製程式碼
抽象工廠類給具體工廠提供了生產手機的介面,因此不同的具體工廠可以按照自己的方式來生產手機。下面看一下具體工廠:
蘋果手機工廠 IPhoneFactory
//================== IPhoneFactory.h ==================
@interface IPhoneFactory : PhoneFactory
@end
//================== IPhoneFactory.m ==================
#import "IPhone.h"
@implementation IPhoneFactory
+ (Phone *)createPhone{
IPhone *iphone = [[IPhone alloc] init];
NSLog(@"iPhone has been created");
return iphone;
}
@end
複製程式碼
小米手機工廠 MIPhoneFactory
:
//================== MIPhoneFactory.h ==================
@interface MPhoneFactory : PhoneFactory
@end
//================== MIPhoneFactory.m ==================
#import "MiPhone.h"
@implementation MPhoneFactory
+ (Phone *)createPhone{
MiPhone *miPhone = [[MiPhone alloc] init];
NSLog(@"MIPhone has been created");
return miPhone;
}
@end
複製程式碼
華為手機工廠 HWPhoneFactory
:
//================== HWPhoneFactory.h ==================
@interface HWPhoneFactory : PhoneFactory
@end
//================== HWPhoneFactory.m ==================
#import "HWPhone.h"
@implementation HWPhoneFactory
+ (Phone *)createPhone{
HWPhone *hwPhone = [[HWPhone alloc] init];
NSLog(@"HWPhone has been created");
return hwPhone;
}
@end
複製程式碼
以上就是宣告的抽象工廠類和具體工廠類。因為生產手機的責任分配給了各個具體工廠類,因此客戶端只需要委託所需手機所對應的工廠就可以獲得其生產的手機了。
因為抽象產品類
Phone
和三個具體產品類(IPhone
,MIPhone
,HWPhone
)和簡單工廠模式中介紹的例子中的一樣,因此這裡就不再重複介紹了。
下面我們用程式碼模擬一下該場景:
//================== Using by client ==================
//A phone store
Store *phoneStore = [[Store alloc] init];
//phoneStore wants to sell iphone
Phone *iphone = [IPhoneFactory createPhone];
[iphone packaging];
[phoneStore sellPhone:iphone];
//phoneStore wants to sell MIPhone
Phone *miPhone = [MPhoneFactory createPhone];
[miPhone packaging];
[phoneStore sellPhone:miPhone];
//phoneStore wants to sell HWPhone
Phone *hwPhone = [HWPhoneFactory createPhone];
[hwPhone packaging];
[phoneStore sellPhone:hwPhone];
複製程式碼
由上面的程式碼可以看出:客戶端phoneStore
只需委託iPhone,MIPhone,HWPhone對應的工廠即可獲得對應的手機了。
而且以後如果增加其他牌子的手機,例如魅族手機,就可以宣告一個魅族手機類和魅族手機的工廠類並實現createPhone
這個方法即可,而不需要改動原有已經宣告好的各個手機類和具體工廠類。
下面我們看一下該例子對應的 UML類圖,可以更直觀地看一下各個成員之間的關係:
程式碼對應的類圖
優點
- 使用者只需要關心其所需產品對應的具體工廠是哪一個即可,不需要關心產品的建立細節,也不需要知道具體產品類的類名。
- 當系統中加入新產品時,不需要修改抽象工廠和抽象產品提供的介面,也無須修改客戶端和其他的具體工廠和具體產品,而只要新增一個具體工廠和與其對應的具體產品就可以了,符合了開閉原則(這一點與簡單工廠模式不同)。
缺點
- 當系統中加入新產品時,除了需要提供新的產品類之外,還要提供與其對應的具體工廠類。因此係統中類的個數將成對增加,增加了系統的複雜度。
iOS SDK 和 JDK 中的應用
- 暫未發現iOS SDK中使用工廠方法的例子,有知道的小夥伴歡迎留言。
- 在JDK中,
Collection
介面宣告瞭iterator()
方法,該方法返回結果的抽象類是Iterator
。ArrayList
就實現了這個介面;,而ArrayList對應的具體產品是Itr
。
三. 抽象工廠模式
定義
抽象工廠模式(Abstract Factory Pattern):提供一個建立一系列相關或相互依賴物件的介面,而無須指定它們具體的類。
適用場景
有時候我們需要一個工廠可以提供多個產品物件,而不是單一的產品物件。比如系統中有多於一個的產品族,而每次只使用其中某一產品族,屬於同一個產品族的產品將在一起使用。
在這裡說一下產品族和產品等級結構的概念:
- 產品族:同一工廠生產的不同產品
- 產品等級結構:同一型別產品的不同實現
用一張圖來幫助理解:
在上圖中:
- 縱向的,不同形狀,相同色系的圖形屬於同一產品組的產品,而同一產品族的產品對應的是同一個工廠;
- 橫向的,同一形狀,不同色系的圖形屬於統一產品等級結構的產品,而統一產品等級結構的產品對應的是同一個工廠方法。
下面再舉一個例子幫助大家理解:
我們將小米,華為,蘋果公司比作抽象工廠方法裡的工廠:這三個工廠都有自己生產的手機,平板和電腦。 那麼小米手機,小米平板,小米電腦就屬於小米這個工廠的產品族;同樣適用於華為工廠和蘋果工廠。 而小米手機,華為手機,蘋果手機則屬於同一產品等級結構:手機的產品等級結構;平板和電腦也是如此。
結合這個例子對上面的圖做一個修改可以更形象地理解抽象工廠方法的設計:
上面的關於產品族和產品等級結構的說法參考了慕課網實戰課程:java設計模式精講 Debug 方式+記憶體分析的6-1節。
成員與類圖
成員
抽象工廠模式的成員和工廠方法模式的成員是一樣的,只不過抽象工廠方法裡的工廠是面向產品族的。
- 抽象工廠(Abstract Factory):抽象工廠負責宣告具體工廠的建立產品族內的所有產品的介面。
- 具體工廠(Concrete Factory):具體工廠負責建立產品族內的產品。
- 抽象產品(Abstract Product):抽象產品是工廠所建立的所有產品物件的父類,負責宣告所有產品例項所共有的公共介面。
- 具體產品(Concrete Product):具體產品是工廠所建立的所有產品物件類,它以自己的方式來實現其共同父類宣告的介面。
下面通過類圖來看一下各個成員之間的關係:
模式類圖
- 抽象工廠模式與工廠方法模式最大的區別在於,工廠方法模式針對的是一個產品等級結構,而抽象工廠模式則需要面對多個產品等級結構
- 增加新的具體工廠和產品族很方便,無須修改已有系統,符合“開閉原則”。
程式碼示例
場景概述
由於抽象工廠方法裡的工廠是面向產品族的,所以為了貼合抽象工廠方法的特點,我們將上面的場景做一下調整:在上面兩個例子中,商店只賣手機。在這個例子中我們讓商店也賣電腦:分別是蘋果電腦,小米電腦,華為電腦。
場景分析
如果我們還是套用上面介紹過的工廠方法模式來實現該場景的話,則需要建立三個電腦產品對應的工廠:蘋果電腦工廠,小米電腦工廠,華為電腦工廠。這就導致類的個數直線上升,以後如果還增加其他的產品,還需要新增其對應的工廠類,這顯然是不夠優雅的。
仔細看一下這六個產品的特點,我們可以把這它們劃分在三個產品族裡面:
- 蘋果產品族:蘋果手機,蘋果電腦
- 小米產品族:小米手機,小米電腦
- 華為產品族:華為手機,華為電腦
而抽象方法恰恰是面向產品族設計的,因此該場景適合使用的是抽象工廠方法。下面結合程式碼來看一下該如何設計。
程式碼實現
首先引入電腦的基類和各個品牌的電腦類:
電腦基類:
//================== Computer.h ==================
@interface Computer : NSObject
//package to store
- (void)packaging;
@end
//================== Computer.m ==================
@implementation Computer
- (void)packaging{
//implemented by subclass
}
@end
複製程式碼
蘋果電腦類 MacBookComputer
:
//================== MacBookComputer.h ==================
@interface MacBookComputer : Computer
@end
//================== MacBookComputer.m ==================
@implementation MacBookComputer
- (void)packaging{
NSLog(@"MacBookComputer has been packaged");
}
@end
複製程式碼
小米電腦類 MIComputer
:
//================== MIComputer.h ==================
@interface MIComputer : Computer
@end
//================== MIComputer.m ==================
@implementation MIComputer
- (void)packaging{
NSLog(@"MIComputer has been packaged");
}
@end
複製程式碼
華為電腦類 MateBookComputer
:
//================== MateBookComputer.h ==================
@interface MateBookComputer : Computer
@end
//================== MateBookComputer.m ==================
@implementation MateBookComputer
- (void)packaging{
NSLog(@"MateBookComputer has been packaged");
}
@end
複製程式碼
引入電腦相關產品類以後,我們需要重新設計工廠類。因為抽象工廠方法模式的工廠是面向產品族的,所以抽象工廠方法模式裡的工廠所建立的是同一產品族的產品。下面我們看一下抽象工廠方法模式的工廠該如何設計:
首先建立所有工廠都需要整合的抽象工廠,它宣告瞭生產同一產品族的所有產品的介面:
//================== Factory.h ==================
#import "Phone.h"
#import "Computer.h"
@interface Factory : NSObject
+ (Phone *)createPhone;
+ (Computer *)createComputer;
@end
//================== Factory.m ==================
@implementation Factory
+ (Phone *)createPhone{
//implemented by subclass
return nil;
}
+ (Computer *)createComputer{
//implemented by subclass
return nil;
}
@end
複製程式碼
接著,根據不同的產品族,我們建立不同的具體工廠:
首先是蘋果產品族工廠 AppleFactory
:
//================== AppleFactory.h ==================
@interface AppleFactory : Factory
@end
//================== AppleFactory.m ==================
#import "IPhone.h"
#import "MacBookComputer.h"
@implementation AppleFactory
+ (Phone *)createPhone{
IPhone *iPhone = [[IPhone alloc] init];
NSLog(@"iPhone has been created");
return iPhone;
}
+ (Computer *)createComputer{
MacBookComputer *macbook = [[MacBookComputer alloc] init];
NSLog(@"Macbook has been created");
return macbook;
}
@end
複製程式碼
接著是小米產品族工廠 MIFactory
:
//================== MIFactory.h ==================
@interface MIFactory : Factory
@end
//================== MIFactory.m ==================
#import "MIPhone.h"
#import "MIComputer.h"
@implementation MIFactory
+ (Phone *)createPhone{
MIPhone *miPhone = [[MIPhone alloc] init];
NSLog(@"MIPhone has been created");
return miPhone;
}
+ (Computer *)createComputer{
MIComputer *miComputer = [[MIComputer alloc] init];
NSLog(@"MIComputer has been created");
return miComputer;
}
@end
複製程式碼
最後是華為產品族工廠 HWFactory
:
//================== HWFactory.h ==================
@interface HWFactory : Factory
@end
//================== HWFactory.m ==================
#import "HWPhone.h"
#import "MateBookComputer.h"
@implementation HWFactory
+ (Phone *)createPhone{
HWPhone *hwPhone = [[HWPhone alloc] init];
NSLog(@"HWPhone has been created");
return hwPhone;
}
+ (Computer *)createComputer{
MateBookComputer *hwComputer = [[MateBookComputer alloc] init];
NSLog(@"HWComputer has been created");
return hwComputer;
}
@end
複製程式碼
以上就是工廠類的設計。這樣設計好之後,客戶端如果需要哪一產品族的某個產品的話,只需要找到對應產品族工廠後,呼叫生產該產品的介面即可。假如需要蘋果電腦,只需要委託蘋果工廠來製造蘋果電腦即可;如果需要小米手機,只需要委託小米工廠製造小米手機即可。
下面用程式碼來模擬一下這個場景:
//================== Using by client ==================
Store *store = [[Store alloc] init];
//Store wants to sell MacBook
Computer *macBook = [AppleFactory createComputer];
[macBook packaging];
[store sellComputer:macBook];
//Store wants to sell MIPhone
Phone *miPhone = [MIFactory createPhone];
[miPhone packaging];
[store sellPhone:miPhone];
//Store wants to sell MateBook
Computer *mateBook = [HWFactory createComputer];
[mateBook packaging];
[store sellComputer:mateBook];
複製程式碼
上面的程式碼就是模擬了商店售賣蘋果電腦,小米手機,華為電腦的場景。而今後如果該商店引入了新品牌的產品,比如聯想手機,聯想電腦,那麼我們只需要新增聯想手機類,聯想電腦類,聯想工廠類即可。
下面我們看一下該例子對應的 UML類圖,可以更直觀地看一下各個成員之間的關係:
程式碼對應的類圖
由於三個工廠的產品總數過多,因此在這裡只體現了蘋果工廠和小米工廠的產品。
優點
- 具體產品在應用層程式碼隔離,不需要關心產品細節。只需要知道自己需要的產品是屬於哪個工廠的即可 當一個產品族中的多個物件被設計成一起工作時,它能夠保證客戶端始終只使用同一個產品族中的物件。這對一些需要根據當前環境來決定其行為的軟體系統來說,是一種非常實用的設計模式。
缺點
- 規定了所有可能被建立的產品集合,產品族中擴充套件新的產品困難,需要修改抽象工廠的介面。
- 新增產品等級比較困難
- 產品等級固定,而產品族不固定,擴充套件性強的場景。
iOS SDK 和 JDK 中的應用
- 暫未發現iOS SDK中使用抽象工廠方法的例子,有知道的小夥伴歡迎留言。
- JDK中有一個資料庫連線的介面
Connection
。在這個介面裡面有createStatement()
和prepareStatement(String sql)
。這兩個介面都是獲取的統一產品族的物件,比如MySql和PostgreSQL產品族,具體返回的是哪個產品族物件,取決於所連線的資料庫型別。
OK,到現在三個工廠模式已經講完了。在繼續講解下面三個設計模式之前,先簡單回顧一下上面講解的三個工廠模式:
大體上看,簡單工廠模式,工廠方法模式和抽象工廠模式的複雜程度是逐漸升高的。
- 簡單工廠模式使用不同的入參來讓同一個工廠生產出不同的產品。
- 工廠方法模式和抽象工廠模式都需要有特定的工廠類來生產對應的產品;而工廠方法模式裡的工廠是面向同一產品等級的產品;而抽象工廠方法模式裡的工廠是面向同一產品族的產品的。
在實際開發過程中,我們需要根據業務場景的複雜程度的不同來採用最適合的工廠模式。
四. 單例模式
定義
單例模式(Singleton Pattern):單例模式確保某一個類只有一個例項,並提供一個訪問它的全劇訪問點。
適用場景
系統只需要一個例項物件,客戶呼叫類的單個例項只允許使用一個公共訪問點,除了該公共訪問點,不能通過其他途徑訪問該例項。比較典型的例子是音樂播放器,日誌系統類等等。
成員與類圖
成員
單例模式只有一個成員,就是單例類。因為只有一個成員,所以該設計模式的類圖比較簡單:
模式類圖
一般來說單例類會給外部提供一個獲取單例物件的方法,內部會用靜態物件的方式儲存這個物件。
程式碼示例
場景概述
在這裡我們建立一個簡單的列印日至或上報日至的日至管理單例。
場景分析
在建立單例時,除了要保證提供唯一例項物件以外,還需注意多執行緒的問題。下面用程式碼來看一下。
程式碼實現
建立單例類 LogManager
//================== LogManager.h ==================
@interface LogManager : NSObject
+(instancetype)sharedInstance;
- (void)printLog:(NSString *)logMessage;
- (void)uploadLog:(NSString *)logMessage;
@end
//================== LogManager.m ==================
@implementation LogManager
static LogManager* _sharedInstance = nil;
+(instancetype)sharedInstance
{
static dispatch_once_t onceToken ;
dispatch_once(&onceToken, ^{
_sharedInstance = [[super allocWithZone:NULL] init] ;
}) ;
return _sharedInstance ;
}
+(id)allocWithZone:(struct _NSZone *)zone
{
return [LogManager sharedInstance] ;
}
-(id)copyWithZone:(struct _NSZone *)zone
{
return [LogManager sharedInstance];
}
-(id)mutableCopyWithZone:(NSZone *)zone
{
return [LogManager sharedInstance];
}
- (void)printLog:(NSString *)logMessage{
//print logMessage
}
- (void)uploadLog:(NSString *)logMessage{
//upload logMessage
}
@end
複製程式碼
從上面的程式碼中可以看到:
sharedInstance
方法是向外部提供的獲取唯一的例項物件的方法,也是該類中的其他可以建立物件的方法的都呼叫的方法。在這個方法內部使用了dispatch_once
函式來避免多執行緒訪問導致建立多個例項的情況。- 為了在
alloc init
出初始化方法可以返回同一個例項物件,在allocWithZone:
方法裡面仍然呼叫了sharedInstance
方法。 - 而且為了在
copy
和mutableCopy
方法也可以返回同一個例項物件,在copyWithZone:
與mutableCopyWithZone
也是呼叫了sharedInstance
方法。
下面分別用這些介面來驗證一下例項的唯一性:
//================== Using by client ==================
//alloc&init
LogManager *manager0 = [[LogManager alloc] init];
//sharedInstance
LogManager *manager1 = [LogManager sharedInstance];
//copy
LogManager *manager2 = [manager0 copy];
//mutableCopy
LogManager *manager3 = [manager1 mutableCopy];
NSLog(@"\nalloc&init: %p\nsharedInstance: %p\ncopy: %p\nmutableCopy: %p",manager0,manager1,manager2,manager3);
複製程式碼
我們看一下列印出來的四個指標所指向物件的地址:
alloc&init: 0x60000000f7e0
sharedInstance: 0x60000000f7e0
copy: 0x60000000f7e0
mutableCopy: 0x60000000f7e0
複製程式碼
可以看出列印出來的地址都相同,說明都是同一物件,證明了實現方法的正確性。
下面我們看一下該例子對應的 UML類圖,可以更直觀地看一下各個成員之間的關係:
程式碼對應的類圖
優點
- 提供了對唯一例項的受控訪問。因為單例類封裝了它的唯一例項,所以它可以嚴格控制客戶怎樣以及何時訪問它。
- 因為該類在系統記憶體中只存在一個物件,所以可以節約系統資源。
缺點
- 由於單例模式中沒有抽象層,因此單例類很難進行擴充套件。
- 對於有垃圾回收系統的語言(Java,C#)來說,如果物件長時間不被利用,則可能會被回收。那麼如果這個單例持有一些資料的話,在回收後重新例項化時就不復存在了。
iOS SDK 和 JDK 中的應用
- 在Objective-C語言中使用單例模式的類有
NSUserDefaults
(key-value持久化)和UIApplication
類(代表應用程式,可以處理一些點選事件等)。 - 在JDK中使用的單例模式的類有
Runtime
類(代表應用程式的執行環境,使應用程式能夠與其執行的環境相連線);Desktop
類(允許 Java 應用程式啟動已在本機桌面上註冊的關聯應用程式)
五. 生成器模式
定義
生成器模式(Builder Pattern):也叫建立者模式,它將一個複雜物件的構建與它的表示分離,使得同樣的構建過程可以建立不同的表示。
具體點說就是:有些物件的建立流程是一樣的,但是因為自身特性的不同,所以在建立他們的時候需要將建立過程和特性的定製分離開來。
下面我們看一下該設計模式的適用場景。
適用場景
當建立複雜物件的演算法應該獨立於該物件的組成部分以及它們的裝配方式時比較適合使用生成器模式。
一些複雜的物件,它們擁有多個組成部分(如汽車,它包括車輪、方向盤、傳送機等各種部件)。而對於大多數使用者而言,無須知道這些部件的裝配細節,也幾乎不會使用單獨某個部件,而是使用一輛完整的汽車。而且這些部分的建立順序是固定的,或者是需要指定的。
在這種情況下可以通過建造者模式對其進行設計與描述,生成器模式可以將部件和其組裝過程分開,一步一步建立一個複雜的物件。
成員與類圖
成員
建造者模式包含4個成員:
- 抽象建造者(Builder):定義構造產品的幾個公共方法。
- 具體建造者(ConcreteBuilder):根據不同的需求來實現抽象建造者定義的公共方法;每一個具體建造者都包含一個產品物件作為它的成員變數。
- 指揮者(Director):根據傳入的具體建造者來返回其所對應的產品物件。
- 產品角色(Product):建立的產品。
下面通過類圖來看一下各個成員之間的關係:
模式類圖
需要注意的是:
- Builder類中的product成員變數的關鍵字為
protected
,目的是為了僅讓它和它的子類可以訪問該成員變數。- Director類中的
constructProductWithBuilder(Builder builder)
方法是通過傳入不同的builder來構造產品的。而且它的getProduct()
方法同時也封裝了Concrete Builder
類的getProduct()
方法,目的是為了讓客戶端直接從Director
拿到對應的產品(有些資料裡面的Director
類沒有封裝Concrete Builder
類的getProduct()
方法)。
程式碼示例
場景概述
模擬一個製造手機的場景:手機的組裝需要幾個固定的零件:CPU,RAM,螢幕,攝像頭,而且需要CPU -> RAM ->螢幕 -> 攝像頭的順序來製造。
場景分析
我們使用建造者設計模式來實現這個場景:首先不同的手機要匹配不同的builder;然後在Director
類裡面來定義製造順序。
程式碼實現
首先我們定義手機這個類,它有幾個屬性:
//================== Phone.h ==================
@interface Phone : NSObject
@property (nonatomic, copy) NSString *cpu;
@property (nonatomic, copy) NSString *capacity;
@property (nonatomic, copy) NSString *display;
@property (nonatomic, copy) NSString *camera;
@end
複製程式碼
然後我們建立抽象builder類:
//================== Builder.h ==================
#import "Phone.h"
@interface Builder : NSObject
{
@protected Phone *_phone;
}
- (void)createPhone;
- (void)buildCPU;
- (void)buildCapacity;
- (void)buildDisplay;
- (void)buildCamera;
- (Phone *)obtainPhone;
@end
複製程式碼
抽象builder類宣告瞭建立手機各個元件的介面,也提供了返回手機例項的物件。
接下來我們建立對應不同手機的具體生成者類:
IPhoneXR手機的builder:IPhoneXRBuilder
:
//================== IPhoneXRBuilder.h ==================
@interface IPhoneXRBuilder : Builder
@end
//================== IPhoneXRBuilder.m ==================
@implementation IPhoneXRBuilder
- (void)createPhone{
_phone = [[Phone alloc] init];
}
- (void)buildCPU{
[_phone setCpu:@"A12"];
}
- (void)buildCapacity{
[_phone setCapacity:@"256"];
}
- (void)buildDisplay{
[_phone setDisplay:@"6.1"];
}
- (void)buildCamera{
[_phone setCamera:@"12MP"];
}
- (Phone *)obtainPhone{
return _phone;
}
@end
複製程式碼
小米8手機的builder:MI8Builder
:
//================== MI8Builder.h ==================
@interface MI8Builder : Builder
@end
//================== MI8Builder.m ==================
@implementation MI8Builder
- (void)createPhone{
_phone = [[Phone alloc] init];
}
- (void)buildCPU{
[_phone setCpu:@"Snapdragon 845"];
}
- (void)buildCapacity{
[_phone setCapacity:@"128"];
}
- (void)buildDisplay{
[_phone setDisplay:@"6.21"];
}
- (void)buildCamera{
[_phone setCamera:@"12MP"];
}
- (Phone *)obtainPhone{
return _phone;
}
@end
複製程式碼
從上面兩個具體builder的程式碼可以看出,這兩個builder都按照其對應的手機配置來建立其對應的手機。
下面來看一下Director的用法:
//================== Director.h ==================
#import "Builder.h"
@interface Director : NSObject
- (void)constructPhoneWithBuilder:(Builder *)builder;
- (Phone *)obtainPhone;
@end
//================== Director.m ==================
implementation Director
{
Builder *_builder;
}
- (void)constructPhoneWithBuilder:(Builder *)builder{
_builder = builder;
[_builder buildCPU];
[_builder buildCapacity];
[_builder buildDisplay];
[_builder buildCamera];
}
- (Phone *)obtainPhone{
return [_builder obtainPhone];
}
@end
複製程式碼
Director類提供了
construct:
方法,需要傳入builder的例項。該方法裡面按照既定的順序來建立手機。
最後我們看一下客戶端是如何使用具體的Builder和Director例項的:
//================== Using by client ==================
//Get iPhoneXR
//1. A director instance
Director *director = [[Director alloc] init];
//2. A builder instance
IPhoneXRBuilder *iphoneXRBuilder = [[IPhoneXRBuilder alloc] init];
//3. Construct phone by director
[director construct:iphoneXRBuilder];
//4. Get phone by builder
Phone *iPhoneXR = [iphoneXRBuilder obtainPhone];
NSLog(@"Get new phone iPhoneXR of data: %@",iPhoneXR);
//Get MI8
MI8Builder *mi8Builder = [[MI8Builder alloc] init];
[director construct:mi8Builder];
Phone *mi8 = [mi8Builder obtainPhone];
NSLog(@"Get new phone MI8 of data: %@",mi8);
複製程式碼
從上面可以看出客戶端獲取具體產品的過程:
- 首先需要例項化一個Director的例項。
- 然後根據所需要的產品找出其對應的builder。
- 將builder傳入director例項的
construct:
方法。- 從builder的
obtainPhone
獲取手機例項。
下面我們看一下該例子對應的 UML類圖,可以更直觀地看一下各個成員之間的關係:
程式碼對應的類圖
優點
- 客戶端不必知道產品內部組成的細節,將產品本身與產品的建立過程解耦,使得相同的建立過程可以建立不同的產品物件。
- 每一個具體建造者都相對獨立,而與其他的具體建造者無關,因此可以很方便地替換具體建造者或增加新的具體建造者, 使用者使用不同的具體建造者即可得到不同的產品物件 。
- 增加新的具體建造者無須修改原有類庫的程式碼,指揮者類針對抽象建造者類程式設計,系統擴充套件方便,符合“開閉原則”。
- 可以更加精細地控制產品的建立過程 。將複雜產品的建立步驟分解在不同的方法中,使得建立過程更加清晰,也更方便使用程式來控制建立過程。
缺點
-
建造者模式所建立的產品一般具有較多的共同點,其組成部分相似,如果產品之間的差異性很大,則不適合使用建造者模式,因此其使用範圍受到一定的限制。
-
如果產品的內部變化複雜,可能會導致需要定義很多具體建造者類來實現這種變化,導致系統變得很龐大。
iOS SDK 和 JDK 中的應用
- 暫未發現iOS SDK中使用生成器設計模式的例子,有知道的小夥伴歡迎留言。
- JDK中的
StringBuilder
屬於builder,它向外部提供append(String)
方法來拼接字串(也可以傳入int等其他型別);而toString()
方法來返回字串。
六. 原型模式
定義
原型模式(Prototype Pattern): 使用原型例項指定待建立物件的型別,並且通過複製這個原型來建立新的物件。
適用場景
-
物件層級巢狀比較多,從零到一建立物件的過程比較繁瑣時,可以直接通過複製的方式建立新的物件
-
當一個類的例項只能有幾個不同狀態組合中的一種時,我們可以利用已有的物件進行復制來獲得
成員與類圖
成員
原型模式主要包含如下兩個角色:
- 抽象原型類(Prototype):抽象原型類宣告克隆自身的介面。
- 具體原型類(ConcretePrototype):具體原型類實現克隆的具體操作(克隆資料,狀態等)。
下面通過類圖來看一下各個成員之間的關係:
模式類圖
需要注意的是,這裡面的clone()
方法返回的是被複製出來的例項物件。
程式碼示例
場景概述
模擬一份校招的簡歷,簡歷裡面有人名,性別,年齡以及學歷相關的資訊。這裡面學歷相關的資訊又包含學校名稱,專業,開始和截止年限的資訊。
場景分析
這裡的學歷相關資訊可以使用單獨一個物件來做,因此整體的簡歷物件的結構可以是:
簡歷物件:
- 人名
- 性別
- 年齡
- 學歷物件
- 學校名稱
- 專業
- 開始年份
- 結束年份
而且因為對於同一學校同一屆的同一專業的畢業生來說,學歷物件中的資訊是相同的,這時候如果需要大量生成這些畢業生的簡歷的話比較適合使用原型模式。
程式碼實現
首先定義學歷物件:
//================== UniversityInfo.h ==================
@interface UniversityInfo : NSObject<NSCopying>
@property (nonatomic, copy) NSString *universityName;
@property (nonatomic, copy) NSString *startYear;
@property (nonatomic, copy) NSString *endYear;
@property (nonatomic, copy) NSString *major;
- (id)copyWithZone:(NSZone *)zone;
@end
//================== UniversityInfo.m ==================
@implementation UniversityInfo
- (id)copyWithZone:(NSZone *)zone
{
UniversityInfo *infoCopy = [[[self class] allocWithZone:zone] init];
[infoCopy setUniversityName:[_universityName mutableCopy]];
[infoCopy setStartYear:[_startYear mutableCopy]];
[infoCopy setEndYear:[_endYear mutableCopy]];
[infoCopy setMajor:[_major mutableCopy]];
return infoCopy;
}
@end
複製程式碼
因為學歷物件是支援複製的,因此需要遵從
<NSCopying>
協議並實現copyWithZone:
方法。而且支援的是深複製,所以在複製NSString的過程中需要使用mutableCopy
來實現。
接著我們看一下簡歷物件:
//================== Resume.h ==================
#import "UniversityInfo.h"
@interface Resume : NSObject<NSCopying>
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *gender;
@property (nonatomic, copy) NSString *age;
@property (nonatomic, strong) UniversityInfo *universityInfo;
@end
//================== Resume.m ==================
@implementation Resume
- (id)copyWithZone:(NSZone *)zone
{
Resume *resumeCopy = [[[self class] allocWithZone:zone] init];
[resumeCopy setName:[_name mutableCopy]];
[resumeCopy setGender:[_gender mutableCopy]];
[resumeCopy setAge:[_age mutableCopy]];
[resumeCopy setUniversityInfo:[_universityInfo copy]];
return resumeCopy;
}
@end
複製程式碼
同樣地,簡歷物件也需要遵從
<NSCopying>
協議並實現copyWithZone:
方法。
最後我們看一下複製的效果有沒有達到我們的預期(被複制物件和複製物件的地址和它們所有的屬性物件的地址都不相同)
//================== Using by client ==================
//resume for LiLei
Resume *resume = [[Resume alloc] init];
resume.name = @"LiLei";
resume.gender = @"male";
resume.age = @"24";
UniversityInfo *info = [[UniversityInfo alloc] init];
info.universityName = @"X";
info.startYear = @"2014";
info.endYear = @"2018";
info.major = @"CS";
resume.universityInfo = info;
//resume_copy for HanMeiMei
Resume *resume_copy = [resume copy];
NSLog(@"\n\n\n======== original resume ======== %@\n\n\n======== copy resume ======== %@",resume,resume_copy);
resume_copy.name = @"HanMeiMei";
resume_copy.gender = @"female";
resume_copy.universityInfo.major = @"TeleCommunication";
NSLog(@"\n\n\n======== original resume ======== %@\n\n\n======== revised copy resume ======== %@",resume,resume_copy);
複製程式碼
上面的程式碼模擬了這樣一個場景:李雷同學寫了一份自己的簡歷,然後韓梅梅複製了一份並修改了姓名,性別和專業這三個和李雷不同的資訊。
這裡我們重寫了Resume
的description
方法來看一下所有屬性的值及其記憶體地址。最後來看一下resume物件和resume_copy物件列印的結果:
//================== Output log ==================
======== original resume ========
resume object address:0x604000247d10
name:LiLei | 0x10bc0c0b0
gender:male | 0x10bc0c0d0
age:24 | 0x10bc0c0f0
university name:X| 0x10bc0c110
university start year:2014 | 0x10bc0c130
university end year:2018 | 0x10bc0c150
university major:CS | 0x10bc0c170
======== copy resume ========
resume object address:0x604000247da0
name:LiLei | 0xa000069654c694c5
gender:male | 0xa000000656c616d4
age:24 | 0xa000000000034322
university name:X| 0xa000000000000581
university start year:2014 | 0xa000000343130324
university end year:2018 | 0xa000000383130324
university major:CS | 0xa000000000053432
======== original resume ========
resume object address:0x604000247d10
name:LiLei | 0x10bc0c0b0
gender:male | 0x10bc0c0d0
age:24 | 0x10bc0c0f0
university name:X| 0x10bc0c110
university start year:2014 | 0x10bc0c130
university end year:2018 | 0x10bc0c150
university major:CS | 0x10bc0c170
======== revised copy resume ========
resume object address:0x604000247da0
name:HanMeiMei | 0x10bc0c1b0
gender:female | 0x10bc0c1d0
age:24 | 0xa000000000034322
university name:X| 0xa000000000000581
university start year:2014 | 0xa000000343130324
university end year:2018 | 0xa000000383130324
university major:TeleCommunication | 0x10bc0c1f0
複製程式碼
- 上面兩個是原resume和剛被複制後的 copy resume的資訊,可以看出來無論是這兩個物件的地址還是它們的值對應的地址都是不同的,說明成功地實現了深複製。
- 下面兩個是原resume和被修改後的 copy_resume的資訊,可以看出來新的copy_resume的值發生了變化,而且值所對應的地址還是和原resume的不同。
注:還可以用序列化和反序列化的辦法來實現深複製,因為與程式碼設計上不是很複雜,很多語言直接提供了介面,故這裡不做介紹。
下面我們看一下該例子對應的 UML類圖,可以更直觀地看一下各個成員之間的關係:
程式碼對應的類圖
在這裡需要注意的是:
copy
方法是NSObject
類提供的複製本物件的介面。NSObject
類似於Java中的Object
類,在Objective-C中幾乎所有的物件都繼承與它。而且這個copy
方法也類似於Object
類的clone()
方法。copyWithZone(NSZone zone)
方法是介面NSCopying
提供的介面。而因為這個介面存在於實現檔案而不是標頭檔案,所以它不是對外公開的;即是說外部無法直接呼叫copyWithZone(NSZone zone)
方法。copyWithZone(NSZone zone)
方法是在上面所說的copy
方法呼叫後再呼叫的,作用是將物件的所有資料都進行復制。因此使用者需要在copyWithZone(NSZone zone)
方法裡做工作,而不是copy
方法,這一點和Java的clone
方法不同。
優點
- 可以利用原型模式簡化物件的建立過程,尤其是對一些建立過程繁瑣,包含物件層級比較多的物件來說,使用原型模式可以節約系統資源,提高物件生成的效率。
- 可以很方便得通過改變值來生成新的物件:有些物件之間的差別可能只在於某些值的不同;用原型模式可以快速複製出新的物件並手動修改值即可。
缺點
- 物件包含的所有物件都需要配備一個克隆的方法,這就使得在物件層級比較多的情況下,程式碼量會很大,也更加複雜。
iOS SDK 和 JDK 中的應用
- Objective-C中可以使用
<NSCopying>
協議,配合- (id)copyWithZone:(NSZone *)zone
方法; 或者<NSMutableCopying>
協議,配合copyWithZone:/mutableCopyWithZone:
方法 - Java中可以讓一個類實現
Cloneable
介面並實現clone()
方法來複制該類的例項。
到這裡設計模式中的建立型模式就介紹完了,讀者可以結合UML類圖和demo的程式碼來理解每個設計模式的特點和相互之間的區別,希望讀者可以有所收穫。
另外,本篇部落格的程式碼和類圖都儲存在我的GitHub庫中:knightsj:object-oriented-design中的Chapter2。
下一篇是物件導向系列的第三篇,講解的是物件導向設計模式中的結構型模式。 該系列的第一篇講解的是設計原則,有興趣的讀者可以移步:物件導向設計的六大設計原則(附 Demo 及 UML 類圖)
參考書籍和教程
本篇已同步到個人部落格:物件導向設計的設計模式(一):建立型模式(附 Demo 及 UML 類圖)
筆者在近期開通了個人公眾號,主要分享程式設計,讀書筆記,思考類的文章。
- 程式設計類文章:包括筆者以前釋出的精選技術文章,以及後續釋出的技術文章(以原創為主),並且逐漸脫離 iOS 的內容,將側重點會轉移到提高程式設計能力的方向上。
- 讀書筆記類文章:分享程式設計類,思考類,心理類,職場類書籍的讀書筆記。
- 思考類文章:分享筆者平時在技術上,生活上的思考。
因為公眾號每天釋出的訊息數有限制,所以到目前為止還沒有將所有過去的精選文章都發布在公眾號上,後續會逐步釋出的。
而且因為各大部落格平臺的各種限制,後面還會在公眾號上釋出一些短小精幹,以小見大的乾貨文章哦~
掃下方的公眾號二維碼並點選關注,期待與您的共同成長~