本文大部分內容翻譯至《Pro Design Pattern In Swift》By Adam Freeman,一些地方做了些許修改,并將代碼升級到了Swift2.0,翻譯不當之處望多包涵。
對象模版模式
這里我們將介紹一個對于面向對象編程來說非常基礎的技術以至于不把它歸類到設計模式里面-用類或結構體創建新對象。
Swift中的元組是一系列的值組合在一起,用起來也很方便和簡單,但是它們卻有一些限制。下面我們創建Command Line Tool 工程,請看main.swift文件。
main.swift
import Foundation
var products = [
("Kayak", "A boat for one person", 275.0, 10),
("Lifejacket", "Protective and fashionable", 48.95, 14),
("Soccer Ball", "FIFA-approved size and weight", 19.5, 32)
]
func calculateTax(product:(String, String, Double, Int)) -> Double {
return product.2 * 0.2
}
func calculateStockValue(tuples:[(String, String, Double, Int)]) -> Double {
return tuples.reduce(0, combine: {
(total, product) -> Double in
return total + (product.2 * Double(product.3))
})
}
print("Sales tax for Kayak: $\(calculateTax(products[0]))")
print("Total value of stock: $\(calculateStockValue(products))")
在上面的代碼中,我們定義了一個元素是元祖類型的數組代表產品以及兩個方法來操作它們。calculateTax方法接受一個元祖類型的參數用來計算價格的消費稅,calculateStockValue方法對數組中所有的產品進行總價計算。執行代碼,我們可以看見:
Sales tax for Kayak: $55.0
Total value of stock: $4059.3
設計模式要解決的問題大部分都是組件之間的緊密耦合。當一個在一個組件的內部去操作另一個組件時會產生緊密的耦合,或者,換一種說法,當你想變更一個組件而不需要去升級另一個組件才是設計模式所推崇和解決的。請看下圖:
兩個方法都跟元祖緊密的耦合在一起,不僅是它們定義參數的方式還是方法的內容。 當用元祖做參數的時候,元祖的大小,順序,類型都必須完全匹配。在方法中,又用元祖的下標來獲取值,這就導致更緊密的耦合。
下面你將看見當我刪掉元祖中一個值會發生什么:
import Foundation
var products = [
("Kayak", 275.0, 10),
("Lifejacket", 48.95, 14),
("Soccer Ball", 19.5, 32)
]
func calculateTax(product:(String, String, Double, Int)) -> Double {
return product.2 * 0.2
}
func calculateStockValue(tuples:[(String, String, Double, Int)]) -> Double {
return tuples.reduce(0, combine: {
(total, product) -> Double in
return total + (product.2 * Double(product.3))
})
}
print("Sales tax for Kayak: $\(calculateTax(products[0]))")
print("Total value of stock: $\(calculateStockValue(products))")
我們刪掉了元祖中關于產品描述的值,但是很顯然我們現在跟著修改這兩個方法。
理解對象模版模式
對象模版模式利用一個類或者是結構體來定義一個能夠創建對象的模版。當組件需要一個對象時,它會通過指定模版的名稱來請求Swift運行環境創建并且運行環境會根據需要來初始化這個對象。
第一步就是組件提供模版名稱和一些要求的運行時數據請求Swift運行環境創建對象。 第二步,Swift運行環境會給要求的對象分配內存然后用模版來創建它,模版包含了用來設置對象的初始化狀態的初始化方法。最后一步Swift運行環境將創建好的對象交給請求的組件。
實現對象模版模式
Product.swift
import Foundation
class Product {
var name:String
var price:Double
var stock:Int
init(name:String, description:String, price:Double, stock:Int) {
self.name = name
self.description = description
self.price = price
self.stock = stock
}
}
使用對象模版模式的好處
-
解耦
我們將代碼做如下修改:
Product.swift
import Foundation
class Product {
var name:String
var description:String
var price:Double
var stock:Int
init(name:String, price:Double, stock:Int) {
self.name = name
self.price = price
self.stock = stock
}
}
main.swift
import Foundation
var products = [
Product(name: "Kayak", price: 275, stock: 10),
Product(name: "Lifejacket", price: 48.95, stock: 14),
Product(name: "Soccer Ball", price: 19.5, stock: 32)
]
func calculateTax(product:Product) -> Double {
return product.price * 0.2;
}
func calculateStockValue(productsArray:[Product]) -> Double {
return productsArray.reduce(0, combine: {(total, product) -> Double in
return total + (product.price * Double(product.stock))
})
}
我們更新了Product類將description屬性刪除了。最值得注意的是盡管我們修改了Product類,卻對calculateTax方法和calculateStockValue方法沒有任何影響。這是因為在Product類中每一個屬性都是獨立定義的并且這兩個方法也沒有操作description屬性。
-
封裝
用類或者結構體來定義數據模版最大的好處就是封裝。封裝允許數值和操作這些數值的邏輯以一個簡單的方式聯系起來。將數值和邏輯聯系起來的話使得代碼更有可讀性。
Product.swift
import Foundation
class Product {
var name:String
var price:Double
var stock:Int
var stockValue:Double{
return self.price * Double(self.stock)
}
init(name:String, price:Double, stock:Int) {
self.name = name
self.price = price
self.stock = stock
}
func calculateTax(rate: Double) -> Double {
return self.price * rate
}
}
main.swift
...
func calculateStockValue(productsArray:[Product]) -> Double {
return productsArray.reduce(0, combine: {(total, product) -> Double in
return total + product.stockValue
})
}
...
這看著也許是簡單的修改,但是重要的事情發生了:Product類現在有了公有描述(public presentation)和私有實現(private implementation)。
公有描述就是其他組件能夠使用的API。任何組件都能設置和獲取name,price和stock屬性的值。公有描述也包括stockValue屬性和calculateTax方法,但重要的是注意并不是指它們的實現。
阻止一個屬性或者一個方法暴露它具體實現能夠很容易的打破耦合,因為這樣就使得其他組件的依賴變成不可能。例如我們將Proudct類中的calculateTax方法做下面的修改:
...
func calculateTax(rate: Double) -> Double {
return min(10, self.price * rate)
}
...
因為修改的內容是在Product類中,這個修改對于其他的組件來說是不可見的,所以可以看出其他組件不會對Product類的實現產生依賴。
-
進化的公有描述
Swift 中很重要的一個特型就是可以隨著應用的改變你也可以進化類的公有描述。就目前來看,stock屬性是一個可以設置任何Int類型值的存儲屬性,但是對于庫存來說負數是沒有任何意義的。Swift支持我們能夠無縫的將存儲屬性修改成計算屬性。
import Foundation
class Product {
var name:String
var price:Double
private var stockBackingValue:Int = 0
var stock:Int {
get {
return stockBackingValue
}
set {
stockBackingValue = max(0, newValue)
}
}
var stockValue:Double{
return self.price * Double(self.stock)
}
init(name:String, price:Double, stock:Int) {
self.name = name
self.price = price
self.stock = stock
}
func calculateTax(rate: Double) -> Double {
return min(10,self.price * rate)
}
}
我們定義了一個 stockBackingValue存儲屬性的變量來儲存計算屬性變量stock的值,stock的get方法僅僅是簡單的返回stockBackingValue的值,但是set方法去用了max方法來保證當設定值是負數的時候用0來代替。
現在修改main.swift如下
main.swift
import Foundation
var products = [
Product(name: "Kayak", price: 275, stock: 10),
Product(name: "Lifejacket", price: 48.95, stock: 14),
Product(name: "Soccer Ball", price: 19.5, stock: 32)
]
func calculateTax(product:Product) -> Double {
return product.price * 0.2;
}
func calculateStockValue(productsArray:[Product]) -> Double {
return productsArray.reduce(0, combine: {(total, product) -> Double in
return total + product.stockValue
})
}
print("Sales tax for Kayak: $\(products[0].calculateTax(0.2))")
print("Total value of stock: $\(calculateStockValue(products))")
products[0].stock = -50
print("Stock Level for Kayak: \(products[0].stock)")
運行程序,得到下面結果:
Sales tax for Kayak: $10.0
Total value of stock: $4059.3
Stock Level for Kayak: 0
理解對象模版模式的陷阱
對象模版模式需要注意的陷阱就是模版的類型選擇,好比當用類更合適的時候卻使用了結構體。Swift的類和結構體有很多共性,但尤其注意的是結構體是值類型,類是引用類型。