Iterator模式 (迭代器)
一個一個遍歷
一個集合類可以遵守 Iterator 協(xié)議,并實現(xiàn)一個 Iterator,一般包含 next()方法來獲取下一個值,借此來實現(xiàn)數(shù)據(jù)的遍歷。我們來實現(xiàn)一個簡單的數(shù)組的迭代器例子:
protocol IteratorProtocol {
associatedtype Element // 關(guān)聯(lián)類型
func next() -> Self.Element? // 返回下一個數(shù)值
}
class Iterator: IteratorProtocol {
var currentIndex: Int = 0
var datas: [Element]
init(datas: [Element]) {
self.datas = datas
}
func next() -> Self.Element? {
if self.datas.count > currentIndex {
defer {
self.currentIndex += 1
}
return self.datas[currentIndex]
} else {
return nil
}
}
}
可以將遍歷和實現(xiàn)分離開來,如果以后集合類型發(fā)生改變,只要同樣為其實現(xiàn)了 Iteraltor協(xié)議的方法,就無需大批量的修改代碼。
Adapter模式 (適配器)
加個適配器以便復(fù)用
就像要將一個220v的電源,轉(zhuǎn)換為10v的輸出一樣,我們需要一個適配器來進行輸入和輸出的轉(zhuǎn)換。如果一個類Number有createNumber()的方法,而有一個協(xié)議Print需要實現(xiàn)一個printNumber() 的方法,我們可以通過一個中間類NumberPrint繼承至Number并實現(xiàn)Print 協(xié)議,這樣的話我們就可以使用 createNumber()的方法,來實現(xiàn)printNumber()方法。這無疑是提高了代碼的復(fù)用,并且降低了代碼的耦合度,我們不需要修改原來的類型就能夠進行新的擴展。這種方法叫做類適配器模式。
class Number {
func createNumber() -> Int { // 生成一個隨機數(shù)
return arc4random() % 999
}
}
protocol Print {
// 需要打印出一個數(shù)字,這里舉例只是打印數(shù)字。實際項目中可能需要的是一個復(fù)雜的操作
func printNumber()
}
// 一個繼承于`Number`并遵守`?Print`的適配器類,它可以借助父類的方法來實現(xiàn)協(xié)議的方法
class NumberPrint: Number, Print {
func printNumber() {
print(super.createNumber()) // 借助父類的方法實現(xiàn),來實現(xiàn)協(xié)議的功能
}
}
還有一種模式是對象適配器模式,如果需要轉(zhuǎn)換的不是協(xié)議Print,而是類Print。對于無法進行多繼承的語言來說,無法創(chuàng)建一個中間類NumberPrint 同時繼承兩個類。
這是我們需要進行一些轉(zhuǎn)變,我們可以創(chuàng)建一個繼承于Print 的類 NumberPrint,而NumberPrint 中有一個屬性是 Number 的實例變量,我們可以通過調(diào)用實例變量的對象方法來實現(xiàn)Print 中的 printNumber()方法。
// 此時的 Print 是一個類
class NumberPrint: Print {
var number: Number = Number()
// 重寫父類的方法,利用屬性`number` 來實現(xiàn)父類的方法
func printNumber() {
let n = self.number.createNumber()
print(n)
}
}
當(dāng)需要擴展類的功能時,無需對現(xiàn)有的類進行修改。有時一個類已經(jīng)被多地方復(fù)用,并確認(rèn)可靠,如果冒然進行修改可能會出現(xiàn)意想不到的錯誤,而且又要進行一輪新的測試,所以可以使用適配器進行處理。
還有當(dāng)進行版本更新的時候,可能會有版本適配的要求,這個時候如何對舊的代碼進行適配就很重要了,我們可以通過適配器模式,對新的功能進行轉(zhuǎn)換,而保留舊的接口。
交給子類
Template Method模式(模板模式)
將具體的實現(xiàn)交給子類
如果一個類的邏輯代碼在父類中,而其具體的方法需要子類來實現(xiàn),我們就可以稱之為模板模式。其實父類就是一個抽象類,比如我們定義一個類Person,它實現(xiàn)了eat()、run()和sleep()方法。我們可以在父類中定義它的執(zhí)行順序。但是具體的方法是如何吃、如何跑、如何睡覺的我們要交給子類來實現(xiàn),畢竟Child(小孩)和Adult(成人)的習(xí)慣是不同的,不是嗎?
使用模板模式,當(dāng)我們遇到了執(zhí)行邏輯的改變時,我們不需要去修改各個子類,我們只需要修改抽象類就行了。并且無論是任何子類都可以被父類執(zhí)行。
class Person {
// 控制人的行為,先吃放后跑步,最后睡覺
func action() { eat()
run()
sleep()
}
// 要交給子類實現(xiàn)的方法
func run(){}
func eat(){}
func sleep()
}
class Child: Person {
func eat() {
print("drink milk")
}
func run() {
print("run 10m")
}
func sleep() {
print("sleep for 10 hours")
}
}
class Adult: Person {
func eate() {
print("eat food")
}
func run() {
print("run 10km")
}
func sleep() {
print("sleep for 6 hours")
}
}
上面的例子中我們使用一個抽象類來描述Person。你也可以使用協(xié)議Protocol來達到同樣的目的,不同的操作實現(xiàn),相同的操作步驟。
我們這里將邏輯代碼放到了父類中,把具體實現(xiàn)放到了子類中,但是實際使用時,如何分配父類和子類之間的代碼的處理級別就需要大家們自己斟酌了,如果父類中實現(xiàn)的太多,就失去了模板的意義,降低了父類的靈活性。但是父類實現(xiàn)的太少也會導(dǎo)致子類中的代碼重復(fù),所以一切看大家的感覺了。
Factory Method模式(工廠模式)
將實例的生成交給子類
什么是工廠模式呢?通過一個Factory 生成一個 Product, 而 Factory和 Product 的實現(xiàn)是由子類來實現(xiàn)的,使用了模板模式。所以你可以定制自己的工廠生產(chǎn)出你想要的實例。 舉個例子來說,我們把一種面點機器作為Factory,而生成的面點是類Product。面點機器生成面點用方法create(),而面點可以被吃掉有方法eat()。
protocol Factory { // 定義了一個工廠
func create() -> Product
}
protocol Product { // 定義了一個產(chǎn)品
func eat()
}
但是我們并沒有定義機器如何生產(chǎn)面點以及面點該如何被吃掉,這個時候我們創(chuàng)建類Dumpling Machine(餃子機器)繼承于Factory 并實現(xiàn)方法create().
接下來我們實現(xiàn)類Dumpling 繼承至Product 并實現(xiàn)了方法eat()。然后我們就可以通過DumplingMachine的create() 來生成Dumpling的實例了。
class DumplingMachine: Factory {
func create() -> Dumpling {
return Dumpling()
}
}
class Dumpling: Product {
func eat() {
... // 實現(xiàn)怎么吃餃子
}
}
這就是工廠模式的使用,那我們什么時候使用工廠模式呢。它又有什么好處呢?
工廠模式將框架和具體實現(xiàn)分離開來了,當(dāng)我們實現(xiàn)自己的框架(Factory,Product)時,我們直接定義相應(yīng)的方法邏輯和屬性結(jié)構(gòu),抽象的描述框架的行為。而開發(fā)者可以通過框架來實現(xiàn)自己的工廠和產(chǎn)品。當(dāng)我們使用工廠方法生成實例時,我們不需要考慮框架內(nèi)部的實現(xiàn),只需要實現(xiàn)預(yù)先約定的方法和屬性就OK了。
例如Object-C中的NSNumber 就使用了工廠模式。 我們可以通過NSNumber 生成不同的數(shù)值類型。
使用工廠模式的時候,生成實例我們有幾個實現(xiàn)的方法。<1> 指定其為抽象方法,這樣如何子類不實現(xiàn)的話,編譯器就會報錯。如果語法不支持定義抽象方法,這種方法就無法使用了。<2>為其實現(xiàn)默認(rèn)的實現(xiàn),當(dāng)子類沒有繼承父類的方法時。我們可以默認(rèn)實現(xiàn)此過程,不過我不推薦使用這種方法,因為往往默認(rèn)的實現(xiàn)都是不符合實際需要的,如果是忘了子類實現(xiàn)也無法通過編譯器來提醒。<3>在父類的實現(xiàn)里,拋出異常。并提示用戶必須實現(xiàn)子類的方法。這樣如果用戶忘記在子類中實現(xiàn)這個方法,就會拋出異樣,防止進一步的錯誤方法,也能夠提示用戶的錯誤發(fā)生在哪里。
生成實例
Singleton 模式(單例模式)
只有一個實例
當(dāng)你想要保證在任何情況下都只有一個實例,程序?qū)ν庖脖憩F(xiàn)出只有一個實例的時候,你就可以選擇使用單例模式來實現(xiàn)你的類。
單例模式,顧名思義就是這個類,只會生成一個實例變量。當(dāng)你想要新實例化一個類的時候,它要不返回給你唯一的實例,要不就拋出異常。通常一個單例模式的類,都只有一個類似于shareInstance()的類方法用來獲取唯一的實例。具體的實現(xiàn)方法,根據(jù)各個語言的不同而做改變。
基本上,就是在類中創(chuàng)建一個實例變量,這個實例變量一旦被初始化就無法被改變和銷毀。而獲取實例的方法,總是返回這是實例就 ok 了。
在某些情況下,類似于程序的窗口,一定是只有一個的。這個時候,為了方便管理這個窗口,我們就可以實現(xiàn)一個單例來處理具體的事物。亦或是在程序啟動以后需要,一個始終純在的類實例,來進行公共數(shù)據(jù)的處理和傳遞。
在 GUI 上也有可以用到的地方,比如一個界面上,只能同時出現(xiàn)一個的彈窗。你不必在顯示一個的時候,去關(guān)閉另一個,你只需要在這個地方顯示一個彈窗,另一個就會消失,畢竟只有一個實例。(你可以不必在其他的類中持有此實例,在想用的時候,直接獲取單例就 OK 了)
Prototype 模式(原型模式)
通過復(fù)制生成實例
一般情況下,我們在生成一個實例的時候。都是使用初始化方法,根據(jù)一個類來生成一個實例。當(dāng)時在某些情況下,我們可能并不想根據(jù)一個類來實例化一個對象,這個時候我們可以通過一個實例來生成另一個實例,這種復(fù)制的方式,我們就稱之為 Prototype 模式。
什么情況下我們可以選擇使用 prototype 模式呢?大概有以下三種情況。
- 當(dāng)對象功能相近而種類又太多的時候。如果使用類的話,會創(chuàng)建很多的類。如果是在一個類中,創(chuàng)建不同功能的實例,然后通過實例復(fù)制來進行后續(xù)的對象生成。(有一種把實例當(dāng)類用的感覺...)
- 太過復(fù)雜,無法通過類來生成實例。比如一個畫板上筆的運動軌跡,通過一個對象來記錄。在另一個地方要生成一個和這個筆的運動軌跡完全一樣的對象時,你很難通過一個類來實例化出這個對象。而對這個對象的復(fù)制就能夠很容易的做到。
- 寫框架時,想要將類和實例解耦。這個在實現(xiàn)的過程中,你就可要體會到,它的復(fù)用性很高,類之間是沒有耦合的。
實現(xiàn)的過程大概如此: 我們通過創(chuàng)建一個類Manager,并實現(xiàn)register()和createClone()方法,用來注冊和生成實例。然后是協(xié)議Product,它定義了方法use()和copySelf()。繼承此協(xié)議的類需要實現(xiàn)use()來實現(xiàn)如何使用執(zhí)行,copySelf()則是復(fù)制自己以生成新的實例。
另外則是是具體的類了,我們舉個例子是類State,它用來描述一個人身體的狀況,State 需要遵守協(xié)議Product并實現(xiàn)方法use()和copySelf()。我們生成一個一個state 實例,并使用Manager 來注冊此實例,然后可以通過 createClone()來進行復(fù)制,下面是簡化的偽代碼
這里我們可以把Manager想象為一個庫房,因為通過實例來生成實例,畢竟要有一個母體。而這個母體不能像是一個類一樣隨時可以調(diào)用,所以我們需要把它放在一個地方,在我們想用的時候,隨時可以使用,并且防止母體被意外修改,Manager 只是提供了復(fù)制的方法,你不能獲取和修改母體。
protocol Product {
func copySelf() -> Product
func use()
}
class Manager { // 用來管理可以自我復(fù)制的實例
private var datas: [String: Product] = []
public func register(name: String, object: Product)
{
datas[string] = object
}
public func createClone(name: String) -> Product {
return datas[name].copySelf
}
}
class State: Product {
var height: Double = 0
var weight: Double = 0
var age: Int = 0
func run() {
print("run")
}
func copySelf() -> State {
return self.copy()
}
func use() {
run()
}
}
Builder 模式 (構(gòu)建模式)
組裝復(fù)雜的實例
有時候,當(dāng)我們在構(gòu)建一個復(fù)雜的模塊的時候,我們需要將其拆分出來。形成一個個小的組件,然后通過一個管理類,進行重新組合。這樣的話我們可以最大限度的提供代碼的復(fù)用性及可替代性。
Builder 的思路十分的簡潔,主要分為Director(管理者), Builder(構(gòu)建器)和 Buinder 的子類。Director 通過一個 Builder 的實例來生成所需要的數(shù)據(jù),而數(shù)據(jù)的具體實現(xiàn)方式,則是通過子類來實現(xiàn)的。Builder 中應(yīng)當(dāng)涵蓋構(gòu)建數(shù)據(jù)所需要的所有必要方法,但是不應(yīng)當(dāng)含有特別的方法。這樣 Director 可以通過一個 Builder 來實現(xiàn)功能。
而具體的實現(xiàn)方式,它就不知道了,它只是知道其調(diào)用了一個 Builder,但是 Builder 有很多,它并不知道調(diào)用的是哪一個 Builder。這種不知道則提高了模式的靈活,只有不知道,才能夠被替換。
protocol Builder {
func playVideo()
}
class Director {
var builder: Builder? // 一個可以播放視頻的構(gòu)建器
func playVideo() { // 管理者想要實現(xiàn)播放視頻的功能
builder?.playVideo()
}
}
class Mp4Player: Builder { //實現(xiàn)一個 mp4播放器
func playVideo() {
self.playWithMp4()
}
func playWithMp4() {
... // 特有方法,以 MP4格式播放
}
}
class AviPlayer: Builder { // 實現(xiàn)一個 Avi 播放器
func playVideo() {
self.playWithAvi()
}
func playWithAvi() {
... // 特有方法,以 Avi格式播放
}
}
// 實際的使用中,你可以為 Director 提供 mp4或是 avi 播放器
// 只要符合`Builder`標(biāo)準(zhǔn), 你可以隨意替換 builder 及其內(nèi)部的實現(xiàn)
// 而不影響其他的代碼
main {
let player = Director()
player.builder = Mp4Player()
// palyer.builder = AviPlayer()
}
到現(xiàn)在為止,大家們對抽象這個概念應(yīng)該都很了解了,抽象可以是
抽象類也可以是接口,抽象的目的就是隱藏實現(xiàn)而突出邏輯。將邏輯和實現(xiàn)分開是實現(xiàn)代碼復(fù)用和提高維護性減少耦合常用的方法。以后如果提到抽象,希望大家都能理解它的含義。
Abstract Factory 模式 (抽象工廠模式)
將關(guān)聯(lián)的零件組裝成產(chǎn)品
抽象工廠的作用就是將抽象零件加工成抽象產(chǎn)品。直接理解的話可能不是特別容易懂,我們直接舉一個例子,就大概明白它的意思了。
我們的抽象工廠就是一個生產(chǎn)抽象產(chǎn)品電子板的機器,這個電子板上有很多的電容、電阻和各種各樣的元件(抽象元件)。這里我們并不知道電子板的大小和所需元件的參數(shù)和數(shù)量。那就意味著我們可以通過實現(xiàn)不同的電子板子類(抽象產(chǎn)品的子類)來生產(chǎn)不同的產(chǎn)品。而抽象元件 只要符合對應(yīng)的參數(shù),我們可以使用任意廠商的元件(抽象零件的子類)來使用。產(chǎn)品模型有了,元件也有了,那么實現(xiàn)一個具體的工廠來生產(chǎn)特定的產(chǎn)品是很重要的,不可能一個工廠可以生產(chǎn)任何產(chǎn)品吧,我們也可以通過修改工廠實例來優(yōu)化生產(chǎn)的工藝和流程。
這樣我們就實現(xiàn)了一個可以生產(chǎn)各式各樣產(chǎn)品的生產(chǎn)線。當(dāng)需要修改的時候,我們不用替換很多的數(shù)據(jù),只要將特定的子類替換掉就可以實現(xiàn)產(chǎn)品線的跟新,是不是和現(xiàn)在的代工廠一模一樣。
你會發(fā)現(xiàn)大部分的設(shè)計模式都要牽扯到抽象概念(接口)。這是很多模式優(yōu)化的基礎(chǔ)。如果你知道面對對象編程和函數(shù)響應(yīng)式編程等等,那你肯定對面對接口編程也有所耳聞,Swfit 相比 OC 就大量的使用了面向接口編程。這種編程方式的靈活性很高,如果大家感興趣,可以去多了解一下
分開考慮
Bridge 模式 (橋接模式)
將類的功能層次結(jié)構(gòu)和實現(xiàn)層次結(jié)構(gòu)分類開來
為了了解我們是為了橋接誰和誰,我們需要先來了解一下什么是類的功能層次和類的實現(xiàn)層次:
- 類的功能層次: 功能層次其實就是實現(xiàn)一個類的子類,當(dāng)你需要給一個類添加新的功能的時候,我們可以通過實現(xiàn)一個子類來完成。隨著功能的增多,我們可以實現(xiàn)一個又一個子類,并不斷的加深這個結(jié)構(gòu),這就是類的功能層次。(類的功能層次過多是不好的設(shè)計)就行下面這樣:
- Person
- Men
- Boy
- 類的實現(xiàn)層次: 類的實現(xiàn)層次則是抽象類的實現(xiàn),當(dāng)我們需要改變一個類的方法實現(xiàn)方式的時候,我們只需一個繼承抽象類的子類就行了,我們并不是為了給父類中添加其沒有的新功能,我們只是為原功能提供了不同的實現(xiàn)而已。
- Display
- StringDisplay
- HtmlDisplay
在實際的使用中,我們往往要根據(jù)實際的需求,靈活的運用這兩種結(jié)構(gòu)。如果僅僅是將它們混合在一起使用的話,當(dāng)應(yīng)用變得更為復(fù)雜的時候,你就很難清楚的認(rèn)識到,你到底應(yīng)該繼承哪個類。
所以我們需要將功能層次和實現(xiàn)層次分離開來。但前面說了,我們要靈活的運用兩種層次,那就要讓他們之間有聯(lián)系,這時我們就需要在它們之間建一條橋梁。
大概的實現(xiàn)就是下圖這樣,Display 類是功能層次的最高層級,它持有一個 DisplayImp1的實例,這個實例中有與 Display 相對應(yīng)的功能。DisplayImp1是一個抽象類, 而 StringDisplayImp1 繼承了 DisplayImp1,實現(xiàn)了所有的方法。

這時我們可以創(chuàng)建一個 stringDisplayImp1的實例,通過這個實例來創(chuàng)建一個 Display 或是 PhotoDisplay 來使用。
Strategy 模式 (策略模式)
整體地替換算法
策略在程序中也可以被稱作算法。我們在處理程序中一些復(fù)雜的關(guān)系時,所使用的算法可能會根據(jù)軟件的系統(tǒng)、時間的需求、錯誤率及系統(tǒng)硬件機能等進行相應(yīng)的調(diào)整。這是我們要同時完備幾種算法以便在系統(tǒng)中進行替換,不加設(shè)計的話,替換算法本身也將是一個麻煩的事情。 Strategy 模式就可以方便的完整替換整個算法。
例如我們想要實現(xiàn)一個棋類應(yīng)用,單機模式下我們將會有一個AI來和玩家對戰(zhàn)。我們定義一個 Player 類作為AI的類。創(chuàng)建一個Player 需要提供一個策略,而這個策略Strategy是一個抽象類,它定義了一系列的方法,可以通過現(xiàn)在棋局的數(shù)據(jù)推算出下一步該往哪走。我們根據(jù)游戲的算法來制定算法,這個時候我們就可以通過不同的子類策略實現(xiàn)設(shè)備的適配和 AI 難度的調(diào)節(jié)。
無論策略發(fā)生了什么改變,我們無需修改任何的接口,我們只需要替換一個策略的類,就可以完成整個算法的替換。
Strategy 模式常用在棋牌類游戲中,而且確實很實用。我感覺 Strategy 模式不太像一個正經(jīng)的設(shè)計模式,它的概念很簡單,甚至就是抽象類或接口模式的基礎(chǔ)應(yīng)用而已。我們平時寫代碼的時候,多多少少會用過或見過這類用法。
一致性
Composite 模式 (復(fù)合模式)
容器與內(nèi)容一致性
我們平時使用的電腦、ipad和手機等電子設(shè)備都有自己的文件管理系統(tǒng)。他們的基本結(jié)構(gòu)就是有一個根目錄,下屬很多的文件夾和文件。文件夾下面又是文件夾或是文件。我們所看的這種樹狀結(jié)構(gòu)看起來是由兩種數(shù)據(jù)類型組成的。其實我們完全可以把它們統(tǒng)一看做為一種目錄條目。
這種目錄條目擁有通用的屬性和方法,它們擁有一致的行為。能夠使容器和內(nèi)容具有一致性,創(chuàng)造出遞歸的結(jié)構(gòu)的模式就是 Composite 模式。

Entry 是一個抽象類,它定義了一個
條目。它通過getName()來獲取條目名字、getSize()獲取條目大小、printList()是用來打印內(nèi)容列表,add()則是提供給子類Directory來添加新的File和Directory。
File是文件類,它可以返回文件名、大小和報告自己的目錄。Directory是文件夾類,它有名字name, 還有directories用來存儲自己內(nèi)部的文件和文件夾列表,但是它沒有自己的大小,它的大小是通過內(nèi)容的getSize()方法相加獲取。
通過這樣的方式,我們就構(gòu)建了一個遞歸的文件結(jié)構(gòu),這種結(jié)構(gòu)將內(nèi)部的內(nèi)容和外部的容器統(tǒng)一起來,使對象的調(diào)用變得更易理解和簡潔。
Decorator 模式 (裝飾器模式)
裝飾邊框與被裝飾物的一致性
一提到裝飾器,大家肯定都知道裝飾的概念。裝飾器就像你照片的相框,水果蛋糕上的點心一樣,通過裝飾物使主體[被裝飾物](相片和蛋糕)變得與眾不同。這個模式的作用也是如此,但是如果只是這樣的話,你很容易把裝飾器看做和主體不同的東西。你的想法大概是這樣的:

你可能以為它們是一被一個一個放到被裝飾物上的。這樣的話,你就無法裝飾裝飾物本身了,整個模式的擴展性就被降低了。我們需要裝飾物和被裝飾物具備一致性,這樣的話接口就變得透明了起來,無論我們?nèi)绾螌?strong>被裝飾物進行裝飾,我們最后所看到的被裝飾物所體現(xiàn)的接口和行為還是和最初是一樣的。而這樣的形式才是真正的裝飾器模式,它就像一個俄羅斯套娃,一層嵌套一層,每層都可以看著一個包裝或裝飾,直到最后一個套娃出現(xiàn)。

那我們?nèi)绾螌崿F(xiàn)這個結(jié)構(gòu)呢,它看起來和 Composite 模式有些像。我們舉一個顯示程序的例子,它可以為顯示內(nèi)容添加+、-、* 等字符邊框。
我們需要一個抽象類 Dispaly來描述顯示的流程,通過一個子類 StringDisplay 可以顯示一行字符串。接下來我們定義一個裝飾器的抽象類Decorator,它是繼承于Display 的子抽象類。Decorator 中有一個類型為Display的成員變量display,它表示被裝飾物。
這樣我們就將裝飾物和被裝飾物統(tǒng)一起來了,使他們滿足一致性。它的優(yōu)缺點大概有以下幾點
- 接口的透明性 無論我們進行多少次裝飾,被裝飾物的接口都沒有被隱藏起來,還是和當(dāng)初一樣。
- 可以在不改變裝飾物的前提下去添加功能 當(dāng)我們添加功能的時候,只需要實現(xiàn)不同的裝飾器就 OK 了。
- 需要實現(xiàn)太多的小類 我們需要些很多不同功能的裝飾器,這些類的功能通常不多,但是卻數(shù)量巨大。
訪問數(shù)據(jù)結(jié)構(gòu)
Visitor 模式 (訪問者模式)
訪問數(shù)據(jù)結(jié)構(gòu)并處理數(shù)據(jù)
我們大家都學(xué)過數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)結(jié)構(gòu)的重要性就不言而喻了。但是此設(shè)計模式的重點不是如何設(shè)計數(shù)據(jù)結(jié)構(gòu),而是如何訪問數(shù)據(jù)結(jié)構(gòu)。
一般我們在實現(xiàn)了一個數(shù)據(jù)結(jié)構(gòu)后,都會將數(shù)據(jù)的操作方法和數(shù)據(jù)結(jié)構(gòu)本身綁定在同一個類中。使它們成為一個整體。這樣做在以后使用的時候會方便很多,也不用那么多的類進行操作。
但是當(dāng)你想要擴張數(shù)據(jù)結(jié)構(gòu)的處理方法的時候,你就需要直接修改數(shù)據(jù)結(jié)構(gòu)的類。這樣做很不方便,既麻煩又不利于項目的穩(wěn)定。
如何解決呢?以我們學(xué)習(xí)了以上那么多設(shè)計模式的經(jīng)驗,當(dāng)然是將數(shù)據(jù)結(jié)構(gòu)和訪問操作分離開來嘍。
在 Visitor 模式中,Visitor 是一個訪問者的形象。它是一個抽象類,內(nèi)部定義了一個 visit()方法。,以我們在 Composite 模式中使用的文件系統(tǒng)為例,這里的Entry類同樣代表了數(shù)據(jù)結(jié)構(gòu)的抽象形式,它的具體實現(xiàn)是由File 和Direcotry 實現(xiàn)的。
不一樣的地方是,我們這里要定義一個新的接口 Element,這個接口定義了一個accpet(Visiter)方法,Entry 遵守這個接口,而它的子類需要實現(xiàn)這個接口的方法。
accept()方法接受一個Visitor 實例,并在 accept()的方法內(nèi)部調(diào)用Visitor 的visit()方法,同時把自己【也就是Entry 本身】傳給這個方法。這樣在visit()的內(nèi)部就可以進行數(shù)據(jù)的處理了,而細(xì)節(jié)則是由Visitor 的子類所實現(xiàn)的。
此時,我們就可以通過實現(xiàn)不同的Visitor 子類進行數(shù)據(jù)訪問方式的擴展。
整個模式的結(jié)構(gòu)就是如此,但是你可能會為里面 visit()和 accpet()的調(diào)用感到困惑在處理 Directory 的過程中,我們需要遍歷里面的所以對象,并一個個調(diào)用他們的 accpet()方法,同時把Visitor 自己也給傳過去,然后在各個Entry 中再次調(diào)用 visit()方法,進行同樣的操作,直到最后一個文件是File,遞歸就結(jié)束了。
這里用到了兩個類的兩個方法來進行嵌套遞歸,著實很難讓人理解。一個遞歸就已經(jīng)讓人頭痛了,這樣的遞歸也主要是為了實現(xiàn)數(shù)據(jù)處理和數(shù)據(jù)結(jié)構(gòu)的分離,并簡化數(shù)據(jù)的處理過程。
當(dāng)你實現(xiàn)了一個新的Visitor 并通過一句調(diào)用就可以直接處理一個數(shù)據(jù)類型,而不用關(guān)心具體的類型時,你就會感受到它的好處了。
一切的辛苦都是有價值的。
Chain of Responsibility 模式 (責(zé)任鏈模式)
推卸責(zé)任
看到這個模式的描述推卸責(zé)任,是不是感覺有些奇葩。我們生活中,推卸責(zé)任看起來像是一個低效的處理問題的方式,那是如何在程序中發(fā)生正向的作用呢?
我們先看一下責(zé)任鏈模式的結(jié)構(gòu):
- 問題: 既然要處理問題,那問題本身就很重要了。我們需要一個抽象類或是接口來定義問題。
- 處理問題的抽象類: 問題需要交給一個類來處理,而這個類定義了處理問題的流程,比如判斷自己能否解決,如果能解決就返回結(jié)果。如果不能解決,就自動交給下一個類來處理,要是沒有下一個類就返回錯誤。
- 具體處理問題的類: 抽象類我們已經(jīng)定義了,但是具體的解決問題的方法,需要不同的子類來實現(xiàn)。
比如我們有多種不同等級的預(yù)警處理方案來處理一個警報。警報分為藍、黃、橙和紅四個級別。我們定義一個警報類
Alert,它通過初始化方法init(int)傳入一個等級來創(chuàng)建。
接下來我們定義一個抽象類Handle來實現(xiàn)處理警報的流程,它有屬性next(Handle 的實例)表示要將責(zé)任推給那個對象。以及一個讓子類實現(xiàn)的抽象方法resolve(),用來表示解決問題的具體實現(xiàn)。
它通過方法targetr(Alert)來處理警報,target內(nèi)會調(diào)用resolve()方法來處理問題,如果能處理就返回成功,如果無法處理,就將問題交給next,接下來會調(diào)用next的target()方法。直到有方案能處理警報,或是沒有辦法處理,報告錯誤。
現(xiàn)在想一想我們開頭的問題,責(zé)任鏈模式有什么好處呢。
-
我們可以簡化處理問題的流程,如果不踢皮球的話,我們需要為每個問題指明對應(yīng)的處理方法。那
Alert本身就需要知道自己能被哪個類處理,這就像你想要解決一個問題,你未必就一定能找到一個對的人。你只是純粹的想解決問題而已。讓問題知道自己該被誰解決,就會讓問題本身變得更加復(fù)雜。 -
可以動態(tài)的修改流程,我們的處理順序是?鏈?zhǔn)降模弦粋€類決定下一個要處理的類。我們只需要需改一下
next就能夠輕松的改變處理問題的順序。
使用責(zé)任鏈模式的一個問題是,會增加處理問題的時間,因為是一個一個去判斷能不能解決的。如果問題沒有固定的解決方案,使用
責(zé)任鏈模式是沒有任何問題的。如果能夠確定問題的處理方式就沒必要這樣了。
簡單化
Facade 模式 (窗口模式)
簡單窗口
隨著時間的腳步,我們的程序會越來越完善,同時也會變得更加復(fù)雜和冗余。當(dāng)我們實現(xiàn)新的功能時,我們需要在眾多的類中,找到需要的類,并組織邏輯和順序。特別是在大型程序中,每次調(diào)用都要注意眾多的類之間錯中復(fù)雜的關(guān)系。難道我們就不能使用一個統(tǒng)一的窗口,只需要調(diào)用這個窗口的方法,我們就可以實現(xiàn)這個操作,這個思想就是Facade 模式。
例如要實現(xiàn)一個發(fā)布模塊,要發(fā)布的內(nèi)容有文字、視頻和圖片。原來的操作是我們要分別上傳圖片 >> 上傳視頻 >> 處理文字 >> 整理json 數(shù)據(jù) >> 上傳服務(wù)器。 這樣的操作做一次還好,如果有多個地方需要使用到發(fā)布的功能,這樣就顯得太過復(fù)雜了,也不利于整合模塊的功能。
我們現(xiàn)在使用 Facade 模式進行改進,實現(xiàn)一個PublishManager 類就是Facade模式 中的窗口,它有一個方法publish(text, image, video) 可以直接接受文字、圖片和視頻,在PublishManager 內(nèi)部,它可以把文字交給TextHandle[處理文字的類] 來處理,把圖片和視頻的上傳交給UploadManager[進行上傳的類],拿到 url 后通過JSONSerialization進行 JSON 處理。最后通過HTTPManager 將數(shù)據(jù)傳遞給后臺。
這樣以后,我們無論在任何地方需要使用到發(fā)布功能的時候,我們只需要調(diào)用PublishManager 的發(fā)布方法,就可以直接進行發(fā)布,這里我們就實現(xiàn)了一個窗口,進行發(fā)布的窗口,而復(fù)雜的內(nèi)部調(diào)用,就被我們隱藏起來了,我們無需關(guān)心它的內(nèi)部調(diào)用,如果以后需要進行修改我們可以直接修改PublishManager 而不用再調(diào)整其他的地方,使得發(fā)布的功能變得更加純粹。
Mediator 模式 (中介模式、仲裁者模式)
只有一個仲裁者
如果你要編寫一個聯(lián)機的棋類游戲,同時有4名玩家進行對戰(zhàn),每人一步,通過某個規(guī)則可以吃掉別人棋子。我們該如何同步各個玩家的棋盤和管理各個玩家的狀態(tài)呢。
如果我們每個玩家的終端,各自控制自己的狀態(tài)而后將數(shù)據(jù)發(fā)送到其他的終端。那每個終端都要處理其他終端發(fā)送過來的數(shù)據(jù),而后同步自己的狀態(tài)。
這時每個終端都有一份自己的數(shù)據(jù),處理的邏輯隨著玩家的個數(shù)增加也會變得更加復(fù)雜。并且一旦一個玩家的數(shù)據(jù)出錯,他會把錯誤的數(shù)據(jù)發(fā)送給其他的終端,這時雙方的數(shù)據(jù)會發(fā)生沖突而產(chǎn)生致命錯誤。
而今天我們將通過 Mediator 模式來解決這個問題。我們通過一個仲裁者,你可以把它作為游戲的一個中間服務(wù)器。玩家的每個終端都只是接收仲裁者發(fā)來的屬于自己的數(shù)據(jù)并進行狀態(tài)的更新,而自己的每一步操作就只是傳遞給仲裁者。仲裁者進行數(shù)據(jù)的處理后,再通知所有的終端分別更新狀態(tài),這樣一來各個終端的操作實時匯集到仲裁者,而仲裁者再實時進行數(shù)據(jù)分發(fā)。
這樣做就不會出現(xiàn)數(shù)據(jù)不同步的狀況了,而數(shù)據(jù)的處理集中到了一點,降低了出現(xiàn) bug 的概率。即使出現(xiàn)了問題也容易排查 bug 發(fā)生在哪里。
除了上述的使用情景以外,我們在項目當(dāng)中處理 GUI 的點擊、界面和操作邏輯管理時,也可以使用Mediator 模式。 我們創(chuàng)建一個抽象類類Manager作為 Mediator,再創(chuàng)建一個接口Colleague, 表示和Manager 連接的各個控件。Manager 定義了各種各樣設(shè)置Colleague 的方法和方法didChange( Colleague )來告知Manager哪個控件發(fā)生了改變。我們實例化Manager 的一個子類,將其傳遞給各個控件[實現(xiàn)接口 Colleague],當(dāng)控件發(fā)生狀態(tài)變更時就傳遞給這個仲裁者,而后仲裁者進行處理后,通過各個設(shè)置Colleague 的方法進行控件狀態(tài)的更新。
開到這里我們就能發(fā)現(xiàn),
Mediator 模式是一種雙向綁定機制。只不過是各個對象都綁定同一個仲裁者,而后通過與它進行通信借以實現(xiàn)與其他的對象進行通信的目的。
管理狀態(tài)
Observer 模式 [觀察者模式]
發(fā)送狀態(tài)變化通知
說到觀察者模式,我想大家都應(yīng)該有所了解。很多語言中都有Observer 模式的設(shè)計,雖然各種各樣的實現(xiàn)各有區(qū)別,但都是以Observer 觀察被觀察者,當(dāng)被觀察者發(fā)生改變時,通知 Observer 發(fā)生了什么改變為目的。
我們現(xiàn)在來實現(xiàn)一個簡單化的觀察者模式,我們創(chuàng)建一個抽象類NumberGenerator, 再創(chuàng)建一個RandomNumberGenerator 繼承自NumberGenerator,
class NumberGenerator {
var value: Int = 0 // 在這里簡單的表示為自己的值
public var observers: [Observer] = [] // 儲存所有的觀察者
func addObserver(ob: Observer) {...} // 添加觀察者
func deleteObserver(ob: Observer) {...} // 刪除觀察者
func notifyObserver() {...} // 通過所有的觀察者,數(shù)據(jù)發(fā)生改變
func excute() {...} // 執(zhí)行數(shù)據(jù)跟新
func getNumber() { FatalError() }// 交給子類實現(xiàn),實現(xiàn)數(shù)值如何生成
}
class RandomNumberGenerator: NumberGenerator {
func getNumber() {...} // 返回一個隨機數(shù)
}
接下來就是創(chuàng)建一個接口Observer,只有實現(xiàn)了此接口的類才能成為 NumberGenerator 的觀察者。它只有一個 update 方法 [在 swfit 中接口相當(dāng)于協(xié)議]
protocol Observer {
func update(obj: NumberGenerator)
}
class Display: Observer {
func update(obj: NumberGenerator) {
print(obj.value)
}
}
我們通過Dispaly的實現(xiàn),將每次訂閱到的值顯示出來。下面是一個簡單的使用
let generator = RandomNumberGenerator()
let observer = Display()
generator.addObserver(observer)
generator.excute()
// print: 2 打印出一個隨機數(shù)
以上就是一個簡單化的Observer 模式的使用,如果細(xì)心的話你會看到,我們直接將被觀察者本身返回給了觀察者。一個對象可以同時被很多觀察者觀察,但是觀察者想要獲取的信息可能各有不同,所以直接將自身傳遞,讓觀察者自己去查找。
當(dāng)然了,這是由于我們的設(shè)計過于簡陋。在 Objective-C 中,我們可以直接監(jiān)聽各個對象的屬性。
其實,觀察者模式,我們也可以稱為訂閱模式。觀察者并不是去主動觀察,而是被觀察者通知觀察者的。如果理解為發(fā)布和訂閱就更加契合了,你可以訂閱一個對象,如果他發(fā)布了新的內(nèi)容,你就會得到通知。
到這里,如果你上面的各種模式都了解了一遍的話,你就會發(fā)現(xiàn),在很多模式中已經(jīng)出現(xiàn)了很多的這種可替換性設(shè)計了。通常進行替換性設(shè)計,可以提高系統(tǒng)的靈活性和降低耦合性。一般我們通過以下兩者方式進行替換性設(shè)計。
- 利用抽象類和接口從具體類中提取出抽象方法
- 在將實例作為參數(shù)傳遞至類中,或是在類的字段中保存實例時,不使用具體的類型,而是使用抽象類和接口
使用這種設(shè)計我們可以輕松的替換項目中的具體類。
Momento 模式
保存對象狀態(tài)
我們平時使用的文本編輯器、PS 等等,都有一系列十分重要的功能,就是撤銷(undo)、 重做(redo) 和歷史快照(history)。像是撤銷這樣的操作我每天要使用幾百次,那如何記錄每個操作節(jié)點的狀態(tài)就十分重要了。
而 Momento 模式就十分善于處理這種情況,Momento 有紀(jì)念品的意思,我們也可以想象著把一個對象每個時間點的狀態(tài)拍上一張照片作為紀(jì)念品。
當(dāng)我們需要的時候,我們可以通過每個時間點的快照來恢復(fù)對象的狀態(tài)。比如我們要記錄一個棋局,類ChessGame表示一局正在進行的棋盤。里面有方法createMomento()通過當(dāng)前棋子的數(shù)據(jù)存儲快照。我們是創(chuàng)建一個類Momento 來存儲棋局?jǐn)?shù)據(jù)的。生成的快照被存入棋局的數(shù)組history 中,當(dāng)調(diào)用undo()方法時,我們就取出最后一個棋局狀態(tài)進行棋局的復(fù)原,這就是Momento 模式。
class ChessGame {
private var chessmanLocations: [Any]! // 這里面是此次雙方旗子的位置信息
private var history: [Momento]? // 所以得快照數(shù)組
func undo() {
let state = self.history.pop()
... 根據(jù)信息恢復(fù)所有的棋子數(shù)據(jù)
} // 撤銷
func createMomento() {
let mom = Momento(self.chessmanLocations)
self.history.append(mom)
} // 生成一個快照,并存入數(shù)組中
}
class Momento {
var chessmanLocations: [Any]!
}
這里的 Momento 模式和以前的 Prototype 模式在存儲狀態(tài)上也一點點相似,但是這里的Momento 只是存儲恢復(fù)狀態(tài)所需要的必要數(shù)據(jù),而Prototype 模式中,實例復(fù)制成的則是完完全全相同的另一個實例,所以它們的區(qū)別還是很明顯的。
State 模式 (狀態(tài)模式)
用類表示狀態(tài)
有些時候我們在項目當(dāng)中會遇到各種各樣的狀態(tài),比如應(yīng)用的夜間模式和白天模式,再或者是一個警報系統(tǒng)的各個預(yù)警狀態(tài)。使用夜間、白天模式是一些閱讀軟件常備的功能,切換不同的模式,整個應(yīng)用的界面會發(fā)生色調(diào)的轉(zhuǎn)變。而警報系統(tǒng)在不同的預(yù)警狀態(tài)下,對同一事件的處理方式也是不同的。
針對這種需要根據(jù)狀態(tài)判斷的例子,我們通常使用的方法,就是通過 if或是switch 來判斷不同的狀態(tài),而執(zhí)行不同的實現(xiàn)方法。比如應(yīng)用的夜間和白天模式:
class Manager {
public var isNight: Bool
func navBarColor() -> UIColor {
if self.isNight {
return UIColor.black
} else {
return UIColor.white
}
}
func bgColor() -> UIColor {
if self.isNight {
return ...
} else {
return ...
}
}
...
}
這個就是我們一般的實現(xiàn)方式,這樣的實現(xiàn)方式在簡單的狀態(tài)切換時到?jīng)]有什么。但是像是以上這樣的白天和黑夜模式的界面顏色獲取,可能有幾十個方法,一個類中滿滿的都是if 看起來就眼花。如果這個時候你需要添加另一個模式,你就需要在每個方法下面添加一個 else if,重要的是,編譯器并不會因為你忘記寫一個,而通知你, 所以,在添加新的模式時,我們很容易出錯,接下來就是用到 State 模式的時候了?。
通過一個類來表示一個狀態(tài),就是狀態(tài)模式。 在State 模式中我們通過創(chuàng)建一個類來表示一個新狀態(tài)。像以前一樣,我們需要創(chuàng)建一個抽象類State 來定義狀態(tài)中需要實現(xiàn)的方法。接下來我們分別定義NightState 和DayState來表示白天和黑夜的狀態(tài),通過以下的代碼我們來看看有什么區(qū)別。
public class State {
public func navBarColor() -> UIColor {
FatalError("no implementation")
}
public func bgColor() -> UIColor {
FatalError("no implementation")
}
}
class DayState: State {
static let instance = DayState() // 狀態(tài)不需要重復(fù)創(chuàng)建,使用單例模式
class func shared() -> DayState {
return self;
}
override public func navBarColor() -> UIColor {
return UIColor.white
}
override public func bgColor() -> UIColor {
return UIColor.white
}
}
class DayState: State {
static let instance = DayState() // 狀態(tài)不需要重復(fù)創(chuàng)建,使用單例模式
override public func navBarColor() -> UIColor {
return UIColor.black
}
override public func bgColor() -> UIColor {
return UIColor.black
}
}
class UIManager {
var currentState: State;
func resetState() {
let date = NSDate()
if (date => 9am && date <= 7pm) {
self.currentState = DayState.instance
} else {
self.currentState = NightState.instance
}
}
func showNavBarColor() {
setupNavBarColor(self.currentState.navBarColor)
}
func showBgColor() {
setupBgColor(self.currentState.bgColor)
}
}
通過上面的例子,我想你一定明白了它們的區(qū)別。在這樣的 State 模式下,UIManager 是用來控制界面的顏色顯示的。它負(fù)責(zé)切換和控制狀態(tài),所以它需要知道所有狀態(tài)的條件。
除了讓UIManager 控制狀態(tài)的切換外,我們還可以讓每個狀態(tài)本身去控制現(xiàn)在的狀態(tài),這里就像是 Chain of Responsibility 模式(責(zé)任模式)。我們擴展一下這個協(xié)議:
extension State {
func setTime(manager: UIManager, time: Date) {
}
}
// DaySate 和 NightState 需要將上對應(yīng)的方法
class DayState: State {
.....
func setTime(manager: UIManager, time: Date) {
if (date < 9am && date > 7pm) {
manager.currentState = NightState.instance
}
}
}
class NightState: State {
..... func setTime(manager: UIManager, time: Date) {
if (date >= 9am && date <= 7pm) {
manager.currentState = DayState.instance
}
}
}
可以看出來,UIManager 只需要默認(rèn)一個狀態(tài),然后再調(diào)用方法前,告知當(dāng)前模式時間,它就可以通過自己的判斷來尋找正確的狀態(tài)。這里的狀態(tài)只有兩種,如果有很多種的話,自己不是此狀態(tài),就傳遞給下一個狀態(tài),直到找到一個正確的狀態(tài)。
使用第一種方法,manager 就需要知道所有的狀態(tài)關(guān)系。但是耦合度很低,各個狀態(tài)不需要知道其他的狀態(tài)。
而第二種方法,每個狀態(tài)或多或少的需要知道其他的狀態(tài),這樣增加了耦合度。不過 Manager 不用再管理所有的狀態(tài)了,它只需要處理方法就行了。
-
我們可以方便的添加各種各樣的狀態(tài)我們只需要實現(xiàn)
State的方法就行了,可能還需要處理一下切換到其他狀態(tài)的情況,不過這是你使用第二種Manager 管理的時候。 -
添加依賴于狀態(tài)的處理十分的麻煩當(dāng)我們對狀態(tài)添加一個新的處理方法的時候,我們需要修改每一個狀態(tài),這十分的麻煩。所幸的是,我們不會忘了給其中的一個狀態(tài)添加新的處理方法,因為編譯器會提示我們,如果我們忘記了給任意一個狀態(tài)添加方法。如果不使用
State 模式就不會得到編譯器的幫助,可想而知,一旦大意,就會引發(fā)不可知的 bug。
避免浪費
Flyweight 模式 (輕量級模式)
共享對象,避免浪費
我們都知道在應(yīng)用當(dāng)中使用的對象都占用了一定的系統(tǒng)內(nèi)存,當(dāng)我們的對象占用內(nèi)存過大時,就會降低系統(tǒng)的運行速度和穩(wěn)定性,甚至引發(fā)崩潰。 如果有些對象可以被共同使用,就可以減少創(chuàng)建新對象的開銷,也可以降低內(nèi)存的占用。所以 Flyweight 模式就是 通過盡量共享實例來避免 new 出新的實例來大大降低系統(tǒng)的內(nèi)存消耗。
這里我們舉一個例子,比如我們要打印一張圖片,而這張圖片是又幾種不同的素材圖片拼出來的。當(dāng)我們在在打印圖片的時候,我們需要先將對應(yīng)的素材按照順序排列好,才能進行打印。
class Image {
let id: Int
let data: Data?
init(id: Int) {
self.id = id
self.data = createData(id)
}
func createData(id: Int) -> Data {
... // 根據(jù)id 生成圖片的數(shù)據(jù)
return data
}
}
class ImageManager {
var imageIds: [Int] = [] // 需要排列的圖片 id 數(shù)組
var imageCache: [Int : Image] = [:] // 每個 id 對應(yīng)一個它的圖片緩存
// 通過 id 獲取圖片,如果緩存中有的話就直接使用,如果沒有的話,就創(chuàng)建一個放入到緩存中
func getImage(id: Int) -> Image {
if let image = imageCache[id] {
return image
} else {
let image = Image(id)
imageCache[id]= image
return image
}
}
// 打印圖片,根據(jù) id 數(shù)組的順序進行排序
func printImage(imageIds: [Int]) {
self.imageIds = imageIds
var images = imageIds.map { return getImage($0) }
printWithImageData(images)
}
func printWithImageData(imageDatas: [Image]) {
... //根據(jù)圖片的數(shù)據(jù)進行打印
}
}
我們創(chuàng)建Image 當(dāng)做是素材,ImageManager是用來排版素材的類,它通過傳入一個包含素材 id 的數(shù)組來打印出對應(yīng)的圖片。 在排列過程中,我們每種素材的信息其實是不變的,所以它是可以共享的,我們使用一個字典把 id 當(dāng)做 key來 實現(xiàn)緩存素材數(shù)據(jù)。當(dāng)通過 id 排列素材時,我們直接獲取緩存中的素材數(shù)據(jù),如果重復(fù)使用了一個素材,也不會再次創(chuàng)建,而是共享一個對象。通過這樣的方式,我們就能夠減少一大部分的內(nèi)存消耗。
不過共享同一個對象也有問題,就是改變了這個對象,那么所有共享它的也會發(fā)生改變。這有時候是好事,有時候是壞事,具體要看應(yīng)用的場景。
但是大概可以這樣判斷是否該共享該對象。
-
代表本質(zhì)的,不依賴于狀態(tài)和位置的對象可以共享它是一個
intrinsic 信息。 -
外在的,依賴于狀態(tài)和位置的對象不能共享 它是一個
extrinsic 信息。
一般的對象都適用于這兩個規(guī)則。根據(jù)項目的實現(xiàn)目的,靈活的運用Flyweight 模式可以優(yōu)化你的應(yīng)用內(nèi)存占用。
Proxy 模式 (代理人模式)
只在必要時生成實例
當(dāng)讀到代理人模式的時候,希望你不會把它和OC 中的delegate 弄混淆了。OC中的 delegate 其實是接口interface或者說是protocol 的使用,而我們今天要了解的Proxy 模式中的代理人指的是替原本的對象來執(zhí)行操作。
在哪些情況下,我們需要使用Proxy 模式呢? 通常是當(dāng)一個對象的創(chuàng)建需要消耗大量的性能,而它的重要操作又可以延后的時候。在這種情況下,如果需要使用此對象,就立刻創(chuàng)建,可能會占用過高的性能,而后又沒有使用到這個對象的重要功能,那豈不是白白浪費了大量的系統(tǒng)算力。
所以我們需要使用一個代理人來替代這個本人。它實現(xiàn)這個本人的基本屬性和方法,而將耗時的工作交給真正的本人去做,那樣只有在真正需要本人去做得事情才會去創(chuàng)建本人,而其他的不耗時操作將交給代理人去做。
這里我們舉一個例子,比如有一個打印圖片的類ImagePrint,它通過一個url 來初始化實例,調(diào)用Print 方法就可以打印出這張圖片,這就是本人。又有一個類ImagePrintProxy 表示它的代理人。接口ImagePrintable 規(guī)定了本人和代理人都應(yīng)該具備的方法和屬性。下面我們通過偽代碼來具體了解一下整個過程:
// 首先是接口 ImagePrintable,它定義了一個能打印圖片的類,都需要實現(xiàn)什么方法
protocol ImagePrintable { func setUrl(urlStr: String) // 設(shè)置圖片地址
func getUrl() -> String // 獲取圖片地址
func print() // 根據(jù)地址,打印圖片
}
// ImagePrint 本人,它是打印圖片的實際操作者,打印圖片是一個耗時的操作,
class ImagePrint: ImagePrintable {
var url: String
var printer: PhotoPrinter? // 這是一個圖片打印機,初始化它需要耗費大量的時間
init(url: String) { self.url = url
self.printer = PhotoPrinter() // 這是一個耗時操作
}
func getUrl() -> String {
return self.url
}
func setUrl(urlStr: String) { self.url = urlStr
}
func print() {
self.printer.printImage()
... // 根據(jù) url 下載圖片然后使用 printer 再打印出來
}
}
// 最后就是 ImagePrintProxy 代理人,通過代理人我們可以在不打印圖片時
// 設(shè)置和獲取圖片的地址,而不用初始化 ImagePrint。因為初始化`ImagePrint` 時
// 會創(chuàng)建`printer`,這會耗費大量的時間。而是在調(diào)用 print 的時候,在初始化它。
class ImagePrintProxy: ImagePrintable {
var url: String
var real: ImagePrint? // 這是真正執(zhí)行打印操作的對象
init(url: String) {
self.url = url
}
func getUrl() -> String {
return self.url
}
func setUrl(urlStr: String) {
if self.real != nil { //當(dāng)存在本人時,就設(shè)置本人的值
self.real.url = urlStr
}
self.url = urlStr
}
func print() {
self.release()
self.real.print() // 調(diào)用本人來實現(xiàn)打印圖片的方法
}
func release() { // 生成原始對象的方法
if self.real == nil {
self.real = ImagePrint(self.url)
}
}
}
看過這個例子以后,就很容易理解什么是Proxy 模式了,使用 Proxy 模式的時候,調(diào)用者并不關(guān)心是誰實現(xiàn)了里面的方法,它只是調(diào)用了符合ImagePrintable 的類。而實際的執(zhí)行者ImagePrint 也不關(guān)心自己是被直接調(diào)用還是間接調(diào)用。對問題的處理就交給了ImagePrintProxy 這個代理人身上。這樣的話,代理人就可以根據(jù)實際的情況來替?本人完成一些簡單的工作,而盡量將本人的創(chuàng)建延后,只在真正需要使用的時候,才會創(chuàng)建本人。
這樣的設(shè)計,對外顯示出了一致性,在不影響調(diào)用關(guān)系的情況下。節(jié)省了系統(tǒng)的性能消耗,能提高應(yīng)用的流暢性。
用類來表示
Command 模式 (命令模式)
命令也是類
通常我們所說的命令都是實例的方法,雖然調(diào)用的結(jié)果會在實例的狀態(tài)中得到反饋,但是卻無法留下調(diào)用的歷史記錄。當(dāng)我們想要把每一次調(diào)用都記錄下來時,我們可以把類當(dāng)作命令來看待,使用類來表示要做的操作。這樣我們管理一系列操作時就是直接管理這些命令類的實例,而不是通過方法進行動態(tài)操作了。
那我們該如何進行設(shè)計以實現(xiàn)Command 模式呢,一樣,我們舉一個例子。比如我們實現(xiàn)一個和Flyweight 模式(上上個模式)一樣的功能,通過素材打印圖片,這里我們再為它添加一些新的功能,并進行優(yōu)化。
- 如果素材進行排列的時候,不是按照順序,而是有各自的坐標(biāo)
- 并且每添加一個素材我們就立即打印出來。
現(xiàn)在我們把每次添加一個素材的操作不在看做是一個方法里面的循環(huán)執(zhí)行,而是一個個命令。我們需要一個接口Command (interface)表示什么是命令,命令很簡單,只需要能執(zhí)行就 OK 了。 每次繪制素材的操作用DrawCommand來表示,它繼承于Command。
有時我們可能需要執(zhí)行一系列的操作,所以我們需要一個表示操作集合的類MacroCommand,它同樣也繼承于Command,在MacroCommand 中有添加和移除Command 的命令,同樣有保存所有操作的屬性commands。
有了命令,但是命令本身不執(zhí)行具體的繪制操作,它僅僅是提供操作的具體數(shù)據(jù)。我們還需要一個繪制類,這個繪制類我們不具體創(chuàng)建,而是通過一個接口Drawable 來表示, Drawable 需要實現(xiàn)繪制方法draw()。為什么這樣設(shè)計,如果你看了以上的設(shè)計模式,我想你應(yīng)該已經(jīng)很清楚了。使用接口,能方便的替換繪制實現(xiàn),也為你要繪制不同的東西提供了擴展的可能性,并且不影響其他代碼的結(jié)構(gòu),這就是代碼的可替換性。
這里我們用ImageDrawCanvas 來表示一個簡單的繪制圖片的圖層。下面是偽代碼的實現(xiàn)
// 命令接口,只定義了 excute
protocol Command {
open func excute()
}
// DrawCommand 表示繪制命令的類
class DrawCommand: Command {
var url: String // 圖片地址
var position: Point // 圖片位置
var drawable: Drawable // 執(zhí)行繪制操作的圖層,并未指定具體的類型,而是接口 Drawable
// 初始化一個命令
init(url: String, position: Point, drawable: Drawable) {
self.url = url
self.position = position
self.drawable = drawable
}
func excute() { // 執(zhí)行繪制命令
self.drawable.draw(url: self.url, position: self.position)
}
}
// MacroCommand 一個命令集合
class MacroCommand: Command {
var commands: [Command] // 所有的命令,只要是`Command` 就可以,這意味著不但可以添加`DrawCommand`還可以添加`MacroCommand`,命令集合在本質(zhì)上還是命令。
func addCommand(command: Command) { // 添加一個命令
if command != self { // 不能添加添加自己,防止死循環(huán)
self.commands.append(command)
}
}
func undo() { // 移除最后一個命令
self.commands.removeLast();
}
func clear() { // 移除所有的命令
self.commands.removeAll()
}
func excute() { // 執(zhí)行命令
for command in self.commands { // 遍歷執(zhí)行所有的命令
command.excute()
}
}
}
// 繪制接口
protocol Drawable {
func draw(url:String, position: Point)
}
// 圖片繪制類
class ImageDrawCanvas: Drawable {
var history: MacroCommand // 繪制的命令歷史,當(dāng)你需要重新繪制的時候,可以直接調(diào)用
var size: Size // 畫布大小
init(size: Size, history: MacroCommand) {
self.size = size
self.history = history
}
func draw(url: String, position: Point) {
... // 根據(jù)圖片的地址和坐標(biāo),進行圖片的繪制
}
func redo() { //重新繪制
self.history.excute()
}
}
// 所有的類都準(zhǔn)備好了,我們來看一下如何操作
func main { var history: MacroCommand = MacroCommand()
lazy var imageCanvas: ImageDrawCanvas {
return ImageDrawCanvas(Size(width: 1000, height: 1000), self.history)c
}
func viewDidload() {
super.viewDidLoad()
for i in 0...100 { // 循環(huán)添加100個素材
let command = DrawCommand(url: "http://www.ssbun.com/12.png", position: Point(x: i, y: i), self.imageCanvas)
command.excute() // 執(zhí)行繪制
self.history.addCommand(command) // 加入到歷史記錄中
}
}
}
以上偽實現(xiàn)了一個Commnad 模式的圖片繪制功能,不過Command 模式的主要實現(xiàn)就是這樣的。通過具象一個操作為一個實例,我們能精準(zhǔn)的操控每一個操作,并重復(fù)任意的步驟。我們還可以將這些實例進行歸檔處理,永久保存我們的操作記錄。在我們了解的以上所有的設(shè)計模式,除了本文的把類作為命令,還有State 模式中的把類作為狀態(tài)。 以后再遇到操作是需要在實例的方法內(nèi)進行很多的判斷和選擇,你可以試著將不同的情況拆分為不同的類來實現(xiàn),或許會豁然開朗。
Interpreter 模式 (翻譯模式)
語法規(guī)則也是類
又多了一個用類來替換某些東西的類,而這次,我們這模擬的是語法。在某些特殊的情況下,我們可能想要設(shè)計一種新的迷你語言來方便的編寫繁瑣的操作。例如正則表達式就可以通過簡短的語法來描述復(fù)雜的篩選條件。我們也可以設(shè)計一款小語言來這樣做,再編寫一個翻譯程序將它翻譯成你所使用的語言。而其中的各種語法可以被翻譯為不同的類,比如Add 作為 +, CommandList 作為 repeat 等等。但是,這個過程還是很麻煩的,這里的篇幅已經(jīng)很長了。而敘述一個迷你語言,或許需要更大的篇幅才能講明白,而這篇文章只是想要使用簡單的文字來幫助你了解所謂的23設(shè)計模式。
結(jié)語
終于看完了所有的23種設(shè)計模式,其實很多的設(shè)計模式已經(jīng)不知不覺中被我們使用了無數(shù)次了。對于經(jīng)驗豐富的程序員而言,設(shè)計模式中的方法在他們看來是理所應(yīng)當(dāng)?shù)?。畢竟,設(shè)計模式本身就是對前輩們經(jīng)驗的總結(jié),本身并沒有什么突出的特點。它也不能幫你解決所有的問題,但是通過了解設(shè)計模式,我們可以更快的學(xué)習(xí)到前輩的經(jīng)驗。在實際的使用中,對我們的幫助是顯而易見的。
設(shè)計模式雖然很重要,但是你卻不用想著把它們都記在自己的腦海中。死記硬背從來都不是好方法,你只要有些許的印象,知道遇見這樣的問題時該使用什么樣的模式,隨后再去查詢具體的資料就是行了,善用搜索引擎可是程序員最重要的一項技能。
說了那么多,最后再說點我的感悟。
語言技巧很多,黑魔法很多,設(shè)計思想也很多,學(xué)完所有為大家所稱贊的思想和技巧,也并不能讓你的項目看起來更完美。遇見問題時,越是簡單的實現(xiàn)就越有可能解決問題,也更容易被人看懂。讓程序看起來簡單,而不是讓它看起來 NB。有一句話說的好 “要讓程序看起來明顯沒有問題,而不是沒有明顯的問題。”