策略模式 (Strategy Pattern)簡介 讓程式碼拓展起來更容易


本篇原文(標題:Understanding the Strategy Pattern )刊登於作者 Medium,由 Jimmy M Andersson 所著,並授權翻譯及轉載。

我們在編寫類別時,有時會用上大量看上去很相似的方法,但礙於它們在計算方式上存在關鍵的差異,讓我們無法編寫一個通用函數,而刪減其他的函數。今天,讓我帶大家看看一種設計模式,它讓我們可以創建一個函數,來管理所有函數,如此一來,我們就可以刪除那些幾乎一模一樣的方法 ── 那就是策略模式 (Strategy Pattern)。

什麼是策略模式?

策略模式是屬於物件導向程式設計 (Object-oriented Programming, OOP) 結構,是行為模式 (behavioral pattern) 的一種,也被稱為 Policy Pattern。它背後的邏輯是,程式應該能夠延遲在運行時才選擇演算法,並在編譯器完成工作之後才執行,以免被演算過程打擾。

讓我們做個比喻,假設你要計算幾條非常困難的數學方程式,要買 4 部不同的計算機才能解決四式運算,這不是很麻煩嗎(而且非常不環保)?你應該也會覺得,一部可以根據問題切換不同計算方法的計算機更好吧!這正是策略模式的基礎。

程式碼範例

以下的程式碼範例是構建一個距離計算機,目的是計算兩點之間的距離。要計算兩點之間的距離,通常會使用歐氏距離 (Euclidian Metric)。但是,實際上還有很多種方法可以計算,具體取決於兩點之間的空間看起來像甚麼。

讓我們簡單一點來使用曼哈頓距離 (Manhattan distance),也被稱為 Snake DistanceCity 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

In this attempt, we’re creating an enum that contains a few static functions for us to use (we’re using a case-less enum because it can’t be instantiated). Each function gives us access to a different way of calculating the distance between two points, and all is well… almost.

在上面的程式碼中,我們創建了一個列舉 (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 屬性,並按接收到的兩個點進行呼叫。

這個做法有兩大好處:

  1. 我們可以輕易添加距離計算用例,而無須改變其他的程式碼。我們只需要把新的策略插入即可!
  2. 我們可以改變 DistanceStrategyType 為結構或類別,或者重新命名,甚至實作大量其他方法,但是完全不會影響其他程式碼,只要我們創建的新型別符合 DistanceStrategy 協定即可。

本篇文章到此為止,如果你有任何問題,歡迎在下面留言。

本篇原文(標題:Understanding the Strategy Pattern)刊登於作者 Medium,由 Jimmy M Andersson 所著,並授權翻譯及轉載。

作者簡介:Jimmy M Andersson 是一名軟件開發人員,在活躍於汽車行業的 NIRA Dynamics 中負責數據採集。他開發了監控和日誌記錄 App,來演示及可視化公司的產品組合和其功能。他目前正在進修資訊科技碩士學位,目標是以數據科學專業畢業。他每週都會在 Medium 發表軟件開發文章。你也可以在 Twitter 或 Email 聯絡他。

譯者簡介:Kelly Chan-AppCoda 編輯小姐。


此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。

blog comments powered by Disqus
Shares
Share This