我們在編寫類別時,有時會用上大量看上去很相似的方法,但礙於它們在計算方式上存在關鍵的差異,讓我們無法編寫一個通用函數,而刪減其他的函數。今天,讓我帶大家看看一種設計模式,它讓我們可以創建一個函數,來管理所有函數,如此一來,我們就可以刪除那些幾乎一模一樣的方法 ── 那就是策略模式 (Strategy Pattern)。
什麼是策略模式?
策略模式是屬於物件導向程式設計 (Object-oriented Programming, OOP) 結構,是行為模式 (behavioral pattern) 的一種,也被稱為 Policy Pattern。它背後的邏輯是,程式應該能夠延遲在運行時才選擇演算法,並在編譯器完成工作之後才執行,以免被演算過程打擾。
讓我們做個比喻,假設你要計算幾條非常困難的數學方程式,要買 4 部不同的計算機才能解決四式運算,這不是很麻煩嗎(而且非常不環保)?你應該也會覺得,一部可以根據問題切換不同計算方法的計算機更好吧!這正是策略模式的基礎。
程式碼範例
以下的程式碼範例是構建一個距離計算機,目的是計算兩點之間的距離。要計算兩點之間的距離,通常會使用歐氏距離 (Euclidian Metric)。但是,實際上還有很多種方法可以計算,具體取決於兩點之間的空間看起來像甚麼。
讓我們簡單一點來使用曼哈頓距離 (Manhattan distance),也被稱為 Snake Distance、City Block Distance、或 Rectilinear Distance)。曼哈頓距離是用於計算計程車幾何 (Taxicab geometry) 的距離。我不會在這裡深入討論,如果你對計程車幾何有興趣的話,可以瀏覽這個網站。好了,讓我們看看下面的實作:
import Foundation
import CoreGraphics
struct NaiveDistanceCalculator {
static func euclidianDistance(from first: CGPoint, to second: CGPoint) -> CGFloat {
return sqrt(pow(second.x - first.x, 2) + pow(second.y - first.y, 2))
}
static func manhattanDistance(from first: CGPoint, to second: CGPoint) -> CGFloat {
return abs(second.x - first.x) + abs(second.y - first.y)
}
}
print("Naive Euclidian Distance: \(NaiveDistanceCalculator.euclidianDistance(from: .zero, to: .init(x: 3, y: 4)))")
print("Naive Manhattan Distance: \(NaiveDistanceCalculator.manhattanDistance(from: .zero, to: .init(x: 3, y: 4)))")
// Prints:
// Naive Euclidian Distance: 5.0
// Naive Manhattan Distance: 7.0
在上面的程式碼中,我們創建了一個列舉 (enum),當中包含了幾個靜態函數(我們使用不區分大小寫的列舉,這樣就不會被實例化了)。每個函式都讓我們得到了一種不同的方式,來計算兩點之間的距離。這看似很完美了,但是 ⋯ ⋯
這個方法最大的缺點,就是如果我們要添加一個函式,來計算明氏距離 (Minkowski Distance),就需要添加一個新函式,取一個與其他函數相似的名稱,然後在想要計算新距離的地方更改所有函式呼叫 ⋯ ⋯ 多浪費時間啊!
利用策略模式來解決
如果我們利用策略模式來解決這個問題呢?讓我們看看以下實作:
import Foundation
import CoreGraphics
protocol DistanceStrategy {
var distance: (_ from: CGPoint, _ to: CGPoint) -> CGFloat { get }
}
enum DistanceStrategyType:DistanceStrategy, CaseIterable {
case manhattan
case euclidian
var distance: (_ from: CGPoint, _ to: CGPoint) -> CGFloat {
switch self {
case .manhattan:
return { (_ from: CGPoint, _ to: CGPoint) -> CGFloat in abs(to.x - from.x) + abs(to.y - from.y) }
case .euclidian:
return { (_ from: CGPoint, _ to: CGPoint) -> CGFloat in sqrt(pow(to.x - from.x, 2) + pow(to.y - from.y, 2)) }
}
}
}
struct DistanceCalculator {
var strategy: DistanceStrategy
init(strategy: DistanceStrategyType = .euclidian) {
self.strategy = strategy
}
public func distance(from: CGPoint, to: CGPoint) -> CGFloat {
return self.strategy.distance(from, to)
}
}
var strategyDistanceCalculator = DistanceCalculator()
for strategy in DistanceStrategyType.allCases {
strategyDistanceCalculator.strategy = strategy
print("Strategy \(String(describing: strategy).capitalized) Distance: \(strategyDistanceCalculator.distance(from: .init(x: 0, y: 0), to: .init(x: 4, y: 3)))")
}
// Prints:
// Strategy Manhattan Distance: 7.0
// Strategy Euclidian Distance: 5.0
讓我們逐步了解程式碼所做的事情。
首先,我們定義一個名為 DistanceStrategy
的協定,其用作抽象化 (abstraction),如此一來,即使我們更改提供策略的具體型別,也不必更改整段整式碼。
然後,我們創建一個名為 DistanceStrategyType
的列舉,其中包含兩種情況,分別對應我們的距離計算策略:.euclidian
和 .manhattan
。在列舉裡面,我們為 DistanceStrategy
協定中聲明的 distance
屬性提供一個實作,並讓它回傳不同的方程式,以兩個策略計算距離。
接下來,我們就要創建實際的 DistanceCalculator
。在這篇文章中,我選擇將其作為結構,你也可以按需要將其作為類別。DistanceCalculator
有一個策略變數,變數應該要符合 DistanceStrategy
協定。有了它,我們就可以實作 .distance(_:_:) -> CGFloat
方法,來簡單地計算策略的 distance 屬性,並按接收到的兩個點進行呼叫。
這個做法有兩大好處:
- 我們可以輕易添加距離計算用例,而無須改變其他的程式碼。我們只需要把新的策略插入即可!
- 我們可以改變
DistanceStrategyType
為結構或類別,或者重新命名,甚至實作大量其他方法,但是完全不會影響其他程式碼,只要我們創建的新型別符合DistanceStrategy
協定即可。
本篇文章到此為止,如果你有任何問題,歡迎在下面留言。