Iterator模式 (迭代器)
一個(gè)一個(gè)遍歷
一個(gè)集合類可以遵守 Iterator 協(xié)議,并實(shí)現(xiàn)一個(gè) Iterator,一般包含 next()方法來(lái)獲取下一個(gè)值,借此來(lái)實(shí)現(xiàn)數(shù)據(jù)的遍歷。我們來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的數(shù)組的迭代器例子:
protocol IteratorProtocol {
associatedtype Element // 關(guān)聯(lián)類型
func next() -> Self.Element? // 返回下一個(gè)數(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
}
}
}
可以將遍歷和實(shí)現(xiàn)分離開來(lái),如果以后集合類型發(fā)生改變,只要同樣為其實(shí)現(xiàn)了 Iteraltor協(xié)議的方法,就無(wú)需大批量的修改代碼。
Adapter模式 (適配器)
加個(gè)適配器以便復(fù)用
就像要將一個(gè)220v的電源,轉(zhuǎn)換為10v的輸出一樣,我們需要一個(gè)適配器來(lái)進(jìn)行輸入和輸出的轉(zhuǎn)換。如果一個(gè)類Number
有createNumber()的方法,而有一個(gè)協(xié)議Print
需要實(shí)現(xiàn)一個(gè)printNumber() 的方法,我們可以通過(guò)一個(gè)中間類NumberPrint
繼承至Number
并實(shí)現(xiàn)Print
協(xié)議,這樣的話我們就可以使用 createNumber()的方法,來(lái)實(shí)現(xiàn)printNumber()方法。這無(wú)疑是提高了代碼的復(fù)用,并且降低了代碼的耦合度,我們不需要修改原來(lái)的類型就能夠進(jìn)行新的擴(kuò)展。這種方法叫做類適配器模式
。
class Number {
func createNumber() -> Int { // 生成一個(gè)隨機(jī)數(shù)
return arc4random() % 999
}
}
protocol Print {
// 需要打印出一個(gè)數(shù)字,這里舉例只是打印數(shù)字。實(shí)際項(xiàng)目中可能需要的是一個(gè)復(fù)雜的操作
func printNumber()
}
// 一個(gè)繼承于`Number`并遵守`?Print`的適配器類,它可以借助父類的方法來(lái)實(shí)現(xiàn)協(xié)議的方法
class NumberPrint: Number, Print {
func printNumber() {
print(super.createNumber()) // 借助父類的方法實(shí)現(xiàn),來(lái)實(shí)現(xiàn)協(xié)議的功能
}
}
還有一種模式是對(duì)象適配器模式
,如果需要轉(zhuǎn)換的不是協(xié)議Print
,而是類Print
。對(duì)于無(wú)法進(jìn)行多繼承的語(yǔ)言來(lái)說(shuō),無(wú)法創(chuàng)建一個(gè)中間類NumberPrint
同時(shí)繼承兩個(gè)類。
這是我們需要進(jìn)行一些轉(zhuǎn)變,我們可以創(chuàng)建一個(gè)繼承于Print
的類 NumberPrint
,而NumberPrint
中有一個(gè)屬性是 Number
的實(shí)例變量,我們可以通過(guò)調(diào)用實(shí)例變量的對(duì)象方法來(lái)實(shí)現(xiàn)Print
中的 printNumber()方法。
// 此時(shí)的 Print 是一個(gè)類
class NumberPrint: Print {
var number: Number = Number()
// 重寫父類的方法,利用屬性`number` 來(lái)實(shí)現(xiàn)父類的方法
func printNumber() {
let n = self.number.createNumber()
print(n)
}
}
當(dāng)需要擴(kuò)展類的功能時(shí),無(wú)需對(duì)現(xiàn)有的類進(jìn)行修改。有時(shí)一個(gè)類已經(jīng)被多地方復(fù)用,并確認(rèn)可靠,如果冒然進(jìn)行修改可能會(huì)出現(xiàn)意想不到的錯(cuò)誤,而且又要進(jìn)行一輪新的測(cè)試,所以可以使用適配器進(jìn)行處理。
還有當(dāng)進(jìn)行版本更新的時(shí)候,可能會(huì)有版本適配的要求,這個(gè)時(shí)候如何對(duì)舊的代碼進(jìn)行適配就很重要了,我們可以通過(guò)適配器模式,對(duì)新的功能進(jìn)行轉(zhuǎn)換,而保留舊的接口。
交給子類
Template Method模式(模板模式)
將具體的實(shí)現(xiàn)交給子類
如果一個(gè)類的邏輯代碼在父類中,而其具體的方法需要子類來(lái)實(shí)現(xiàn),我們就可以稱之為模板模式。其實(shí)父類就是一個(gè)抽象類,比如我們定義一個(gè)類Person
,它實(shí)現(xiàn)了eat()、run()和sleep()方法。我們可以在父類中定義它的執(zhí)行順序。但是具體的方法是如何吃、如何跑、如何睡覺的我們要交給子類來(lái)實(shí)現(xiàn),畢竟Child
(小孩)和Adult
(成人)的習(xí)慣是不同的,不是嗎?
使用模板模式,當(dāng)我們遇到了執(zhí)行邏輯的改變時(shí),我們不需要去修改各個(gè)子類,我們只需要修改抽象類就行了。并且無(wú)論是任何子類都可以被父類執(zhí)行。
class Person {
// 控制人的行為,先吃放后跑步,最后睡覺
func action() { eat()
run()
sleep()
}
// 要交給子類實(shí)現(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")
}
}
上面的例子中我們使用一個(gè)抽象類來(lái)描述Person
。你也可以使用協(xié)議Protocol
來(lái)達(dá)到同樣的目的,不同的操作實(shí)現(xiàn),相同的操作步驟。
我們這里將邏輯代碼放到了父類中,把具體實(shí)現(xiàn)放到了子類中,但是實(shí)際使用時(shí),如何分配父類和子類之間的代碼的處理級(jí)別就需要大家們自己斟酌了,如果父類中實(shí)現(xiàn)的太多,就失去了模板的意義,降低了父類的靈活性。但是父類實(shí)現(xiàn)的太少也會(huì)導(dǎo)致子類中的代碼重復(fù),所以一切看大家的感覺了。
Factory Method模式(工廠模式)
將實(shí)例的生成交給子類
什么是工廠模式呢?通過(guò)一個(gè)Factory 生成一個(gè) Product, 而 Factory和 Product 的實(shí)現(xiàn)是由子類來(lái)實(shí)現(xiàn)的,使用了模板模式。所以你可以定制自己的工廠生產(chǎn)出你想要的實(shí)例。 舉個(gè)例子來(lái)說(shuō),我們把一種面點(diǎn)機(jī)器
作為Factory
,而生成的面點(diǎn)是類Product
。面點(diǎn)機(jī)器生成面點(diǎn)用方法create()
,而面點(diǎn)可以被吃掉有方法eat()
。
protocol Factory { // 定義了一個(gè)工廠
func create() -> Product
}
protocol Product { // 定義了一個(gè)產(chǎn)品
func eat()
}
但是我們并沒有定義機(jī)器如何生產(chǎn)面點(diǎn)以及面點(diǎn)該如何被吃掉,這個(gè)時(shí)候我們創(chuàng)建類Dumpling Machine
(餃子機(jī)器)繼承于Factory
并實(shí)現(xiàn)方法create()
.
接下來(lái)我們實(shí)現(xiàn)類Dumpling
繼承至Product
并實(shí)現(xiàn)了方法eat()
。然后我們就可以通過(guò)DumplingMachine
的create()
來(lái)生成Dumpling
的實(shí)例了。
class DumplingMachine: Factory {
func create() -> Dumpling {
return Dumpling()
}
}
class Dumpling: Product {
func eat() {
... // 實(shí)現(xiàn)怎么吃餃子
}
}
這就是工廠模式的使用,那我們什么時(shí)候使用工廠模式呢。它又有什么好處呢?
工廠模式將框架
和具體實(shí)現(xiàn)
分離開來(lái)了,當(dāng)我們實(shí)現(xiàn)自己的框架(Factory
,Product
)時(shí),我們直接定義相應(yīng)的方法邏輯和屬性結(jié)構(gòu),抽象的描述框架的行為。而開發(fā)者可以通過(guò)框架來(lái)實(shí)現(xiàn)自己的工廠和產(chǎn)品。當(dāng)我們使用工廠方法生成實(shí)例時(shí),我們不需要考慮框架內(nèi)部的實(shí)現(xiàn),只需要實(shí)現(xiàn)預(yù)先約定的方法和屬性就OK了。
例如Object-C
中的NSNumber
就使用了工廠模式
。 我們可以通過(guò)NSNumber
生成不同的數(shù)值類型。
使用工廠模式的時(shí)候,生成實(shí)例我們有幾個(gè)實(shí)現(xiàn)的方法。<1> 指定其為抽象方法,這樣如何子類不實(shí)現(xiàn)的話,編譯器就會(huì)報(bào)錯(cuò)。如果語(yǔ)法不支持定義抽象方法,這種方法就無(wú)法使用了。<2>為其實(shí)現(xiàn)默認(rèn)的實(shí)現(xiàn),當(dāng)子類沒有繼承父類的方法時(shí)。我們可以默認(rèn)實(shí)現(xiàn)此過(guò)程,不過(guò)我不推薦使用這種方法,因?yàn)橥J(rèn)的實(shí)現(xiàn)都是不符合實(shí)際需要的,如果是忘了子類實(shí)現(xiàn)也無(wú)法通過(guò)編譯器來(lái)提醒。<3>在父類的實(shí)現(xiàn)里,拋出異常。并提示用戶必須實(shí)現(xiàn)子類的方法。這樣如果用戶忘記在子類中實(shí)現(xiàn)這個(gè)方法,就會(huì)拋出異樣,防止進(jìn)一步的錯(cuò)誤方法,也能夠提示用戶的錯(cuò)誤發(fā)生在哪里。
生成實(shí)例
Singleton 模式(單例模式)
只有一個(gè)實(shí)例
當(dāng)你想要保證在任何情況下都只有一個(gè)實(shí)例,程序?qū)ν庖脖憩F(xiàn)出只有一個(gè)實(shí)例的時(shí)候,你就可以選擇使用單例模式來(lái)實(shí)現(xiàn)你的類。
單例模式,顧名思義就是這個(gè)類,只會(huì)生成一個(gè)實(shí)例變量。當(dāng)你想要新實(shí)例化一個(gè)類的時(shí)候,它要不返回給你唯一的實(shí)例,要不就拋出異常。通常一個(gè)單例模式的類,都只有一個(gè)類似于shareInstance()
的類方法
用來(lái)獲取唯一的實(shí)例。具體的實(shí)現(xiàn)方法,根據(jù)各個(gè)語(yǔ)言的不同而做改變。
基本上,就是在類中創(chuàng)建一個(gè)實(shí)例變量,這個(gè)實(shí)例變量一旦被初始化就無(wú)法被改變和銷毀。而獲取實(shí)例的方法,總是返回這是實(shí)例就 ok 了。
在某些情況下,類似于程序的窗口,一定是只有一個(gè)的。這個(gè)時(shí)候,為了方便管理這個(gè)窗口,我們就可以實(shí)現(xiàn)一個(gè)單例來(lái)處理具體的事物。亦或是在程序啟動(dòng)以后需要,一個(gè)始終純?cè)诘念悓?shí)例,來(lái)進(jìn)行公共數(shù)據(jù)的處理和傳遞。
在 GUI 上也有可以用到的地方,比如一個(gè)界面上,只能同時(shí)出現(xiàn)一個(gè)的彈窗。你不必在顯示一個(gè)的時(shí)候,去關(guān)閉另一個(gè),你只需要在這個(gè)地方顯示一個(gè)彈窗,另一個(gè)就會(huì)消失,畢竟只有一個(gè)實(shí)例。(你可以不必在其他的類中持有此實(shí)例,在想用的時(shí)候,直接獲取單例就 OK 了)
Prototype 模式(原型模式)
通過(guò)復(fù)制生成實(shí)例
一般情況下,我們?cè)谏梢粋€(gè)實(shí)例的時(shí)候。都是使用初始化方法,根據(jù)一個(gè)類來(lái)生成一個(gè)實(shí)例。當(dāng)時(shí)在某些情況下,我們可能并不想根據(jù)一個(gè)類來(lái)實(shí)例化一個(gè)對(duì)象,這個(gè)時(shí)候我們可以通過(guò)一個(gè)實(shí)例來(lái)生成另一個(gè)實(shí)例,這種復(fù)制的方式,我們就稱之為 Prototype 模式
。
什么情況下我們可以選擇使用 prototype 模式
呢?大概有以下三種情況。
- 當(dāng)對(duì)象功能相近而種類又太多的時(shí)候。如果使用類的話,會(huì)創(chuàng)建很多的類。如果是在一個(gè)類中,創(chuàng)建不同功能的實(shí)例,然后通過(guò)實(shí)例復(fù)制來(lái)進(jìn)行后續(xù)的對(duì)象生成。(有一種把實(shí)例當(dāng)類用的感覺...)
- 太過(guò)復(fù)雜,無(wú)法通過(guò)類來(lái)生成實(shí)例。比如一個(gè)畫板上筆的運(yùn)動(dòng)軌跡,通過(guò)一個(gè)對(duì)象來(lái)記錄。在另一個(gè)地方要生成一個(gè)和這個(gè)筆的運(yùn)動(dòng)軌跡完全一樣的對(duì)象時(shí),你很難通過(guò)一個(gè)類來(lái)實(shí)例化出這個(gè)對(duì)象。而對(duì)這個(gè)對(duì)象的復(fù)制就能夠很容易的做到。
- 寫框架時(shí),想要將類和實(shí)例解耦。這個(gè)在實(shí)現(xiàn)的過(guò)程中,你就可要體會(huì)到,它的復(fù)用性很高,類之間是沒有耦合的。
實(shí)現(xiàn)的過(guò)程大概如此: 我們通過(guò)創(chuàng)建一個(gè)類Manager
,并實(shí)現(xiàn)register()
和createClone()
方法,用來(lái)注冊(cè)和生成實(shí)例。然后是協(xié)議Product
,它定義了方法use()
和copySelf()
。繼承此協(xié)議的類需要實(shí)現(xiàn)use()
來(lái)實(shí)現(xiàn)如何使用執(zhí)行,copySelf()
則是復(fù)制自己以生成新的實(shí)例。
另外則是是具體的類了,我們舉個(gè)例子是類State
,它用來(lái)描述一個(gè)人身體的狀況,State
需要遵守協(xié)議Product
并實(shí)現(xiàn)方法use()
和copySelf()
。我們生成一個(gè)一個(gè)state
實(shí)例,并使用Manager
來(lái)注冊(cè)此實(shí)例,然后可以通過(guò) createClone()
來(lái)進(jìn)行復(fù)制,下面是簡(jiǎn)化的偽代碼
這里我們可以把Manager
想象為一個(gè)庫(kù)房,因?yàn)橥ㄟ^(guò)實(shí)例來(lái)生成實(shí)例,畢竟要有一個(gè)母體
。而這個(gè)母體不能像是一個(gè)類一樣隨時(shí)可以調(diào)用,所以我們需要把它放在一個(gè)地方,在我們想用的時(shí)候,隨時(shí)可以使用,并且防止母體
被意外修改,Manager
只是提供了復(fù)制的方法,你不能獲取和修改母體
。
protocol Product {
func copySelf() -> Product
func use()
}
class Manager { // 用來(lái)管理可以自我復(fù)制的實(shí)例
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ù)雜的實(shí)例
有時(shí)候,當(dāng)我們?cè)跇?gòu)建一個(gè)復(fù)雜的模塊的時(shí)候,我們需要將其拆分出來(lái)。形成一個(gè)個(gè)小的組件,然后通過(guò)一個(gè)管理類,進(jìn)行重新組合。這樣的話我們可以最大限度的提供代碼的復(fù)用性及可替代性。
Builder 的思路十分的簡(jiǎn)潔,主要分為Director(管理者), Builder(構(gòu)建器)和 Buinder 的子類。Director 通過(guò)一個(gè) Builder 的實(shí)例來(lái)生成所需要的數(shù)據(jù),而數(shù)據(jù)的具體實(shí)現(xiàn)方式,則是通過(guò)子類來(lái)實(shí)現(xiàn)的。Builder 中應(yīng)當(dāng)涵蓋構(gòu)建數(shù)據(jù)所需要的所有必要方法,但是不應(yīng)當(dāng)含有特別的方法。這樣 Director 可以通過(guò)一個(gè) Builder 來(lái)實(shí)現(xiàn)功能。
而具體的實(shí)現(xiàn)方式,它就不知道了,它只是知道其調(diào)用了一個(gè) Builder,但是 Builder 有很多,它并不知道調(diào)用的是哪一個(gè) Builder。這種不知道則提高了模式的靈活,只有不知道,才能夠被替換。
protocol Builder {
func playVideo()
}
class Director {
var builder: Builder? // 一個(gè)可以播放視頻的構(gòu)建器
func playVideo() { // 管理者想要實(shí)現(xiàn)播放視頻的功能
builder?.playVideo()
}
}
class Mp4Player: Builder { //實(shí)現(xiàn)一個(gè) mp4播放器
func playVideo() {
self.playWithMp4()
}
func playWithMp4() {
... // 特有方法,以 MP4格式播放
}
}
class AviPlayer: Builder { // 實(shí)現(xiàn)一個(gè) Avi 播放器
func playVideo() {
self.playWithAvi()
}
func playWithAvi() {
... // 特有方法,以 Avi格式播放
}
}
// 實(shí)際的使用中,你可以為 Director 提供 mp4或是 avi 播放器
// 只要符合`Builder`標(biāo)準(zhǔn), 你可以隨意替換 builder 及其內(nèi)部的實(shí)現(xiàn)
// 而不影響其他的代碼
main {
let player = Director()
player.builder = Mp4Player()
// palyer.builder = AviPlayer()
}
到現(xiàn)在為止,大家們對(duì)抽象這個(gè)概念應(yīng)該都很了解了,抽象可以是
抽象類
也可以是接口
,抽象的目的就是隱藏實(shí)現(xiàn)
而突出邏輯
。將邏輯和實(shí)現(xiàn)分開是實(shí)現(xiàn)代碼復(fù)用和提高維護(hù)性減少耦合常用的方法。以后如果提到抽象,希望大家都能理解它的含義。
Abstract Factory 模式 (抽象工廠模式)
將關(guān)聯(lián)的零件組裝成產(chǎn)品
抽象工廠
的作用就是將抽象零件
加工成抽象產(chǎn)品
。直接理解的話可能不是特別容易懂,我們直接舉一個(gè)例子,就大概明白它的意思了。
我們的抽象工廠
就是一個(gè)生產(chǎn)抽象產(chǎn)品
電子板的機(jī)器,這個(gè)電子板上有很多的電容、電阻和各種各樣的元件(抽象元件
)。這里我們并不知道電子板的大小和所需元件的參數(shù)和數(shù)量。那就意味著我們可以通過(guò)實(shí)現(xiàn)不同的電子板子類(抽象產(chǎn)品的子類)來(lái)生產(chǎn)不同的產(chǎn)品
。而抽象元件
只要符合對(duì)應(yīng)的參數(shù),我們可以使用任意廠商的元件(抽象零件的子類)來(lái)使用。產(chǎn)品模型有了,元件也有了,那么實(shí)現(xiàn)一個(gè)具體的工廠來(lái)生產(chǎn)特定的產(chǎn)品是很重要的,不可能一個(gè)工廠可以生產(chǎn)任何產(chǎn)品吧,我們也可以通過(guò)修改工廠實(shí)例來(lái)優(yōu)化生產(chǎn)的工藝和流程。
這樣我們就實(shí)現(xiàn)了一個(gè)可以生產(chǎn)各式各樣產(chǎn)品的生產(chǎn)線。當(dāng)需要修改的時(shí)候,我們不用替換很多的數(shù)據(jù),只要將特定的子類替換掉就可以實(shí)現(xiàn)產(chǎn)品線的跟新,是不是和現(xiàn)在的代工廠一模一樣。
你會(huì)發(fā)現(xiàn)大部分的設(shè)計(jì)模式都要牽扯到抽象概念(接口)。這是很多模式優(yōu)化的基礎(chǔ)。如果你知道面對(duì)對(duì)象編程和函數(shù)響應(yīng)式編程等等,那你肯定對(duì)面對(duì)接口編程也有所耳聞,Swfit 相比 OC 就大量的使用了面向接口編程。這種編程方式的靈活性很高,如果大家感興趣,可以去多了解一下
分開考慮
Bridge 模式 (橋接模式)
將類的功能層次結(jié)構(gòu)和實(shí)現(xiàn)層次結(jié)構(gòu)分類開來(lái)
為了了解我們是為了橋接誰(shuí)和誰(shuí),我們需要先來(lái)了解一下什么是類的功能層次和類的實(shí)現(xiàn)層次:
- 類的功能層次: 功能層次其實(shí)就是實(shí)現(xiàn)一個(gè)類的子類,當(dāng)你需要給一個(gè)類添加新的功能的時(shí)候,我們可以通過(guò)實(shí)現(xiàn)一個(gè)子類來(lái)完成。隨著功能的增多,我們可以實(shí)現(xiàn)一個(gè)又一個(gè)子類,并不斷的加深這個(gè)結(jié)構(gòu),這就是類的功能層次。(類的功能層次過(guò)多是不好的設(shè)計(jì))就行下面這樣:
- Person
- Men
- Boy
- 類的實(shí)現(xiàn)層次: 類的實(shí)現(xiàn)層次則是抽象類的實(shí)現(xiàn),當(dāng)我們需要改變一個(gè)類的方法實(shí)現(xiàn)方式的時(shí)候,我們只需一個(gè)繼承抽象類的子類就行了,我們并不是為了給父類中添加其沒有的新功能,我們只是為原功能提供了不同的實(shí)現(xiàn)而已。
- Display
- StringDisplay
- HtmlDisplay
在實(shí)際的使用中,我們往往要根據(jù)實(shí)際的需求,靈活的運(yùn)用這兩種結(jié)構(gòu)。如果僅僅是將它們混合在一起使用的話,當(dāng)應(yīng)用變得更為復(fù)雜的時(shí)候,你就很難清楚的認(rèn)識(shí)到,你到底應(yīng)該繼承哪個(gè)類。
所以我們需要將功能層次和實(shí)現(xiàn)層次分離開來(lái)。但前面說(shuō)了,我們要靈活的運(yùn)用兩種層次,那就要讓他們之間有聯(lián)系,這時(shí)我們就需要在它們之間建一條橋梁。
大概的實(shí)現(xiàn)就是下圖這樣,Display 類是功能層次的最高層級(jí),它持有一個(gè) DisplayImp1的實(shí)例,這個(gè)實(shí)例中有與 Display 相對(duì)應(yīng)的功能。DisplayImp1
是一個(gè)抽象類, 而 StringDisplayImp1
繼承了 DisplayImp1
,實(shí)現(xiàn)了所有的方法。
這時(shí)我們可以創(chuàng)建一個(gè) stringDisplayImp1
的實(shí)例,通過(guò)這個(gè)實(shí)例來(lái)創(chuàng)建一個(gè) Display
或是 PhotoDisplay
來(lái)使用。
Strategy 模式 (策略模式)
整體地替換算法
策略在程序中也可以被稱作算法。我們?cè)谔幚沓绦蛑幸恍?fù)雜的關(guān)系時(shí),所使用的算法可能會(huì)根據(jù)軟件的系統(tǒng)、時(shí)間的需求、錯(cuò)誤率及系統(tǒng)硬件機(jī)能等進(jìn)行相應(yīng)的調(diào)整。這是我們要同時(shí)完備幾種算法以便在系統(tǒng)中進(jìn)行替換,不加設(shè)計(jì)的話,替換算法本身也將是一個(gè)麻煩的事情。 Strategy 模式
就可以方便的完整替換整個(gè)算法。
例如我們想要實(shí)現(xiàn)一個(gè)棋類應(yīng)用,單機(jī)模式下我們將會(huì)有一個(gè)AI來(lái)和玩家對(duì)戰(zhàn)。我們定義一個(gè) Player
類作為AI的類。創(chuàng)建一個(gè)Player
需要提供一個(gè)策略,而這個(gè)策略Strategy
是一個(gè)抽象類,它定義了一系列的方法,可以通過(guò)現(xiàn)在棋局的數(shù)據(jù)推算出下一步該往哪走。我們根據(jù)游戲的算法來(lái)制定算法,這個(gè)時(shí)候我們就可以通過(guò)不同的子類策略實(shí)現(xiàn)設(shè)備的適配和 AI 難度的調(diào)節(jié)。
無(wú)論策略發(fā)生了什么改變,我們無(wú)需修改任何的接口,我們只需要替換一個(gè)策略的類,就可以完成整個(gè)算法的替換。
Strategy 模式常用在棋牌類游戲中,而且確實(shí)很實(shí)用。我感覺 Strategy 模式不太像一個(gè)正經(jīng)的設(shè)計(jì)模式,它的概念很簡(jiǎn)單,甚至就是抽象類或接口模式的基礎(chǔ)應(yīng)用而已。我們平時(shí)寫代碼的時(shí)候,多多少少會(huì)用過(guò)或見過(guò)這類用法。
一致性
Composite 模式 (復(fù)合模式)
容器與內(nèi)容一致性
我們平時(shí)使用的電腦、ipad和手機(jī)等電子設(shè)備都有自己的文件管理系統(tǒng)。他們的基本結(jié)構(gòu)就是有一個(gè)根目錄,下屬很多的文件夾和文件。文件夾下面又是文件夾或是文件。我們所看的這種樹狀結(jié)構(gòu)看起來(lái)是由兩種數(shù)據(jù)類型組成的。其實(shí)我們完全可以把它們統(tǒng)一看做為一種目錄條目
。
這種目錄條目
擁有通用的屬性和方法,它們擁有一致的行為。能夠使容器和內(nèi)容具有一致性,創(chuàng)造出遞歸的結(jié)構(gòu)的模式就是 Composite 模式。
Entry 是一個(gè)抽象類,它定義了一個(gè)
條目
。它通過(guò)getName()
來(lái)獲取條目名字、getSize()
獲取條目大小、printList()
是用來(lái)打印內(nèi)容列表,add()
則是提供給子類Directory
來(lái)添加新的File
和Directory
。
File
是文件類,它可以返回文件名、大小和報(bào)告自己的目錄。Directory
是文件夾類,它有名字name
, 還有directories
用來(lái)存儲(chǔ)自己內(nèi)部的文件和文件夾列表,但是它沒有自己的大小,它的大小是通過(guò)內(nèi)容的getSize()
方法相加獲取。
通過(guò)這樣的方式,我們就構(gòu)建了一個(gè)遞歸的文件結(jié)構(gòu),這種結(jié)構(gòu)將內(nèi)部的內(nèi)容和外部的容器統(tǒng)一起來(lái),使對(duì)象的調(diào)用變得更易理解和簡(jiǎn)潔。
Decorator 模式 (裝飾器模式)
裝飾邊框與被裝飾物的一致性
一提到裝飾器,大家肯定都知道裝飾的概念。裝飾器就像你照片的相框,水果蛋糕上的點(diǎn)心一樣,通過(guò)裝飾物使主體[被裝飾物](相片和蛋糕)變得與眾不同。這個(gè)模式的作用也是如此,但是如果只是這樣的話,你很容易把裝飾器看做和主體不同的東西。你的想法大概是這樣的:
你可能以為它們是一被一個(gè)一個(gè)放到被裝飾物上的。這樣的話,你就無(wú)法裝飾裝飾物本身了,整個(gè)模式的擴(kuò)展性就被降低了。我們需要裝飾物和被裝飾物具備一致性
,這樣的話接口就變得透明了起來(lái),無(wú)論我們?nèi)绾螌?duì)被裝飾物進(jìn)行裝飾,我們最后所看到的被裝飾物所體現(xiàn)的接口和行為還是和最初是一樣的。而這樣的形式才是真正的裝飾器模式
,它就像一個(gè)俄羅斯套娃,一層嵌套一層,每層都可以看著一個(gè)包裝或裝飾,直到最后一個(gè)套娃出現(xiàn)。
那我們?nèi)绾螌?shí)現(xiàn)這個(gè)結(jié)構(gòu)呢,它看起來(lái)和 Composite 模式
有些像。我們舉一個(gè)顯示程序的例子,它可以為顯示內(nèi)容添加+、-、* 等字符邊框。
我們需要一個(gè)抽象類 Dispaly
來(lái)描述顯示的流程,通過(guò)一個(gè)子類 StringDisplay
可以顯示一行字符串。接下來(lái)我們定義一個(gè)裝飾器的抽象類Decorator
,它是繼承于Display 的子抽象類。Decorator
中有一個(gè)類型為Display
的成員變量display,它表示被裝飾物。
這樣我們就將裝飾物和被裝飾物統(tǒng)一起來(lái)了,使他們滿足一致性
。它的優(yōu)缺點(diǎn)大概有以下幾點(diǎn)
- 接口的透明性 無(wú)論我們進(jìn)行多少次裝飾,被裝飾物的接口都沒有被隱藏起來(lái),還是和當(dāng)初一樣。
- 可以在不改變裝飾物的前提下去添加功能 當(dāng)我們添加功能的時(shí)候,只需要實(shí)現(xiàn)不同的裝飾器就 OK 了。
- 需要實(shí)現(xiàn)太多的小類 我們需要些很多不同功能的裝飾器,這些類的功能通常不多,但是卻數(shù)量巨大。
訪問數(shù)據(jù)結(jié)構(gòu)
Visitor 模式 (訪問者模式)
訪問數(shù)據(jù)結(jié)構(gòu)并處理數(shù)據(jù)
我們大家都學(xué)過(guò)數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)結(jié)構(gòu)的重要性就不言而喻了。但是此設(shè)計(jì)模式的重點(diǎn)不是如何設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu),而是如何訪問數(shù)據(jù)結(jié)構(gòu)。
一般我們?cè)趯?shí)現(xiàn)了一個(gè)數(shù)據(jù)結(jié)構(gòu)后,都會(huì)將數(shù)據(jù)的操作方法和數(shù)據(jù)結(jié)構(gòu)本身綁定在同一個(gè)類中。使它們成為一個(gè)整體。這樣做在以后使用的時(shí)候會(huì)方便很多,也不用那么多的類進(jìn)行操作。
但是當(dāng)你想要擴(kuò)張數(shù)據(jù)結(jié)構(gòu)的處理方法的時(shí)候,你就需要直接修改數(shù)據(jù)結(jié)構(gòu)的類。這樣做很不方便,既麻煩又不利于項(xiàng)目的穩(wěn)定。
如何解決呢?以我們學(xué)習(xí)了以上那么多設(shè)計(jì)模式的經(jīng)驗(yàn),當(dāng)然是將數(shù)據(jù)結(jié)構(gòu)和訪問操作分離開來(lái)嘍。
在 Visitor 模式
中,Visitor
是一個(gè)訪問者的形象。它是一個(gè)抽象類,內(nèi)部定義了一個(gè) visit()
方法。,以我們?cè)?Composite 模式中使用的文件系統(tǒng)為例,這里的Entry
類同樣代表了數(shù)據(jù)結(jié)構(gòu)的抽象形式,它的具體實(shí)現(xiàn)是由File
和Direcotry
實(shí)現(xiàn)的。
不一樣的地方是,我們這里要定義一個(gè)新的接口 Element
,這個(gè)接口定義了一個(gè)accpet(Visiter)
方法,Entry
遵守這個(gè)接口,而它的子類需要實(shí)現(xiàn)這個(gè)接口的方法。
accept()
方法接受一個(gè)Visitor
實(shí)例,并在 accept()
的方法內(nèi)部調(diào)用Visitor
的visit()
方法,同時(shí)把自己【也就是Entry 本身】傳給這個(gè)方法。這樣在visit()
的內(nèi)部就可以進(jìn)行數(shù)據(jù)的處理了,而細(xì)節(jié)則是由Visitor
的子類所實(shí)現(xiàn)的。
此時(shí),我們就可以通過(guò)實(shí)現(xiàn)不同的Visitor
子類進(jìn)行數(shù)據(jù)訪問方式的擴(kuò)展。
整個(gè)模式的結(jié)構(gòu)就是如此,但是你可能會(huì)為里面 visit()和 accpet()的調(diào)用感到困惑在處理 Directory
的過(guò)程中,我們需要遍歷里面的所以對(duì)象,并一個(gè)個(gè)調(diào)用他們的 accpet()
方法,同時(shí)把Visitor
自己也給傳過(guò)去,然后在各個(gè)Entry
中再次調(diào)用 visit()
方法,進(jìn)行同樣的操作,直到最后一個(gè)文件是File
,遞歸就結(jié)束了。
這里用到了兩個(gè)類的兩個(gè)方法來(lái)進(jìn)行嵌套遞歸,著實(shí)很難讓人理解。一個(gè)遞歸就已經(jīng)讓人頭痛了,這樣的遞歸也主要是為了實(shí)現(xiàn)數(shù)據(jù)處理
和數(shù)據(jù)結(jié)構(gòu)
的分離,并簡(jiǎn)化數(shù)據(jù)的處理過(guò)程。
當(dāng)你實(shí)現(xiàn)了一個(gè)新的Visitor
并通過(guò)一句調(diào)用就可以直接處理一個(gè)數(shù)據(jù)類型,而不用關(guān)心具體的類型時(shí),你就會(huì)感受到它的好處了。
一切的辛苦都是有價(jià)值的。
Chain of Responsibility 模式 (責(zé)任鏈模式)
推卸責(zé)任
看到這個(gè)模式的描述推卸責(zé)任
,是不是感覺有些奇葩。我們生活中,推卸責(zé)任看起來(lái)像是一個(gè)低效的處理問題的方式,那是如何在程序中發(fā)生正向的作用呢?
我們先看一下責(zé)任鏈模式的結(jié)構(gòu):
- 問題: 既然要處理問題,那問題本身就很重要了。我們需要一個(gè)抽象類或是接口來(lái)定義問題。
- 處理問題的抽象類: 問題需要交給一個(gè)類來(lái)處理,而這個(gè)類定義了處理問題的流程,比如判斷自己能否解決,如果能解決就返回結(jié)果。如果不能解決,就自動(dòng)交給下一個(gè)類來(lái)處理,要是沒有下一個(gè)類就返回錯(cuò)誤。
- 具體處理問題的類: 抽象類我們已經(jīng)定義了,但是具體的解決問題的方法,需要不同的子類來(lái)實(shí)現(xiàn)。
比如我們有多種不同等級(jí)的預(yù)警處理方案來(lái)處理一個(gè)警報(bào)。警報(bào)分為藍(lán)、黃、橙和紅四個(gè)級(jí)別。我們定義一個(gè)警報(bào)類
Alert
,它通過(guò)初始化方法init(int)
傳入一個(gè)等級(jí)來(lái)創(chuàng)建。
接下來(lái)我們定義一個(gè)抽象類Handle
來(lái)實(shí)現(xiàn)處理警報(bào)的流程,它有屬性next
(Handle 的實(shí)例)表示要將責(zé)任推給那個(gè)對(duì)象。以及一個(gè)讓子類實(shí)現(xiàn)的抽象方法resolve()
,用來(lái)表示解決問題的具體實(shí)現(xiàn)。
它通過(guò)方法targetr(Alert)
來(lái)處理警報(bào),target
內(nèi)會(huì)調(diào)用resolve()
方法來(lái)處理問題,如果能處理就返回成功,如果無(wú)法處理,就將問題交給next
,接下來(lái)會(huì)調(diào)用next
的target()
方法。直到有方案能處理警報(bào),或是沒有辦法處理,報(bào)告錯(cuò)誤。
現(xiàn)在想一想我們開頭的問題,責(zé)任鏈模式有什么好處呢。
-
我們可以簡(jiǎn)化處理問題的流程,如果不踢皮球的話,我們需要為每個(gè)問題指明對(duì)應(yīng)的處理方法。那
Alert
本身就需要知道自己能被哪個(gè)類處理,這就像你想要解決一個(gè)問題,你未必就一定能找到一個(gè)對(duì)的人。你只是純粹的想解決問題而已。讓問題知道自己該被誰(shuí)解決,就會(huì)讓問題本身變得更加復(fù)雜。 -
可以動(dòng)態(tài)的修改流程,我們的處理順序是?鏈?zhǔn)降模弦粋€(gè)類決定下一個(gè)要處理的類。我們只需要需改一下
next
就能夠輕松的改變處理問題的順序。
使用責(zé)任鏈模式的一個(gè)問題是,會(huì)增加處理問題的時(shí)間,因?yàn)槭且粋€(gè)一個(gè)去判斷能不能解決的。如果問題沒有固定的解決方案,使用
責(zé)任鏈模式
是沒有任何問題的。如果能夠確定問題的處理方式就沒必要這樣了。
簡(jiǎn)單化
Facade 模式 (窗口模式)
簡(jiǎn)單窗口
隨著時(shí)間的腳步,我們的程序會(huì)越來(lái)越完善,同時(shí)也會(huì)變得更加復(fù)雜和冗余。當(dāng)我們實(shí)現(xiàn)新的功能時(shí),我們需要在眾多的類中,找到需要的類,并組織邏輯和順序。特別是在大型程序中,每次調(diào)用都要注意眾多的類之間錯(cuò)中復(fù)雜的關(guān)系。難道我們就不能使用一個(gè)統(tǒng)一的窗口,只需要調(diào)用這個(gè)窗口的方法,我們就可以實(shí)現(xiàn)這個(gè)操作,這個(gè)思想就是Facade 模式
。
例如要實(shí)現(xiàn)一個(gè)發(fā)布模塊,要發(fā)布的內(nèi)容有文字、視頻和圖片。原來(lái)的操作是我們要分別上傳圖片 >> 上傳視頻 >> 處理文字 >> 整理json 數(shù)據(jù) >> 上傳服務(wù)器。 這樣的操作做一次還好,如果有多個(gè)地方需要使用到發(fā)布的功能,這樣就顯得太過(guò)復(fù)雜了,也不利于整合模塊的功能。
我們現(xiàn)在使用 Facade 模式
進(jìn)行改進(jìn),實(shí)現(xiàn)一個(gè)PublishManager
類就是Facade模式
中的窗口,它有一個(gè)方法publish(text, image, video)
可以直接接受文字、圖片和視頻,在PublishManager
內(nèi)部,它可以把文字交給TextHandle
[處理文字的類] 來(lái)處理,把圖片和視頻的上傳交給UploadManager
[進(jìn)行上傳的類],拿到 url 后通過(guò)JSONSerialization
進(jìn)行 JSON 處理。最后通過(guò)HTTPManager
將數(shù)據(jù)傳遞給后臺(tái)。
這樣以后,我們無(wú)論在任何地方需要使用到發(fā)布功能的時(shí)候,我們只需要調(diào)用PublishManager
的發(fā)布方法,就可以直接進(jìn)行發(fā)布,這里我們就實(shí)現(xiàn)了一個(gè)窗口,進(jìn)行發(fā)布的窗口,而復(fù)雜的內(nèi)部調(diào)用,就被我們隱藏起來(lái)了,我們無(wú)需關(guān)心它的內(nèi)部調(diào)用,如果以后需要進(jìn)行修改我們可以直接修改PublishManager
而不用再調(diào)整其他的地方,使得發(fā)布的功能變得更加純粹。
Mediator 模式 (中介模式、仲裁者模式)
只有一個(gè)仲裁者
如果你要編寫一個(gè)聯(lián)機(jī)的棋類游戲,同時(shí)有4名玩家進(jìn)行對(duì)戰(zhàn),每人一步,通過(guò)某個(gè)規(guī)則可以吃掉別人棋子。我們?cè)撊绾瓮礁鱾€(gè)玩家的棋盤和管理各個(gè)玩家的狀態(tài)呢。
如果我們每個(gè)玩家的終端,各自控制自己的狀態(tài)而后將數(shù)據(jù)發(fā)送到其他的終端。那每個(gè)終端都要處理其他終端發(fā)送過(guò)來(lái)的數(shù)據(jù),而后同步自己的狀態(tài)。
這時(shí)每個(gè)終端都有一份自己的數(shù)據(jù),處理的邏輯隨著玩家的個(gè)數(shù)增加也會(huì)變得更加復(fù)雜。并且一旦一個(gè)玩家的數(shù)據(jù)出錯(cuò),他會(huì)把錯(cuò)誤的數(shù)據(jù)發(fā)送給其他的終端,這時(shí)雙方的數(shù)據(jù)會(huì)發(fā)生沖突而產(chǎn)生致命錯(cuò)誤。
而今天我們將通過(guò) Mediator 模式
來(lái)解決這個(gè)問題。我們通過(guò)一個(gè)仲裁者,你可以把它作為游戲的一個(gè)中間服務(wù)器。玩家的每個(gè)終端都只是接收仲裁者發(fā)來(lái)的屬于自己的數(shù)據(jù)并進(jìn)行狀態(tài)的更新,而自己的每一步操作就只是傳遞給仲裁者。仲裁者進(jìn)行數(shù)據(jù)的處理后,再通知所有的終端分別更新狀態(tài),這樣一來(lái)各個(gè)終端的操作實(shí)時(shí)匯集到仲裁者,而仲裁者再實(shí)時(shí)進(jìn)行數(shù)據(jù)分發(fā)。
這樣做就不會(huì)出現(xiàn)數(shù)據(jù)不同步的狀況了,而數(shù)據(jù)的處理集中到了一點(diǎn),降低了出現(xiàn) bug 的概率。即使出現(xiàn)了問題也容易排查 bug 發(fā)生在哪里。
除了上述的使用情景以外,我們?cè)陧?xiàng)目當(dāng)中處理 GUI 的點(diǎn)擊、界面和操作邏輯管理時(shí),也可以使用Mediator 模式
。 我們創(chuàng)建一個(gè)抽象類類Manager
作為 Mediator,再創(chuàng)建一個(gè)接口Colleague
, 表示和Manager
連接的各個(gè)控件。Manager
定義了各種各樣設(shè)置Colleague
的方法和方法didChange( Colleague )
來(lái)告知Manager
哪個(gè)控件發(fā)生了改變。我們實(shí)例化Manager
的一個(gè)子類,將其傳遞給各個(gè)控件[實(shí)現(xiàn)接口 Colleague],當(dāng)控件發(fā)生狀態(tài)變更時(shí)就傳遞給這個(gè)仲裁者,而后仲裁者進(jìn)行處理后,通過(guò)各個(gè)設(shè)置Colleague
的方法進(jìn)行控件狀態(tài)的更新。
開到這里我們就能發(fā)現(xiàn),
Mediator 模式
是一種雙向綁定機(jī)制。只不過(guò)是各個(gè)對(duì)象都綁定同一個(gè)仲裁者,而后通過(guò)與它進(jìn)行通信借以實(shí)現(xiàn)與其他的對(duì)象進(jìn)行通信的目的。
管理狀態(tài)
Observer 模式 [觀察者模式]
發(fā)送狀態(tài)變化通知
說(shuō)到觀察者模式,我想大家都應(yīng)該有所了解。很多語(yǔ)言中都有Observer 模式
的設(shè)計(jì),雖然各種各樣的實(shí)現(xiàn)各有區(qū)別,但都是以Observer 觀察被觀察者,當(dāng)被觀察者發(fā)生改變時(shí),通知 Observer 發(fā)生了什么改變為目的。
我們現(xiàn)在來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單化的觀察者模式,我們創(chuàng)建一個(gè)抽象類NumberGenerator
, 再創(chuàng)建一個(gè)RandomNumberGenerator
繼承自NumberGenerator
,
class NumberGenerator {
var value: Int = 0 // 在這里簡(jiǎn)單的表示為自己的值
public var observers: [Observer] = [] // 儲(chǔ)存所有的觀察者
func addObserver(ob: Observer) {...} // 添加觀察者
func deleteObserver(ob: Observer) {...} // 刪除觀察者
func notifyObserver() {...} // 通過(guò)所有的觀察者,數(shù)據(jù)發(fā)生改變
func excute() {...} // 執(zhí)行數(shù)據(jù)跟新
func getNumber() { FatalError() }// 交給子類實(shí)現(xiàn),實(shí)現(xiàn)數(shù)值如何生成
}
class RandomNumberGenerator: NumberGenerator {
func getNumber() {...} // 返回一個(gè)隨機(jī)數(shù)
}
接下來(lái)就是創(chuàng)建一個(gè)接口Observer
,只有實(shí)現(xiàn)了此接口的類才能成為 NumberGenerator
的觀察者。它只有一個(gè) update
方法 [在 swfit 中接口相當(dāng)于協(xié)議]
protocol Observer {
func update(obj: NumberGenerator)
}
class Display: Observer {
func update(obj: NumberGenerator) {
print(obj.value)
}
}
我們通過(guò)Dispaly
的實(shí)現(xiàn),將每次訂閱到的值顯示出來(lái)。下面是一個(gè)簡(jiǎn)單的使用
let generator = RandomNumberGenerator()
let observer = Display()
generator.addObserver(observer)
generator.excute()
// print: 2 打印出一個(gè)隨機(jī)數(shù)
以上就是一個(gè)簡(jiǎn)單化的Observer 模式
的使用,如果細(xì)心的話你會(huì)看到,我們直接將被觀察者本身返回給了觀察者。一個(gè)對(duì)象可以同時(shí)被很多觀察者觀察,但是觀察者想要獲取的信息可能各有不同,所以直接將自身傳遞,讓觀察者自己去查找。
當(dāng)然了,這是由于我們的設(shè)計(jì)過(guò)于簡(jiǎn)陋。在 Objective-C 中,我們可以直接監(jiān)聽各個(gè)對(duì)象的屬性。
其實(shí),觀察者模式,我們也可以稱為訂閱模式。觀察者并不是去主動(dòng)觀察,而是被觀察者通知觀察者的。如果理解為發(fā)布和訂閱就更加契合了,你可以訂閱一個(gè)對(duì)象,如果他發(fā)布了新的內(nèi)容,你就會(huì)得到通知。
到這里,如果你上面的各種模式都了解了一遍的話,你就會(huì)發(fā)現(xiàn),在很多模式中已經(jīng)出現(xiàn)了很多的這種可替換性設(shè)計(jì)了。通常進(jìn)行替換性設(shè)計(jì),可以提高系統(tǒng)的靈活性和降低耦合性。一般我們通過(guò)以下兩者方式進(jìn)行替換性設(shè)計(jì)。
- 利用抽象類和接口從具體類中提取出抽象方法
- 在將實(shí)例作為參數(shù)傳遞至類中,或是在類的字段中保存實(shí)例時(shí),不使用具體的類型,而是使用抽象類和接口
使用這種設(shè)計(jì)我們可以輕松的替換項(xiàng)目中的具體類。
Momento 模式
保存對(duì)象狀態(tài)
我們平時(shí)使用的文本編輯器、PS 等等,都有一系列十分重要的功能,就是撤銷(undo)、 重做(redo) 和歷史快照(history)。像是撤銷這樣的操作我每天要使用幾百次,那如何記錄每個(gè)操作節(jié)點(diǎn)的狀態(tài)就十分重要了。
而 Momento 模式
就十分善于處理這種情況,Momento 有紀(jì)念品的意思,我們也可以想象著把一個(gè)對(duì)象每個(gè)時(shí)間點(diǎn)的狀態(tài)拍上一張照片作為紀(jì)念品。
當(dāng)我們需要的時(shí)候,我們可以通過(guò)每個(gè)時(shí)間點(diǎn)的快照來(lái)恢復(fù)對(duì)象的狀態(tài)。比如我們要記錄一個(gè)棋局,類ChessGame
表示一局正在進(jìn)行的棋盤。里面有方法createMomento()
通過(guò)當(dāng)前棋子的數(shù)據(jù)存儲(chǔ)快照。我們是創(chuàng)建一個(gè)類Momento
來(lái)存儲(chǔ)棋局?jǐn)?shù)據(jù)的。生成的快照被存入棋局的數(shù)組history
中,當(dāng)調(diào)用undo()
方法時(shí),我們就取出最后一個(gè)棋局狀態(tài)進(jìn)行棋局的復(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)
} // 生成一個(gè)快照,并存入數(shù)組中
}
class Momento {
var chessmanLocations: [Any]!
}
這里的 Momento 模式
和以前的 Prototype 模式
在存儲(chǔ)狀態(tài)上也一點(diǎn)點(diǎn)相似,但是這里的Momento
只是存儲(chǔ)恢復(fù)狀態(tài)所需要的必要數(shù)據(jù),而Prototype 模式
中,實(shí)例復(fù)制成的則是完完全全相同的另一個(gè)實(shí)例,所以它們的區(qū)別還是很明顯的。
State 模式 (狀態(tài)模式)
用類表示狀態(tài)
有些時(shí)候我們?cè)陧?xiàng)目當(dāng)中會(huì)遇到各種各樣的狀態(tài),比如應(yīng)用的夜間模式和白天模式,再或者是一個(gè)警報(bào)系統(tǒng)的各個(gè)預(yù)警狀態(tài)。使用夜間、白天模式是一些閱讀軟件常備的功能,切換不同的模式,整個(gè)應(yīng)用的界面會(huì)發(fā)生色調(diào)的轉(zhuǎn)變。而警報(bào)系統(tǒng)在不同的預(yù)警狀態(tài)下,對(duì)同一事件的處理方式也是不同的。
針對(duì)這種需要根據(jù)狀態(tài)判斷的例子,我們通常使用的方法,就是通過(guò) if
或是switch
來(lái)判斷不同的狀態(tài),而執(zhí)行不同的實(shí)現(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 ...
}
}
...
}
這個(gè)就是我們一般的實(shí)現(xiàn)方式,這樣的實(shí)現(xiàn)方式在簡(jiǎn)單的狀態(tài)切換時(shí)到?jīng)]有什么。但是像是以上這樣的白天和黑夜模式的界面顏色獲取,可能有幾十個(gè)方法,一個(gè)類中滿滿的都是if
看起來(lái)就眼花。如果這個(gè)時(shí)候你需要添加另一個(gè)模式,你就需要在每個(gè)方法下面添加一個(gè) else if
,重要的是,編譯器并不會(huì)因?yàn)槟阃泴懸粋€(gè),而通知你, 所以,在添加新的模式時(shí),我們很容易出錯(cuò),接下來(lái)就是用到 State 模式
的時(shí)候了?。
通過(guò)一個(gè)類來(lái)表示一個(gè)狀態(tài),就是狀態(tài)模式。 在State 模式
中我們通過(guò)創(chuàng)建一個(gè)類來(lái)表示一個(gè)新狀態(tài)。像以前一樣,我們需要?jiǎng)?chuàng)建一個(gè)抽象類State
來(lái)定義狀態(tài)中需要實(shí)現(xiàn)的方法。接下來(lái)我們分別定義NightState
和DayState
來(lái)表示白天和黑夜的狀態(tài),通過(guò)以下的代碼我們來(lá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)
}
}
通過(guò)上面的例子,我想你一定明白了它們的區(qū)別。在這樣的 State 模式
下,UIManager
是用來(lái)控制界面的顏色顯示的。它負(fù)責(zé)切換和控制狀態(tài),所以它需要知道所有狀態(tài)的條件。
除了讓UIManager
控制狀態(tài)的切換外,我們還可以讓每個(gè)狀態(tài)本身去控制現(xiàn)在的狀態(tài),這里就像是 Chain of Responsibility 模式
(責(zé)任模式)。我們擴(kuò)展一下這個(gè)協(xié)議:
extension State {
func setTime(manager: UIManager, time: Date) {
}
}
// DaySate 和 NightState 需要將上對(duì)應(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
}
}
}
可以看出來(lái),UIManager
只需要默認(rèn)一個(gè)狀態(tài),然后再調(diào)用方法前,告知當(dāng)前模式時(shí)間,它就可以通過(guò)自己的判斷來(lái)尋找正確的狀態(tài)。這里的狀態(tài)只有兩種,如果有很多種的話,自己不是此狀態(tài),就傳遞給下一個(gè)狀態(tài),直到找到一個(gè)正確的狀態(tài)。
使用第一種方法,manager 就需要知道所有的狀態(tài)關(guān)系。但是耦合度很低,各個(gè)狀態(tài)不需要知道其他的狀態(tài)。
而第二種方法,每個(gè)狀態(tài)或多或少的需要知道其他的狀態(tài),這樣增加了耦合度。不過(guò) Manager 不用再管理所有的狀態(tài)了,它只需要處理方法就行了。
-
我們可以方便的添加各種各樣的狀態(tài)我們只需要實(shí)現(xiàn)
State
的方法就行了,可能還需要處理一下切換到其他狀態(tài)的情況,不過(guò)這是你使用第二種Manager 管理的時(shí)候。 -
添加依賴于狀態(tài)的處理十分的麻煩當(dāng)我們對(duì)狀態(tài)添加一個(gè)新的處理方法的時(shí)候,我們需要修改每一個(gè)狀態(tài),這十分的麻煩。所幸的是,我們不會(huì)忘了給其中的一個(gè)狀態(tài)添加新的處理方法,因?yàn)榫幾g器會(huì)提示我們,如果我們忘記了給任意一個(gè)狀態(tài)添加方法。如果不使用
State 模式
就不會(huì)得到編譯器的幫助,可想而知,一旦大意,就會(huì)引發(fā)不可知的 bug。
避免浪費(fèi)
Flyweight 模式 (輕量級(jí)模式)
共享對(duì)象,避免浪費(fèi)
我們都知道在應(yīng)用當(dāng)中使用的對(duì)象都占用了一定的系統(tǒng)內(nèi)存,當(dāng)我們的對(duì)象占用內(nèi)存過(guò)大時(shí),就會(huì)降低系統(tǒng)的運(yùn)行速度和穩(wěn)定性,甚至引發(fā)崩潰。 如果有些對(duì)象可以被共同使用,就可以減少創(chuàng)建新對(duì)象的開銷,也可以降低內(nèi)存的占用。所以 Flyweight 模式
就是 通過(guò)盡量共享實(shí)例來(lái)避免 new 出新的實(shí)例來(lái)大大降低系統(tǒng)的內(nèi)存消耗。
這里我們舉一個(gè)例子,比如我們要打印一張圖片,而這張圖片是又幾種不同的素材圖片拼出來(lái)的。當(dāng)我們?cè)谠诖蛴D片的時(shí)候,我們需要先將對(duì)應(yīng)的素材按照順序排列好,才能進(jìn)行打印。
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] = [:] // 每個(gè) id 對(duì)應(yīng)一個(gè)它的圖片緩存
// 通過(guò) id 獲取圖片,如果緩存中有的話就直接使用,如果沒有的話,就創(chuàng)建一個(gè)放入到緩存中
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ù)組的順序進(jìn)行排序
func printImage(imageIds: [Int]) {
self.imageIds = imageIds
var images = imageIds.map { return getImage($0) }
printWithImageData(images)
}
func printWithImageData(imageDatas: [Image]) {
... //根據(jù)圖片的數(shù)據(jù)進(jìn)行打印
}
}
我們創(chuàng)建Image
當(dāng)做是素材,ImageManager
是用來(lái)排版素材的類,它通過(guò)傳入一個(gè)包含素材 id 的數(shù)組來(lái)打印出對(duì)應(yīng)的圖片。 在排列過(guò)程中,我們每種素材的信息其實(shí)是不變的,所以它是可以共享的,我們使用一個(gè)字典把 id 當(dāng)做 key來(lái) 實(shí)現(xiàn)緩存素材數(shù)據(jù)。當(dāng)通過(guò) id 排列素材時(shí),我們直接獲取緩存中的素材數(shù)據(jù),如果重復(fù)使用了一個(gè)素材,也不會(huì)再次創(chuàng)建,而是共享一個(gè)對(duì)象。通過(guò)這樣的方式,我們就能夠減少一大部分的內(nèi)存消耗。
不過(guò)共享同一個(gè)對(duì)象也有問題,就是改變了這個(gè)對(duì)象,那么所有共享它的也會(huì)發(fā)生改變。這有時(shí)候是好事,有時(shí)候是壞事,具體要看應(yīng)用的場(chǎng)景。
但是大概可以這樣判斷是否該共享該對(duì)象。
-
代表本質(zhì)的,不依賴于狀態(tài)和位置的對(duì)象可以共享它是一個(gè)
intrinsic 信息
。 -
外在的,依賴于狀態(tài)和位置的對(duì)象不能共享 它是一個(gè)
extrinsic 信息
。
一般的對(duì)象都適用于這兩個(gè)規(guī)則。根據(jù)項(xiàng)目的實(shí)現(xiàn)目的,靈活的運(yùn)用Flyweight 模式
可以優(yōu)化你的應(yīng)用內(nèi)存占用。
Proxy 模式 (代理人模式)
只在必要時(shí)生成實(shí)例
當(dāng)讀到代理人模式
的時(shí)候,希望你不會(huì)把它和OC
中的delegate
弄混淆了。OC中的 delegate 其實(shí)是接口interface
或者說(shuō)是protocol
的使用,而我們今天要了解的Proxy 模式
中的代理人
指的是替原本的對(duì)象來(lái)執(zhí)行操作。
在哪些情況下,我們需要使用Proxy 模式
呢? 通常是當(dāng)一個(gè)對(duì)象的創(chuàng)建需要消耗大量的性能,而它的重要操作又可以延后的時(shí)候。在這種情況下,如果需要使用此對(duì)象,就立刻創(chuàng)建,可能會(huì)占用過(guò)高的性能,而后又沒有使用到這個(gè)對(duì)象的重要功能,那豈不是白白浪費(fèi)了大量的系統(tǒng)算力。
所以我們需要使用一個(gè)代理人來(lái)替代這個(gè)本人。它實(shí)現(xiàn)這個(gè)本人的基本屬性和方法,而將耗時(shí)的工作交給真正的本人
去做,那樣只有在真正需要本人
去做得事情才會(huì)去創(chuàng)建本人
,而其他的不耗時(shí)操作將交給代理人
去做。
這里我們舉一個(gè)例子,比如有一個(gè)打印圖片的類ImagePrint
,它通過(guò)一個(gè)url
來(lái)初始化實(shí)例,調(diào)用Print
方法就可以打印出這張圖片,這就是本人。又有一個(gè)類ImagePrintProxy
表示它的代理人
。接口ImagePrintable
規(guī)定了本人
和代理人
都應(yīng)該具備的方法和屬性。下面我們通過(guò)偽代碼來(lái)具體了解一下整個(gè)過(guò)程:
// 首先是接口 ImagePrintable,它定義了一個(gè)能打印圖片的類,都需要實(shí)現(xiàn)什么方法
protocol ImagePrintable { func setUrl(urlStr: String) // 設(shè)置圖片地址
func getUrl() -> String // 獲取圖片地址
func print() // 根據(jù)地址,打印圖片
}
// ImagePrint 本人,它是打印圖片的實(shí)際操作者,打印圖片是一個(gè)耗時(shí)的操作,
class ImagePrint: ImagePrintable {
var url: String
var printer: PhotoPrinter? // 這是一個(gè)圖片打印機(jī),初始化它需要耗費(fèi)大量的時(shí)間
init(url: String) { self.url = url
self.printer = PhotoPrinter() // 這是一個(gè)耗時(shí)操作
}
func getUrl() -> String {
return self.url
}
func setUrl(urlStr: String) { self.url = urlStr
}
func print() {
self.printer.printImage()
... // 根據(jù) url 下載圖片然后使用 printer 再打印出來(lái)
}
}
// 最后就是 ImagePrintProxy 代理人,通過(guò)代理人我們可以在不打印圖片時(shí)
// 設(shè)置和獲取圖片的地址,而不用初始化 ImagePrint。因?yàn)槌跏蓟痐ImagePrint` 時(shí)
// 會(huì)創(chuàng)建`printer`,這會(huì)耗費(fèi)大量的時(shí)間。而是在調(diào)用 print 的時(shí)候,在初始化它。
class ImagePrintProxy: ImagePrintable {
var url: String
var real: ImagePrint? // 這是真正執(zhí)行打印操作的對(duì)象
init(url: String) {
self.url = url
}
func getUrl() -> String {
return self.url
}
func setUrl(urlStr: String) {
if self.real != nil { //當(dāng)存在本人時(shí),就設(shè)置本人的值
self.real.url = urlStr
}
self.url = urlStr
}
func print() {
self.release()
self.real.print() // 調(diào)用本人來(lái)實(shí)現(xiàn)打印圖片的方法
}
func release() { // 生成原始對(duì)象的方法
if self.real == nil {
self.real = ImagePrint(self.url)
}
}
}
看過(guò)這個(gè)例子以后,就很容易理解什么是Proxy 模式
了,使用 Proxy 模式
的時(shí)候,調(diào)用者并不關(guān)心是誰(shuí)實(shí)現(xiàn)了里面的方法,它只是調(diào)用了符合ImagePrintable
的類。而實(shí)際的執(zhí)行者ImagePrint
也不關(guān)心自己是被直接調(diào)用還是間接調(diào)用。對(duì)問題的處理就交給了ImagePrintProxy
這個(gè)代理人身上。這樣的話,代理人就可以根據(jù)實(shí)際的情況來(lái)替?本人
完成一些簡(jiǎn)單的工作,而盡量將本人
的創(chuàng)建延后,只在真正需要使用的時(shí)候,才會(huì)創(chuàng)建本人
。
這樣的設(shè)計(jì),對(duì)外顯示出了一致性,在不影響調(diào)用關(guān)系的情況下。節(jié)省了系統(tǒng)的性能消耗,能提高應(yīng)用的流暢性。
用類來(lái)表示
Command 模式 (命令模式)
命令也是類
通常我們所說(shuō)的命令都是實(shí)例的方法,雖然調(diào)用的結(jié)果會(huì)在實(shí)例的狀態(tài)中得到反饋,但是卻無(wú)法留下調(diào)用的歷史記錄。當(dāng)我們想要把每一次調(diào)用都記錄下來(lái)時(shí),我們可以把類當(dāng)作命令來(lái)看待,使用類來(lái)表示要做的操作。這樣我們管理一系列操作時(shí)就是直接管理這些命令類的實(shí)例,而不是通過(guò)方法進(jìn)行動(dòng)態(tài)操作了。
那我們?cè)撊绾芜M(jìn)行設(shè)計(jì)以實(shí)現(xiàn)Command 模式
呢,一樣,我們舉一個(gè)例子。比如我們實(shí)現(xiàn)一個(gè)和Flyweight 模式
(上上個(gè)模式)一樣的功能,通過(guò)素材打印圖片,這里我們?cè)贋樗砑右恍┬碌墓δ埽⑦M(jìn)行優(yōu)化。
- 如果素材進(jìn)行排列的時(shí)候,不是按照順序,而是有各自的坐標(biāo)
- 并且每添加一個(gè)素材我們就立即打印出來(lái)。
現(xiàn)在我們把每次添加一個(gè)素材的操作不在看做是一個(gè)方法里面的循環(huán)執(zhí)行,而是一個(gè)個(gè)命令。我們需要一個(gè)接口Command (interface)
表示什么是命令,命令很簡(jiǎn)單,只需要能執(zhí)行就 OK 了。 每次繪制素材的操作用DrawCommand
來(lái)表示,它繼承于Command
。
有時(shí)我們可能需要執(zhí)行一系列的操作,所以我們需要一個(gè)表示操作集合的類MacroCommand
,它同樣也繼承于Command
,在MacroCommand
中有添加和移除Command
的命令,同樣有保存所有操作的屬性commands
。
有了命令,但是命令本身不執(zhí)行具體的繪制操作,它僅僅是提供操作的具體數(shù)據(jù)。我們還需要一個(gè)繪制類,這個(gè)繪制類我們不具體創(chuàng)建,而是通過(guò)一個(gè)接口Drawable
來(lái)表示, Drawable
需要實(shí)現(xiàn)繪制方法draw()
。為什么這樣設(shè)計(jì),如果你看了以上的設(shè)計(jì)模式,我想你應(yīng)該已經(jīng)很清楚了。使用接口,能方便的替換繪制實(shí)現(xiàn),也為你要繪制不同的東西提供了擴(kuò)展的可能性,并且不影響其他代碼的結(jié)構(gòu),這就是代碼的可替換性。
這里我們用ImageDrawCanvas
來(lái)表示一個(gè)簡(jiǎn)單的繪制圖片的圖層。下面是偽代碼的實(shí)現(xiàn)
// 命令接口,只定義了 excute
protocol Command {
open func excute()
}
// DrawCommand 表示繪制命令的類
class DrawCommand: Command {
var url: String // 圖片地址
var position: Point // 圖片位置
var drawable: Drawable // 執(zhí)行繪制操作的圖層,并未指定具體的類型,而是接口 Drawable
// 初始化一個(gè)命令
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 一個(gè)命令集合
class MacroCommand: Command {
var commands: [Command] // 所有的命令,只要是`Command` 就可以,這意味著不但可以添加`DrawCommand`還可以添加`MacroCommand`,命令集合在本質(zhì)上還是命令。
func addCommand(command: Command) { // 添加一個(gè)命令
if command != self { // 不能添加添加自己,防止死循環(huán)
self.commands.append(command)
}
}
func undo() { // 移除最后一個(gè)命令
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)你需要重新繪制的時(shí)候,可以直接調(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),進(jìn)行圖片的繪制
}
func redo() { //重新繪制
self.history.excute()
}
}
// 所有的類都準(zhǔn)備好了,我們來(lái)看一下如何操作
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個(gè)素材
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) // 加入到歷史記錄中
}
}
}
以上偽實(shí)現(xiàn)了一個(gè)Commnad 模式
的圖片繪制功能,不過(guò)Command 模式
的主要實(shí)現(xiàn)就是這樣的。通過(guò)具象一個(gè)操作為一個(gè)實(shí)例,我們能精準(zhǔn)的操控每一個(gè)操作,并重復(fù)任意的步驟。我們還可以將這些實(shí)例進(jìn)行歸檔處理,永久保存我們的操作記錄。在我們了解的以上所有的設(shè)計(jì)模式,除了本文的把類作為命令,還有State 模式
中的把類作為狀態(tài)。 以后再遇到操作是需要在實(shí)例的方法內(nèi)進(jìn)行很多的判斷和選擇,你可以試著將不同的情況拆分為不同的類來(lái)實(shí)現(xiàn),或許會(huì)豁然開朗。
Interpreter 模式 (翻譯模式)
語(yǔ)法規(guī)則也是類
又多了一個(gè)用類來(lái)替換某些東西的類,而這次,我們這模擬的是語(yǔ)法。在某些特殊的情況下,我們可能想要設(shè)計(jì)一種新的迷你語(yǔ)言
來(lái)方便的編寫繁瑣的操作。例如正則表達(dá)式
就可以通過(guò)簡(jiǎn)短的語(yǔ)法來(lái)描述復(fù)雜的篩選條件。我們也可以設(shè)計(jì)一款小語(yǔ)言來(lái)這樣做,再編寫一個(gè)翻譯程序
將它翻譯成你所使用的語(yǔ)言。而其中的各種語(yǔ)法可以被翻譯為不同的類,比如Add 作為 +
, CommandList 作為 repeat
等等。但是,這個(gè)過(guò)程還是很麻煩的,這里的篇幅已經(jīng)很長(zhǎng)了。而敘述一個(gè)迷你語(yǔ)言,或許需要更大的篇幅才能講明白,而這篇文章只是想要使用簡(jiǎn)單的文字來(lái)幫助你了解所謂的23設(shè)計(jì)模式。
結(jié)語(yǔ)
終于看完了所有的23種設(shè)計(jì)模式,其實(shí)很多的設(shè)計(jì)模式已經(jīng)不知不覺中被我們使用了無(wú)數(shù)次了。對(duì)于經(jīng)驗(yàn)豐富的程序員而言,設(shè)計(jì)模式中的方法在他們看來(lái)是理所應(yīng)當(dāng)?shù)摹.吘梗O(shè)計(jì)模式本身就是對(duì)前輩們經(jīng)驗(yàn)的總結(jié),本身并沒有什么突出的特點(diǎn)。它也不能幫你解決所有的問題,但是通過(guò)了解設(shè)計(jì)模式,我們可以更快的學(xué)習(xí)到前輩的經(jīng)驗(yàn)。在實(shí)際的使用中,對(duì)我們的幫助是顯而易見的。
設(shè)計(jì)模式雖然很重要,但是你卻不用想著把它們都記在自己的腦海中。死記硬背從來(lái)都不是好方法,你只要有些許的印象,知道遇見這樣的問題時(shí)該使用什么樣的模式,隨后再去查詢具體的資料就是行了,善用搜索引擎可是程序員最重要的一項(xiàng)技能。
說(shuō)了那么多,最后再說(shuō)點(diǎn)我的感悟。
語(yǔ)言技巧很多,黑魔法很多,設(shè)計(jì)思想也很多,學(xué)完所有為大家所稱贊的思想和技巧,也并不能讓你的項(xiàng)目看起來(lái)更完美。遇見問題時(shí),越是簡(jiǎn)單的實(shí)現(xiàn)就越有可能解決問題,也更容易被人看懂。讓程序看起來(lái)簡(jiǎn)單,而不是讓它看起來(lái) NB。有一句話說(shuō)的好 “要讓程序看起來(lái)明顯沒有問題,而不是沒有明顯的問題。”