一、原理理解
在開發中,有許多算法可以實現某一功能,如查找、排序等,一種常用的方法是硬編碼在一個類中,如需要提供多種查找算法,可以將這些算法寫到一個類中,在該類中提供多個方法,每一個方法對應一個具體的查找算法。除此也可以將這些查找算法封裝在一個統一的方法中,通過if…else…等條件判斷語句來進行選擇。這兩種實現方法我們都可以稱之爲硬編碼,如果需要增加一種新的查找算法,則需要修改我們所封裝的那個類的源代碼。更換查找算法,也需要修改客戶端調用代碼。在這個自定義類中封裝了大量查找算法,該類代碼很複雜,維護起來很困難。
爲了解決這些問題,可以定義一些獨立的類來封裝不同的算法,每一個類封裝一個具體的算法。在這裏,每一個封裝算法的類我們都可以稱之爲策略(Strategy)。爲了保證這些策略的一致性,一般會用一個抽象的策略類來做算法的定義,而具體每種算法則對應一個具體策略類。
策略模式(Strategy Pattern):定義一系列算法,將每一個算法封裝起來,並讓它們可以相互替換。策略模式讓算法獨立於使用它的客戶而變化,也稱爲政策模式(Policy Pattern)。
策略模式包含如下角色:
- Context: 環境類
- Strategy: 抽象策略類
- ConcreteStrategy: 具體策略類
如圖所示:
策略模式是對算法的封裝,它把算法的責任和算法本身分割開,委派給不同的對象管理。策略模式,就是 “準備一組算法,並將每一個算法封裝起來,使得它們可以互換”。
在策略模式中,應當由客戶端自己決定在什麼情況下使用什麼具體策略角色。
策略模式僅僅封裝算法,提供將新算法插入到已有系統中,以及從系統中刪除老算法。策略模式並不決定在何時使用何種算法,算法的選擇由客戶端來決定。這一定程度上提高了系統的靈活性,但是客戶端需要理解所有具體策略類之間的區別,以便選擇在不同情景選擇最合適的算法,這也是策略模式的缺點之一,在一定程度上增加了客戶端的使用難度。
二、實踐示例
本例實現一個答題 demo。用戶點擊答題時候,可以選擇是隨機答題還是順序答題。
項目結構如下:
- 首先定義 QuestionStrategy 協議:
public protocol QuestionStrategy: class {
var title: String { get }
var correctCount: Int { get }
var incorrectCount: Int { get }
func advanceToNextQuestion() -> Bool
func currentQuestion() -> Question
func markQuestionCorrect(_ question: Question)
func markQuestionIncorrect(_ question: Question)
func questionIndexTitle() -> String
}
- 分別實現隨機答題、順序答題算法:
RandomQuestionStrategy:
import GameplayKit.GKRandomSource
public class RandomQuestionStrategy: QuestionStrategy {
// MARK: - Properties
public var correctCount: Int = 0
public var incorrectCount: Int = 0
private let questionGroup: QuestionGroup
private var questionIndex = 0
private let questions: [Question]
// MARK: - Object Lifecycle
public init(questionGroup: QuestionGroup) {
self.questionGroup = questionGroup
let randomSource = GKRandomSource.sharedRandom()
self.questions =
randomSource.arrayByShufflingObjects(
in: questionGroup.questions) as! [Question]
}
// MARK: - QuestionStrategy
public var title: String {
return questionGroup.title
}
public func currentQuestion() -> Question {
return questions[questionIndex]
}
public func advanceToNextQuestion() -> Bool {
guard questionIndex + 1 < questions.count else {
return false
}
questionIndex += 1
return true
}
public func markQuestionCorrect(_ question: Question) {
correctCount += 1
}
public func markQuestionIncorrect(_ question: Question) {
incorrectCount += 1
}
public func questionIndexTitle() -> String {
return "\(questionIndex + 1)/\(questions.count)"
}
}
SequentialQuestionStrategy:
public class SequentialQuestionStrategy: QuestionStrategy {
// MARK: - Properties
public var correctCount: Int = 0
public var incorrectCount: Int = 0
private let questionGroup: QuestionGroup
private var questionIndex = 0
// MARK: - Object Lifecycle
public init(questionGroup: QuestionGroup) {
self.questionGroup = questionGroup
}
// MARK: - QuestionStrategy
public var title: String {
return questionGroup.title
}
public func currentQuestion() -> Question {
return questionGroup.questions[questionIndex]
}
public func advanceToNextQuestion() -> Bool {
guard questionIndex + 1 <
questionGroup.questions.count else {
return false
}
questionIndex += 1
return true
}
public func markQuestionCorrect(_ question: Question) {
correctCount += 1
}
public func markQuestionIncorrect(_ question: Question) {
incorrectCount += 1
}
public func questionIndexTitle() -> String {
return "\(questionIndex + 1)/" +
"\(questionGroup.questions.count)"
}
}
這兩個策略同時遵循 QuestionStrategy ,只是對算法的實現不同,一個是隨機生成題目,一個是順序生成題目。
- 使用方法:
當從 SelectQuestionGroupViewController 頁面 push 到QuestionViewController 時候,把自己選好的 QuestionStrategy (或者順序、或者隨機)賦值給 QuestionViewController。
public override func prepare(for segue: UIStoryboardSegue,
sender: Any?) {
guard let viewController = segue.destination
as? QuestionViewController else { return }
// 這裏根據需要選擇具體的實現策略
viewController.questionStrategy = SequentialQuestionStrategy(
questionGroup: selectedQuestionGroup)
viewController.delegate = self
}
- QuestionViewController 頁面調用相應的方法:
@IBAction func handleCorrect(_ sender: Any) {
let question = questionStrategy.currentQuestion()
questionStrategy.markQuestionCorrect(question)
questionView.correctCountLabel.text =
String(questionStrategy.correctCount)
showNextQuestion()
}
@IBAction func handleIncorrect(_ sender: Any) {
let question = questionStrategy.currentQuestion()
questionStrategy.markQuestionIncorrect(question)
questionView.incorrectCountLabel.text =
String(questionStrategy.incorrectCount)
showNextQuestion()
}
三、小結
- 什麼時候使用策略模式?
當有兩個或多個可選擇的不同行爲時,可使用策略模式。
策略模式類似於 delegation 模式:兩種模式都依賴於協議而不是具體對象來提高靈活性。 因此,任何實現策略協議的對象都可以在運行時用作策略。
與 delegation 不同,策略模式使用一系列對象。
delegation 經常在運行時就已經確定了, 例如,可以從Interface Builder 設置 UITableView 的 dataSource 和delegate,並且在運行時很少更改它們。
但是,策略旨在在運行時輕鬆互換。
- 注意:
- 策略模式定義了一系列可在運行時設置或切換的可互換對象。
- 此模式包含三個部分:使用策略的對象,策略協議和一系列策略對象。
- 策略模式類似於委託模式:兩種模式都使用協議來實現靈活性。 然而,與委託模式不同,策略意味着在運行時切換,而委託通常是固定不變的。
參考: