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