在過(guò)去的時(shí)間里,人們對(duì)于設(shè)計(jì) API 總結(jié)了很多通用的模式和最佳實(shí)踐方案。一般情況下,我們總是可以從蘋果的 Foundation、Cocoa、Cocoa Touch 和很多其他框架中總結(jié)出一些開(kāi)發(fā)中的范例。毫無(wú)疑問(wèn),對(duì)于“特定情境下的 API 應(yīng)該如何設(shè)計(jì)”這個(gè)問(wèn)題,不同的人總是有著不同的意見(jiàn),對(duì)于這個(gè)問(wèn)題有很大的討論空間。不過(guò)對(duì)于很多 Objective-C 的開(kāi)發(fā)者來(lái)說(shuō),對(duì)于那些常用的模式早已習(xí)以為常。
隨著 Swift 的出現(xiàn),設(shè)計(jì) API 引起了更多的問(wèn)題。絕大多數(shù)情況下,我們只能繼續(xù)做著手頭的工作,然后把現(xiàn)有的方法翻譯成 Swift 版本。不過(guò),這對(duì)于 Swift 來(lái)說(shuō)并不公平,因?yàn)楹?Objective-C 相比,Swift 添加了很多新的特性。引用 Swift 創(chuàng)始人 Chris Lattner 的一段話:
Swift 引入了泛型和函數(shù)式編程的思想,極大地?cái)U(kuò)展了設(shè)計(jì)的空間。
在這篇文章里,我們將會(huì)圍繞 Core Image
進(jìn)行 API 封裝,以此為例,探索如何在 API 設(shè)計(jì)中使用這些新的工具。 Core Image
是一個(gè)功能強(qiáng)大的圖像處理框架,但是它的 API 有時(shí)有點(diǎn)笨重。 Core Image
的 API 是弱類型的 - 它通過(guò)鍵值對(duì) (key-value) 設(shè)置圖像濾鏡。這樣在設(shè)置參數(shù)的類型和名字時(shí)很容易失誤,會(huì)導(dǎo)致運(yùn)行時(shí)錯(cuò)誤。新的 API 將會(huì)十分的安全和模塊化,通過(guò)使用類型而不是鍵值對(duì)來(lái)規(guī)避這樣的運(yùn)行時(shí)錯(cuò)誤。
我們的目標(biāo)是構(gòu)建一個(gè) API ,讓我們可以簡(jiǎn)單安全的組裝自定義濾鏡。舉個(gè)例子,在文章的結(jié)尾,我們可以這樣寫:
let myFilter = blur(blurRadius) >|> colorOverlay(overlayColor)
let result = myFilter(image)
上面構(gòu)建了一個(gè)自定義的濾鏡,先模糊圖像,然后再添加一個(gè)顏色蒙版。為了達(dá)到這個(gè)目標(biāo),我們將充分利用 Swift 函數(shù)是一等公民這一特性。項(xiàng)目源碼可以在 Github 上的這個(gè)示例項(xiàng)目中下載。
CIFilter
是 Core Image
中的一個(gè)核心類,用來(lái)創(chuàng)建圖像濾鏡。當(dāng)實(shí)例化一個(gè) CIFilter
對(duì)象之后,你 (幾乎) 總是通過(guò) kCIInputImageKey
來(lái)輸入圖像,然后通過(guò) kCIOutputImageKey
獲取返回的圖像,返回的結(jié)果可以作為下一個(gè)濾鏡的參數(shù)輸入。
在我們即將開(kāi)發(fā)的 API 里,我們會(huì)把這些鍵值對(duì) (key-value) 對(duì)應(yīng)的真實(shí)內(nèi)容抽離出來(lái),為用戶提供一個(gè)安全的強(qiáng)類型 API。我們定義了自己的濾鏡類型 Filter
,它是一個(gè)可以傳入圖片作為參數(shù)的函數(shù),并且返回一個(gè)新的圖片。
typealias Filter = CIImage -> CIImage
這里我們用 typealias
關(guān)鍵字,為 CIImage -> CIImage
類型定義了我們自己的名字,這個(gè)類型是一個(gè)函數(shù),它的參數(shù)是一個(gè) CIImage
,返回值也是 CIImage
。這是我們后面開(kāi)發(fā)需要的基礎(chǔ)類型。
如果你不太熟悉函數(shù)式編程,你可能對(duì)于把一個(gè)函數(shù)類型命名為 Filter
感覺(jué)有點(diǎn)奇怪,通常來(lái)說(shuō),我們會(huì)用這樣的命名來(lái)定義一個(gè)類。如果我們很想以某種方式來(lái)表現(xiàn)這個(gè)類型的函數(shù)式的特性,我們可以把它命名成 FilterFunction
或者一些其他的類似的名字。但是,我們有意識(shí)的選擇了 Filter
這個(gè)名字,因?yàn)樵诤瘮?shù)式編程的核心哲學(xué)里,函數(shù)就是值,函數(shù)和結(jié)構(gòu)體、整數(shù)、多元組、或者類,并沒(méi)有任何區(qū)別。一開(kāi)始我也不是很適應(yīng),不過(guò)一段時(shí)間之后發(fā)現(xiàn),這樣做確實(shí)很有意義。
現(xiàn)在我們已經(jīng)定義了 Filter
類型,接下來(lái)可以定義函數(shù)來(lái)構(gòu)建特定的濾鏡了。這些函數(shù)需要參數(shù)來(lái)設(shè)置特定的濾鏡,并且返回一個(gè)類型為 Filter
的值。這些函數(shù)大概是這個(gè)樣子:
func myFilter(/* parameters */) -> Filter
注意返回的值 Filter
本身就是一個(gè)函數(shù),在后面有利于我們將多個(gè)濾鏡組合起來(lái),以達(dá)到理想的處理效果。
為了讓后面的開(kāi)發(fā)更輕松一點(diǎn),我們擴(kuò)展了 CIFilter
類,添加了一個(gè) convenience 的初始化方法,以及一個(gè)用來(lái)獲取輸出圖像的計(jì)算屬性:
typealias Parameters = Dictionary<String, AnyObject>
extension CIFilter {
convenience init(name: String, parameters: Parameters) {
self.init(name: name)
setDefaults()
for (key, value : AnyObject) in parameters {
setValue(value, forKey: key)
}
}
var outputImage: CIImage { return self.valueForKey(kCIOutputImageKey) as CIImage }
}
這個(gè) convenience 初始化方法有兩個(gè)參數(shù),第一個(gè)參數(shù)是濾鏡的名字,第二個(gè)參數(shù)是一個(gè)字典。字典中的鍵值對(duì)將會(huì)被設(shè)置成新濾鏡的參數(shù)。我們 convenience 初始化方法先調(diào)用了指定的初始化方法,這符合 Swift 的開(kāi)發(fā)規(guī)范。
計(jì)算屬性 outputImage
可以方便地從濾鏡對(duì)象中獲取到輸出的圖像。它查找 kCIOutputImageKey
對(duì)應(yīng)的值并且將其轉(zhuǎn)換成一個(gè) CIImage
對(duì)象。通過(guò)提供這個(gè)屬性, API 的用戶不再需要對(duì)返回的結(jié)果手動(dòng)進(jìn)行類型轉(zhuǎn)換了。
有了這些東西,現(xiàn)在我們就可以定義屬于自己的簡(jiǎn)單濾鏡了。高斯模糊濾鏡只需要一個(gè)模糊半徑作為參數(shù),我們可以非常容易的完成一個(gè)模糊濾鏡:
func blur(radius: Double) -> Filter {
return { image in
let parameters : Parameters = [kCIInputRadiusKey: radius, kCIInputImageKey: image]
let filter = CIFilter(name:"CIGaussianBlur", parameters:parameters)
return filter.outputImage
}
}
就是這么簡(jiǎn)單,這個(gè)模糊函數(shù)返回了一個(gè)函數(shù),新的函數(shù)的參數(shù)是一個(gè)類型為 CIImage
的圖片,返回值 (filter.outputImage
) 是一個(gè)新的圖片 。這個(gè)模糊函數(shù)的格式是 CIImage -> CIImage
,滿足我們前面定義的 Filter
類型的格式。
這個(gè)例子只是對(duì) Core Image
中已有濾鏡的一個(gè)簡(jiǎn)單的封裝,我們可以多次重復(fù)同樣的模式,創(chuàng)建屬于我們自己的濾鏡函數(shù)。
現(xiàn)在讓我們定義一個(gè)顏色濾鏡,可以在現(xiàn)有的圖片上面加上一層顏色蒙版。 Core Image
默認(rèn)沒(méi)有提供這個(gè)濾鏡,不過(guò)我們可以通過(guò)已有的濾鏡組裝一個(gè)。
我們使用兩個(gè)模塊來(lái)完成這個(gè)工作,一個(gè)是顏色生成濾鏡 (CIConstantColorGenerator
),另一個(gè)是資源合成濾鏡 (CISourceOverCompositing
)。讓我們先定義一個(gè)生成一個(gè)常量顏色面板的濾鏡:
func colorGenerator(color: UIColor) -> Filter {
return { _ in
let filter = CIFilter(name:"CIConstantColorGenerator", parameters: [kCIInputColorKey: color])
return filter.outputImage
}
}
這段代碼看起來(lái)和前面的模糊濾鏡差不多,不過(guò)有一個(gè)較為明顯的差異:顏色生成濾鏡不會(huì)檢測(cè)輸入的圖片。所以在函數(shù)里我們不需要給傳入的圖片參數(shù)命名,我們使用了一個(gè)匿名參數(shù) _
來(lái)強(qiáng)調(diào)這個(gè) filter 的圖片參數(shù)是被忽略的。
接下來(lái),我們來(lái)定義合成濾鏡:
func compositeSourceOver(overlay: CIImage) -> Filter {
return { image in
let parameters : Parameters = [
kCIInputBackgroundImageKey: image,
kCIInputImageKey: overlay
]
let filter = CIFilter(name:"CISourceOverCompositing", parameters: parameters)
return filter.outputImage.imageByCroppingToRect(image.extent())
}
}
在這里我們將輸出圖像裁剪到和輸入大小一樣。這并不是嚴(yán)格需要的,要取決于我們想讓濾鏡如何工作。不過(guò),在后面我們的例子中我們可以看出來(lái)這是一個(gè)明智之舉。
func colorOverlay(color: UIColor) -> Filter {
return { image in
let overlay = colorGenerator(color)(image)
return compositeSourceOver(overlay)(image)
}
}
我們?cè)僖淮畏祷亓艘粋€(gè)參數(shù)為圖片的函數(shù),colorOverlay
在一開(kāi)始先調(diào)用了 colorGenerator
濾鏡。colorGenerator
濾鏡需要一個(gè)顏色作為參數(shù),并且返回一個(gè)濾鏡。因此 colorGenerator(color)
是 Filter
類型的。但是 Filter
類型本身是一個(gè) CIImage
向 CIImage
轉(zhuǎn)換的函數(shù),我們可以在 colorGenerator(color)
后面加上一個(gè)類型為 CIImage
的參數(shù),這樣可以得到一個(gè)類型為 CIImage
的蒙版圖片。這就是在定義 overlay
的時(shí)候發(fā)生的事情:我們用 colorGenerator
函數(shù)創(chuàng)建了一個(gè)濾鏡,然后把圖片作為一個(gè)參數(shù)傳給了這個(gè)濾鏡,從而得到了一張新的圖片。返回值 compositeSourceOver(overlay)(image)
和這個(gè)基本相似,它由一個(gè)濾鏡 compositeSourceOver(overlay)
和一個(gè)圖片參數(shù) image
組成。
現(xiàn)在我們已經(jīng)定義了一個(gè)模糊濾鏡和一個(gè)顏色濾鏡,我們?cè)谑褂玫臅r(shí)候可以把它們組合在一起:我們先將圖片做模糊處理,然后再在上面放一個(gè)紅色的蒙層。讓我們先加載一張圖片:
let url = NSURL(string: "http://tinyurl.com/m74sldb");
let image = CIImage(contentsOfURL: url)
現(xiàn)在我們可以把濾鏡組合起來(lái),同時(shí)應(yīng)用到一張圖片上:
let blurRadius = 5.0
let overlayColor = UIColor.redColor().colorWithAlphaComponent(0.2)
let blurredImage = blur(blurRadius)(image)
let overlaidImage = colorOverlay(overlayColor)(blurredImage)
我們又一次的通過(guò)濾鏡組裝了圖片。比如在倒數(shù)第二行,我們先得到了模糊濾鏡 blur(blurRadius)
,然后再把這個(gè)濾鏡應(yīng)用到圖片上。
不過(guò),我們可以做的比上面的更好。我們可以簡(jiǎn)單的把兩行濾鏡的調(diào)用組合在一起變成一行,這是我腦海中想到的第一個(gè)能改進(jìn)的地方:
let result = colorOverlay(overlayColor)(blur(blurRadius)(image))
不過(guò),這些圓括號(hào)讓這行代碼完全不具有可讀性,更好的方式是定義一個(gè)函數(shù)來(lái)完成這項(xiàng)任務(wù):
func composeFilters(filter1: Filter, filter2: Filter) -> Filter {
return { img in filter2(filter1(img)) }
}
composeFilters
函數(shù)的兩個(gè)參數(shù)都是 Filter ,并且返回了一個(gè)新的 Filter 濾鏡。組裝后的濾鏡需要一個(gè) CIImage
類型的參數(shù),并且會(huì)把這個(gè)參數(shù)分別傳給 filter1
和 filter2
。現(xiàn)在我們可以用 composeFilters
來(lái)定義我們自己的組合濾鏡:
let myFilter = composeFilters(blur(blurRadius), colorOverlay(overlayColor))
let result = myFilter(image)
我們還可以更進(jìn)一步的定義一個(gè)濾鏡運(yùn)算符,讓代碼更具有可讀性,
infix operator >|> { associativity left }
func >|> (filter1: Filter, filter2: Filter) -> Filter {
return { img in filter2(filter1(img)) }
}
運(yùn)算符通過(guò) infix
關(guān)鍵字定義,表明運(yùn)算符具有 左
和 右
兩個(gè)參數(shù)。associativity left
表明這個(gè)運(yùn)算滿足左結(jié)合律,即:f1 >|> f2 >|> f3 等價(jià)于 (f1 >|> f2) >|> f3。通過(guò)使這個(gè)運(yùn)算滿足左結(jié)合律,再加上運(yùn)算內(nèi)先應(yīng)用了左側(cè)的濾鏡,所以在使用的時(shí)候?yàn)V鏡順序是從左往右的,就像 Unix 管道一樣。
剩余的部分是一個(gè)函數(shù),內(nèi)容和 composeFilters
基本相同,只不過(guò)函數(shù)名變成了 >|>
。
接下來(lái)我們把這個(gè)組合濾鏡運(yùn)算器應(yīng)用到前面的例子中:
let myFilter = blur(blurRadius) >|> colorOverlay(overlayColor)
let result = myFilter(image)
運(yùn)算符讓代碼變得更易于閱讀和理解濾鏡使用的順序,調(diào)用濾鏡的時(shí)候也更加的方便。就好比是 1 + 2 + 3 + 4
要比 add(add(add(1, 2), 3), 4)
更加清晰,更加容易理解。
很多 Objective-C 的開(kāi)發(fā)者對(duì)于自定義運(yùn)算符持有懷疑態(tài)度。在 Swift 剛發(fā)布的時(shí)候,這是一個(gè)并沒(méi)有很受歡迎的特性。很多人在 C++ 中遭遇過(guò)自定義運(yùn)算符過(guò)度使用 (甚至濫用) 的情況,有些是個(gè)人經(jīng)歷過(guò)的,有些是聽(tīng)到別人談起的。
你可能對(duì)于前面定義的運(yùn)算符 >|>
持有同樣的懷疑態(tài)度,畢竟如果每個(gè)人都定義自己的運(yùn)算符,那代碼豈不是很難理解了?值得慶幸的是在函數(shù)式編程里有很多的操作,為這些操作定義一個(gè)運(yùn)算符并不是一件很罕見(jiàn)的事情。
我們定義的濾鏡組合運(yùn)算符是一個(gè)函數(shù)組合的例子,這是一個(gè)在函數(shù)式編程中廣泛使用的概念。在數(shù)學(xué)里,兩個(gè)函數(shù) f
和 g
的組合有時(shí)候?qū)懽?f ° g
,這樣定義了一種全新的函數(shù),將輸入的 x
映射到 f(g(x))
上。這恰好就是我們的 >|>
所做的工作 (除了函數(shù)的逆向調(diào)用)。
仔細(xì)想想,其實(shí)我們并沒(méi)有必要去定義一個(gè)用來(lái)專門組裝濾鏡的運(yùn)算符,我們可以用一個(gè)泛型的運(yùn)算符來(lái)組裝函數(shù)。目前我們的 >|>
是這樣的:
func >|> (filter1: Filter, filter2: Filter) -> Filter
這樣定義之后,我們傳入的參數(shù)只能是 Filter
類型的濾鏡。
但是,我們可以利用 Swift 的通用特性來(lái)定義一個(gè)泛型的函數(shù)組合運(yùn)算符:
func >|> <A, B, C>(lhs: A -> B, rhs: B -> C) -> A -> C {
return { x in rhs(lhs(x)) }
}
這個(gè)一開(kāi)始可能很難理解 -- 至少對(duì)我來(lái)說(shuō)是這樣。但是分開(kāi)的看了各個(gè)部分之后,一切都變得清晰起來(lái)。
首先,我們來(lái)看一下函數(shù)名后面的尖括號(hào)。尖括號(hào)定義了這個(gè)函數(shù)適用的泛型類型。在這個(gè)例子里我們定義了三個(gè)類型:A、B 和 C。因?yàn)槲覀儾](méi)有指定這些類型,所以它們可以代表任何東西。
接下來(lái)讓我們來(lái)看看函數(shù)的參數(shù):第一個(gè)參數(shù):lhs (left-hand side 的縮寫),是一個(gè)類型為 A -> B 的函數(shù)。這代表一個(gè)函數(shù)的參數(shù)為 A,返回值的類型為 B。第二個(gè)參數(shù):rhs (right-hand side 的縮寫),是一個(gè)類型為 B -> C 的函數(shù)。參數(shù)命名為 lhs 和 rhs,因?yàn)樗鼈兎謩e對(duì)應(yīng)操作符左邊和右邊的值。
重寫了沒(méi)有 Filter
的濾鏡組合運(yùn)算符之后,我們很快就發(fā)現(xiàn)其實(shí)前面實(shí)現(xiàn)的組合運(yùn)算符只是泛型函數(shù)中的一個(gè)特殊情況:
func >|> (filter1: CIImage -> CIImage, filter2: CIImage -> CIImage) -> CIImage -> CIImage
把我們腦海中的泛型類型 A、B、C 都換成 CIImage
,這樣可以清晰的理解用通用運(yùn)算符的來(lái)替換濾鏡組合運(yùn)算符是多么的有用。
至此,我們成功的用函數(shù)式 API 封裝了 Core Image
。希望這個(gè)例子能夠很好的說(shuō)明,對(duì)于 Objective-C 的開(kāi)發(fā)者來(lái)說(shuō),在我們所熟知的 API 的設(shè)計(jì)模式之外有一片完全不同的世界。有了 Swift,我們現(xiàn)在可以動(dòng)手探索那些全新的領(lǐng)域,并且將它們充分地利用起來(lái)。