iOS开发 - 面向对象设计的设计模式(一):创建型模式(附 Demo UML类图)
繼上一篇的面向對象設計的設計原則,本篇是面向對象設計系列的第二個部分:面向對象設計的設計模式的第一篇文章。
另外,本篇博客的代碼和類圖都保存在我的GitHub庫中:中的Chapter2。
最開始說一下什么是設計模式。關于設計模式的概念,有很多不同的版本,在這里說一下我個人比較贊同的一個說法:
設計模式用于在特定的條件下為一些重復出現的軟件設計問題提供合理的、有效的解決方案。
去掉一些定語的修飾,這句話精簡為:
設計模式為問題提供方案。
簡單來看,設計模式其實就是針對某些問題的一些方案。在軟件開發(fā)中,即使很多人在用不同的語言去開發(fā)不同的業(yè)務,但是很多時候這些人遇到的問題抽象出來都是相似的。一些卓越的開發(fā)者將一些常出現的問題和對應的解決方案匯總起來,總結出了這些設計模式。
因此掌握了這些設計模式,可以讓我們更好地去解決開發(fā)過程中遇到的一些常見問題。而且對這些問題的解決方案的掌握程度越好,我們就越能夠打破語言本身的限制去解決問題,也就是增強“軟件開發(fā)的內功”。
介紹設計模式最著名的一本書莫屬《設計模式 可復用面向對象軟件的基礎》這本書,書中共介紹了23個設計模式。而這些設計模式分為三大類:
- 創(chuàng)建型設計模式:側重于對象的創(chuàng)建。
- 結構型設計模式:側重于接口的設計和系統(tǒng)的結構。
- 行為型設計模式:側重于類或對象的行為。
而本篇作為該系列的第一篇,講解的是設計模式中的6個創(chuàng)建型設計模式:
注意:簡單工廠模式不是 GoF總結出來的23種設計模式之一,不存在于《設計模式 可復用面向對象軟件的基礎》這本書中。
在面向對象設計中,類與對象幾乎是構成所有系統(tǒng)的基本元素,因此我認為學好了創(chuàng)建型模式才是學會設計系統(tǒng)的第一步:因為你應該知道如何去創(chuàng)建一些特定性質的對象,這才是設計好的系統(tǒng)的開始。
在講解這6個設計模式之前先說一下該系列文章的講解方式:
從更多維度來理解一件事物有助于更深刻地理解它,因此每個設計模式我都會從以下這幾點來講解:
- 定義
- 使用場景
- 成員與類圖
- 代碼示例
- 優(yōu)點
- 缺點
- iOS SDK 和 JDK 中的應用
最后一項:“iOS SDK 和 JDK中的應用”講解的是該設計模式在Objective-C和java語言(JDK)中的應用。
首先我們看一下簡單工廠模式:
一. 簡單工廠模式
定義
簡單工廠模式(Simple Factory Pattern):專門定義一個類(工廠類)來負責創(chuàng)建其他類的實例。可以根據創(chuàng)建方法的參數來返回不同類的實例,被創(chuàng)建的實例通常都具有共同的父類。
簡單工廠模式又稱為靜態(tài)工廠方法(Static Factory Method)模式,它屬于類創(chuàng)建型模式。
適用場景
如果我們希望將一些為數不多的類似的對象的創(chuàng)建和他們的創(chuàng)建細節(jié)分離開,也不需要知道對象的具體類型,可以使用簡單工廠模式。
舉個形象點的例子:在前端開發(fā)中,常常會使用外觀各式各樣的按鈕:比如有的按鈕有圓角,有的按鈕有陰影,有的按鈕有邊框,有的按鈕無邊框等等。但是因為同一種樣式的按鈕可以出現在項目的很多地方,所以如果在每個地方都把創(chuàng)建按鈕的邏輯寫一遍的話顯然是會造成代碼的重復(而且由于業(yè)務的原因有的按鈕的創(chuàng)建邏輯能比較復雜,代碼量大)。
那么為了避免重復代碼的產生,我們可以將這些創(chuàng)建按鈕的邏輯都放在一個“工廠”里面,讓這個工廠來根據你的需求(傳入的參數)來創(chuàng)建對應的按鈕并返回給你。這樣一來,同樣類型的按鈕在多個地方使用的時候,就可以只給這個工廠傳入其對應的參數并拿到返回的按鈕即可。
下面來看一下簡單工廠模式的成員和類圖。
成員與類圖
成員
簡單工廠模式的結構比較簡單,一共只有三個成員:
- 工廠(Factory):工廠負責實現創(chuàng)建所有產品實例的邏輯
- 抽象產品(Product):抽象產品是工廠所創(chuàng)建的所有產品對象的父類,負責聲明所有產品實例所共有的公共接口。
- 具體產品(Concrete Product):具體產品是工廠所創(chuàng)建的所有產品對象類,它以自己的方式來實現其共同父類聲明的接口。
下面通過類圖來看一下各個成員之間的關系:
模式類圖
從類圖中可以看出,工廠類提供一個靜態(tài)方法:通過傳入的字符串來制造其所對應的產品。
代碼示例
場景概述
舉一個店鋪售賣不同品牌手機的例子:店鋪,即客戶端類向手機工廠購進手機售賣。
場景分析
該場景可以使用簡單工廠的角色來設計:
- 抽象產品:Phone,是所有具體產品類的父類,提供一個公共接口packaging表示手機的裝箱并送到店鋪。
- 具體產品:不同品牌的手機,iPhone手機類(IPhone),小米手機類(MIPhone),華為手機類(HWPhone)。
- 工廠:PhoneFactory根據不同的參數來創(chuàng)建不同的手機。
- 客戶端類:店鋪類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:
工廠類向外部(客戶端)提供了一個創(chuàng)造手機的接口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];上面代碼的解讀:
在這里我們需要注意的是:商店從工廠拿到手機不需要了解手機制作的過程,只需要知道它要工廠做的是手機(只知道Phone類即可),和需要給工廠類傳入它所需手機所對應的參數即可(這里的iPhone手機對應的參數就是i)。
下面我們看一下該例子對應的 UML類圖,可以更直觀地看一下各個成員之間的關系:
代碼對應的類圖
優(yōu)點
- 客戶端只需要給工廠類傳入一個正確的(約定好的)參數,就可以獲取你所需要的對象,而不需要知道其創(chuàng)建細節(jié),一定程度上減少系統(tǒng)的耦合。
- 客戶端無須知道所創(chuàng)建的具體產品類的類名,只需要知道具體產品類所對應的參數即可,減少開發(fā)者的記憶成本。
缺點
- 如果業(yè)務上添加新產品的話,就需要修改工廠類原有的判斷邏輯,這其實是違背了開閉原則的。
- 在產品類型較多時,有可能造成工廠邏輯過于復雜。所以簡單工廠模式比較適合產品種類比較少而且增多的概率很低的情況。
iOS SDK 和 JDK 中的應用
- Objective-C中的類簇就是簡單工廠設計模式的一個應用。如果給NSNumber的工廠方法傳入不同類型的數據,則會返回不同數據所對應的NSNumber的子類。
- JDK中的Calendar類中的私有的createCalendar(TimeZone zone, Locale aLocale)方法通過不同的入參來返回不同類型的Calendar子類的實例。
二. 工廠方法模式
定義
工廠方法模式(Factory Method Pattern)又稱為工廠模式,工廠父類負責定義創(chuàng)建產品對象的公共接口,而工廠子類則負責生成具體的產品對象,即通過不同的工廠子類來創(chuàng)建不同的產品對象。
適用場景
工廠方法模式的適用場景與簡單工廠類似,都是創(chuàng)建數據和行為比較類似的對象。但是和簡單工廠不同的是:在工廠方法模式中,因為創(chuàng)建對象的責任移交給了抽象工廠的子類,因此客戶端需要知道其所需產品所對應的工廠子類,而不是簡單工廠中的參數。
下面我們看一下工廠方法模式的成員和類圖。
成員與類圖
成員
工廠方法模式包含四個成員:
下面通過類圖來看一下各個成員之間的關系:
模式類圖
從類圖中我們可以看到:抽象工廠負責定義具體工廠必須實現的接口,而創(chuàng)建產品對象的任務則交給具體工廠,由特定的子工廠來創(chuàng)建其對應的產品。
這使得工廠方法模式可以允許系統(tǒng)在不修改原有工廠的情況下引進新產品:只需要創(chuàng)建新產品類和其所對應的工廠類即可。
代碼示例
場景概述
同樣也是模擬上面的簡單工廠例子中的場景(手機商店賣手機),但是由于這次是由工廠方法模式來實現的,因此在代碼設計上會有變化。
場景分析
與簡單工廠模式不同的是:簡單工廠模式里面只有一個工廠,而工廠方法模式里面有一個抽象工廠和繼承于它的具體工廠。
因此同樣的三個品牌的手機,我們可以通過三個不同的具體工廠:蘋果手機工廠(IPhoneFactory),小米手機工廠 (MIPhoneFactory),華為手機工廠(HWPhoneFactory)來生產。而這些具體工廠類都會繼承于抽象手機工廠類:PhoneFactory,它來聲明生產手機的接口。
下面我們用代碼來具體來看一下工廠類(抽象工廠和具體工廠)的設計:
代碼實現
首先我們聲明一個抽象工廠類?PhoneFactory:
//================== PhoneFactory.h ================== #import "Phone.h"@interface PhoneFactory : NSObject+ (Phone *)createPhone;@end//================== PhoneFactory.m ================== @implementation PhoneFactory+ (Phone *)createPhone{//implemented by subclassreturn 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類圖,可以更直觀地看一下各個成員之間的關系:
代碼對應的類圖
優(yōu)點
- 用戶只需要關心其所需產品對應的具體工廠是哪一個即可,不需要關心產品的創(chuàng)建細節(jié),也不需要知道具體產品類的類名。
- 當系統(tǒng)中加入新產品時,不需要修改抽象工廠和抽象產品提供的接口,也無須修改客戶端和其他的具體工廠和具體產品,而只要添加一個具體工廠和與其對應的具體產品就可以了,符合了開閉原則(這一點與簡單工廠模式不同)。
缺點
- 當系統(tǒng)中加入新產品時,除了需要提供新的產品類之外,還要提供與其對應的具體工廠類。因此系統(tǒng)中類的個數將成對增加,增加了系統(tǒng)的復雜度。
iOS SDK 和 JDK 中的應用
- 暫未發(fā)現iOS SDK中使用工廠方法的例子,有知道的小伙伴歡迎留言。
- 在JDK中,Collection接口聲明了iterator()方法,該方法返回結果的抽象類是Iterator。ArrayList就實現了這個接口;,而ArrayList對應的具體產品是Itr。
三. 抽象工廠模式
定義
抽象工廠模式(Abstract Factory Pattern):提供一個創(chuàng)建一系列相關或相互依賴對象的接口,而無須指定它們具體的類。
適用場景
有時候我們需要一個工廠可以提供多個產品對象,而不是單一的產品對象。比如系統(tǒng)中有多于一個的產品族,而每次只使用其中某一產品族,屬于同一個產品族的產品將在一起使用。
在這里說一下產品族和產品等級結構的概念:
- 產品族:同一工廠生產的不同產品
- 產品等級結構:同一類型產品的不同實現
用一張圖來幫助理解:
在上圖中:
- 縱向的,不同形狀,相同色系的圖形屬于同一產品組的產品,而同一產品族的產品對應的是同一個工廠;
- 橫向的,同一形狀,不同色系的圖形屬于統(tǒng)一產品等級結構的產品,而統(tǒng)一產品等級結構的產品對應的是同一個工廠方法。
下面再舉一個例子幫助大家理解:
我們將小米,華為,蘋果公司比作抽象工廠方法里的工廠:這三個工廠都有自己生產的手機,平板和電腦。
那么小米手機,小米平板,小米電腦就屬于小米這個工廠的產品族;同樣適用于華為工廠和蘋果工廠。
而小米手機,華為手機,蘋果手機則屬于同一產品等級結構:手機的產品等級結構;平板和電腦也是如此。
結合這個例子對上面的圖做一個修改可以更形象地理解抽象工廠方法的設計:
成員與類圖
成員
抽象工廠模式的成員和工廠方法模式的成員是一樣的,只不過抽象工廠方法里的工廠是面向產品族的。
下面通過類圖來看一下各個成員之間的關系:
模式類圖
- 抽象工廠模式與工廠方法模式最大的區(qū)別在于,工廠方法模式針對的是一個產品等級結構,而抽象工廠模式則需要面對多個產品等級結構
- 增加新的具體工廠和產品族很方便,無須修改已有系統(tǒng),符合“開閉原則”。
代碼示例
場景概述
由于抽象工廠方法里的工廠是面向產品族的,所以為了貼合抽象工廠方法的特點,我們將上面的場景做一下調整:在上面兩個例子中,商店只賣手機。在這個例子中我們讓商店也賣電腦:分別是蘋果電腦,小米電腦,華為電腦。
場景分析
如果我們還是套用上面介紹過的工廠方法模式來實現該場景的話,則需要創(chuàng)建三個電腦產品對應的工廠:蘋果電腦工廠,小米電腦工廠,華為電腦工廠。這就導致類的個數直線上升,以后如果還增加其他的產品,還需要添加其對應的工廠類,這顯然是不夠優(yōu)雅的。
仔細看一下這六個產品的特點,我們可以把這它們劃分在三個產品族里面:
而抽象方法恰恰是面向產品族設計的,因此該場景適合使用的是抽象工廠方法。下面結合代碼來看一下該如何設計。
代碼實現
首先引入電腦的基類和各個品牌的電腦類:
電腦基類:
//================== 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引入電腦相關產品類以后,我們需要重新設計工廠類。因為抽象工廠方法模式的工廠是面向產品族的,所以抽象工廠方法模式里的工廠所創(chuàng)建的是同一產品族的產品。下面我們看一下抽象工廠方法模式的工廠該如何設計:
首先創(chuàng)建所有工廠都需要集成的抽象工廠,它聲明了生產同一產品族的所有產品的接口:
//================== Factory.h ================== #import "Phone.h" #import "Computer.h"@interface Factory : NSObject+ (Phone *)createPhone;+ (Computer *)createComputer;@end//================== Factory.m ================== @implementation Factory+ (Phone *)createPhone{//implemented by subclassreturn nil; }+ (Computer *)createComputer{//implemented by subclassreturn nil; }@end接著,根據不同的產品族,我們創(chuàng)建不同的具體工廠:
首先是蘋果產品族工廠?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];上面的代碼就是模擬了商店售賣蘋果電腦,小米手機,華為電腦的場景。而今后如果該商店引入了新品牌的產品,比如聯(lián)想手機,聯(lián)想電腦,那么我們只需要新增聯(lián)想手機類,聯(lián)想電腦類,聯(lián)想工廠類即可。
下面我們看一下該例子對應的 UML類圖,可以更直觀地看一下各個成員之間的關系:
代碼對應的類圖
由于三個工廠的產品總數過多,因此在這里只體現了蘋果工廠和小米工廠的產品。
優(yōu)點
- 具體產品在應用層代碼隔離,不需要關心產品細節(jié)。只需要知道自己需要的產品是屬于哪個工廠的即可
當一個產品族中的多個對象被設計成一起工作時,它能夠保證客戶端始終只使用同一個產品族中的對象。這對一些需要根據當前環(huán)境來決定其行為的軟件系統(tǒng)來說,是一種非常實用的設計模式。
缺點
- 規(guī)定了所有可能被創(chuàng)建的產品集合,產品族中擴展新的產品困難,需要修改抽象工廠的接口。
- 新增產品等級比較困難
- 產品等級固定,而產品族不固定,擴展性強的場景。
iOS SDK 和 JDK 中的應用
- 暫未發(fā)現iOS SDK中使用抽象工廠方法的例子,有知道的小伙伴歡迎留言。
- JDK中有一個數據庫連接的接口Connection。在這個接口里面有createStatement()和prepareStatement(String sql)。這兩個接口都是獲取的統(tǒng)一產品族的對象,比如MySql和PostgreSQL產品族,具體返回的是哪個產品族對象,取決于所連接的數據庫類型。
OK,到現在三個工廠模式已經講完了。在繼續(xù)講解下面三個設計模式之前,先簡單回顧一下上面講解的三個工廠模式:
大體上看,簡單工廠模式,工廠方法模式和抽象工廠模式的復雜程度是逐漸升高的。
- 簡單工廠模式使用不同的入參來讓同一個工廠生產出不同的產品。
- 工廠方法模式和抽象工廠模式都需要有特定的工廠類來生產對應的產品;而工廠方法模式里的工廠是面向同一產品等級的產品;而抽象工廠方法模式里的工廠是面向同一產品族的產品的。
在實際開發(fā)過程中,我們需要根據業(yè)務場景的復雜程度的不同來采用最適合的工廠模式。
四. 單例模式
定義
單例模式(Singleton Pattern):單例模式確保某一個類只有一個實例,并提供一個訪問它的全劇訪問點。
適用場景
系統(tǒng)只需要一個實例對象,客戶調用類的單個實例只允許使用一個公共訪問點,除了該公共訪問點,不能通過其他途徑訪問該實例。比較典型的例子是音樂播放器,日志系統(tǒng)類等等。
成員與類圖
成員
單例模式只有一個成員,就是單例類。因為只有一個成員,所以該設計模式的類圖比較簡單:
模式類圖
一般來說單例類會給外部提供一個獲取單例對象的方法,內部會用靜態(tài)對象的方式保存這個對象。
代碼示例
場景概述
在這里我們創(chuàng)建一個簡單的打印日至或上報日至的日至管理單例。
場景分析
在創(chuàng)建單例時,除了要保證提供唯一實例對象以外,還需注意多線程的問題。下面用代碼來看一下。
代碼實現
創(chuàng)建單例類?LogManager
//================== LogManager.h ================== @interface LogManager : NSObject+(instancetype)sharedInstance;- (void)printLog:(NSString *)logMessage;- (void)uploadLog:(NSString *)logMessage;@end//================== LogManager.m ================== @implementation LogManagerstatic 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方法是向外部提供的獲取唯一的實例對象的方法,也是該類中的其他可以創(chuàng)建對象的方法的都調用的方法。在這個方法內部使用了dispatch_once函數來避免多線程訪問導致創(chuàng)建多個實例的情況。
- 為了在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類圖,可以更直觀地看一下各個成員之間的關系:
代碼對應的類圖
優(yōu)點
- 提供了對唯一實例的受控訪問。因為單例類封裝了它的唯一實例,所以它可以嚴格控制客戶怎樣以及何時訪問它。
- 因為該類在系統(tǒng)內存中只存在一個對象,所以可以節(jié)約系統(tǒng)資源。
缺點
- 由于單例模式中沒有抽象層,因此單例類很難進行擴展。
- 對于有垃圾回收系統(tǒng)的語言(Java,C#)來說,如果對象長時間不被利用,則可能會被回收。那么如果這個單例持有一些數據的話,在回收后重新實例化時就不復存在了。
iOS SDK 和 JDK 中的應用
- 在Objective-C語言中使用單例模式的類有NSUserDefaults(key-value持久化)和UIApplication類(代表應用程序,可以處理一些點擊事件等)。
- 在JDK中使用的單例模式的類有Runtime類(代表應用程序的運行環(huán)境,使應用程序能夠與其運行的環(huán)境相連接);Desktop類(允許 Java 應用程序啟動已在本機桌面上注冊的關聯(lián)應用程序)
五. 生成器模式
定義
生成器模式(Builder Pattern):也叫創(chuàng)建者模式,它將一個復雜對象的構建與它的表示分離,使得同樣的構建過程可以創(chuàng)建不同的表示。
具體點說就是:有些對象的創(chuàng)建流程是一樣的,但是因為自身特性的不同,所以在創(chuàng)建他們的時候需要將創(chuàng)建過程和特性的定制分離開來。
下面我們看一下該設計模式的適用場景。
適用場景
當創(chuàng)建復雜對象的算法應該獨立于該對象的組成部分以及它們的裝配方式時比較適合使用生成器模式。
一些復雜的對象,它們擁有多個組成部分(如汽車,它包括車輪、方向盤、發(fā)送機等各種部件)。而對于大多數用戶而言,無須知道這些部件的裝配細節(jié),也幾乎不會使用單獨某個部件,而是使用一輛完整的汽車。而且這些部分的創(chuàng)建順序是固定的,或者是需要指定的。
在這種情況下可以通過建造者模式對其進行設計與描述,生成器模式可以將部件和其組裝過程分開,一步一步創(chuàng)建一個復雜的對象。
成員與類圖
成員
建造者模式包含4個成員:
下面通過類圖來看一下各個成員之間的關系:
模式類圖
需要注意的是:
- 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然后我們創(chuàng)建抽象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類聲明了創(chuàng)建手機各個組件的接口,也提供了返回手機實例的對象。
接下來我們創(chuàng)建對應不同手機的具體生成者類:
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都按照其對應的手機配置來創(chuàng)建其對應的手機。
下面來看一下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]; }@endDirector類提供了construct:方法,需要傳入builder的實例。該方法里面按照既定的順序來創(chuàng)建手機。
最后我們看一下客戶端是如何使用具體的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);從上面可以看出客戶端獲取具體產品的過程:
下面我們看一下該例子對應的 UML類圖,可以更直觀地看一下各個成員之間的關系:
代碼對應的類圖
優(yōu)點
- 客戶端不必知道產品內部組成的細節(jié),將產品本身與產品的創(chuàng)建過程解耦,使得相同的創(chuàng)建過程可以創(chuàng)建不同的產品對象。
- 每一個具體建造者都相對獨立,而與其他的具體建造者無關,因此可以很方便地替換具體建造者或增加新的具體建造者, 用戶使用不同的具體建造者即可得到不同的產品對象 。
- 增加新的具體建造者無須修改原有類庫的代碼,指揮者類針對抽象建造者類編程,系統(tǒng)擴展方便,符合“開閉原則”。
- 可以更加精細地控制產品的創(chuàng)建過程 。將復雜產品的創(chuàng)建步驟分解在不同的方法中,使得創(chuàng)建過程更加清晰,也更方便使用程序來控制創(chuàng)建過程。
缺點
-
建造者模式所創(chuàng)建的產品一般具有較多的共同點,其組成部分相似,如果產品之間的差異性很大,則不適合使用建造者模式,因此其使用范圍受到一定的限制。
-
如果產品的內部變化復雜,可能會導致需要定義很多具體建造者類來實現這種變化,導致系統(tǒng)變得很龐大。
iOS SDK 和 JDK 中的應用
- 暫未發(fā)現iOS SDK中使用生成器設計模式的例子,有知道的小伙伴歡迎留言。
- JDK中的StringBuilder屬于builder,它向外部提供append(String)方法來拼接字符串(也可以傳入int等其他類型);而toString()方法來返回字符串。
六. 原型模式
定義
原型模式(Prototype Pattern): 使用原型實例指定待創(chuàng)建對象的類型,并且通過復制這個原型來創(chuàng)建新的對象。
適用場景
-
對象層級嵌套比較多,從零到一創(chuàng)建對象的過程比較繁瑣時,可以直接通過復制的方式創(chuàng)建新的對象
-
當一個類的實例只能有幾個不同狀態(tài)組合中的一種時,我們可以利用已有的對象進行復制來獲得
成員與類圖
成員
原型模式主要包含如下兩個角色:
下面通過類圖來看一下各個成員之間的關系:
模式類圖
需要注意的是,這里面的clone()方法返回的是被復制出來的實例對象。
代碼示例
場景概述
模擬一份校招的簡歷,簡歷里面有人名,性別,年齡以及學歷相關的信息。這里面學歷相關的信息又包含學校名稱,專業(yè),開始和截止年限的信息。
場景分析
這里的學歷相關信息可以使用單獨一個對象來做,因此整體的簡歷對象的結構可以是:
簡歷對象:
- 人名
- 性別
- 年齡
- 學歷對象
- 學校名稱
- 專業(yè)
- 開始年份
- 結束年份
而且因為對于同一學校同一屆的同一專業(yè)的畢業(yè)生來說,學歷對象中的信息是相同的,這時候如果需要大量生成這些畢業(yè)生的簡歷的話比較適合使用原型模式。
代碼實現
首先定義學歷對象:
//================== 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>協(xié)議并實現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>協(xié)議并實現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);上面的代碼模擬了這樣一個場景:李雷同學寫了一份自己的簡歷,然后韓梅梅復制了一份并修改了姓名,性別和專業(yè)這三個和李雷不同的信息。
這里我們重寫了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的值發(fā)生了變化,而且值所對應的地址還是和原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方法不同。
優(yōu)點
- 可以利用原型模式簡化對象的創(chuàng)建過程,尤其是對一些創(chuàng)建過程繁瑣,包含對象層級比較多的對象來說,使用原型模式可以節(jié)約系統(tǒng)資源,提高對象生成的效率。
- 可以很方便得通過改變值來生成新的對象:有些對象之間的差別可能只在于某些值的不同;用原型模式可以快速復制出新的對象并手動修改值即可。
缺點
- 對象包含的所有對象都需要配備一個克隆的方法,這就使得在對象層級比較多的情況下,代碼量會很大,也更加復雜。
iOS SDK 和 JDK 中的應用
- Objective-C中可以使用<NSCopying>?協(xié)議,配合- (id)copyWithZone:(NSZone *)zone方法; 或者<NSMutableCopying>協(xié)議,配合?copyWithZone:/mutableCopyWithZone:方法
- Java中可以讓一個類實現Cloneable接口并實現clone()方法來復制該類的實例。
到這里設計模式中的創(chuàng)建型模式就介紹完了,讀者可以結合UML類圖和demo的代碼來理解每個設計模式的特點和相互之間的區(qū)別,希望讀者可以有所收獲。
另外,本篇博客的代碼和類圖都保存在我的GitHub庫中:中的Chapter2。
總結
以上是生活随笔為你收集整理的iOS开发 - 面向对象设计的设计模式(一):创建型模式(附 Demo UML类图)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 湖南大学计算机技术大纲,湖南大学2018
- 下一篇: Myeclipse报的是严重: Sock