设计模式二:策略模式(Strategy Pattern)
一、原理理解
在开发中,有许多算法可以实现某一功能,如查找、排序等,一种常用的方法是硬编码在一个类中,如需要提供多种查找算法,可以将这些算法写到一个类中,在该类中提供多个方法,每一个方法对应一个具体的查找算法。除此也可以将这些查找算法封装在一个统一的方法中,通过if…else…等条件判断语句来进行选择。这两种实现方法我们都可以称之为硬编码,如果需要增加一种新的查找算法,则需要修改我们所封装的那个类的源代码。更换查找算法,也需要修改客户端调用代码。在这个自定义类中封装了大量查找算法,该类代码很复杂,维护起来很困难。
为了解决这些问题,可以定义一些独立的类来封装不同的算法,每一个类封装一个具体的算法。在这里,每一个封装算法的类我们都可以称之为策略(Strategy)。为了保证这些策略的一致性,一般会用一个抽象的策略类来做算法的定义,而具体每种算法则对应一个具体策略类。
策略模式(Strategy Pattern):定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy Pattern)。
策略模式包含如下角色:
- Context: 环境类
- Strategy: 抽象策略类
- ConcreteStrategy: 具体策略类
如图所示:
Strategy
策略模式是对算法的封装,它把算法的责任和算法本身分割开,委派给不同的对象管理。策略模式,就是 “准备一组算法,并将每一个算法封装起来,使得它们可以互换”。
在策略模式中,应当由客户端自己决定在什么情况下使用什么具体策略角色。
策略模式仅仅封装算法,提供将新算法插入到已有系统中,以及从系统中删除老算法。策略模式并不决定在何时使用何种算法,算法的选择由客户端来决定。这一定程度上提高了系统的灵活性,但是客户端需要理解所有具体策略类之间的区别,以便选择在不同情景选择最合适的算法,这也是策略模式的缺点之一,在一定程度上增加了客户端的使用难度。
二、实践示例
本例实现一个答题 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()
}
三、小结
strategy pattern- 什么时候使用策略模式?
当有两个或多个可选择的不同行为时,可使用策略模式。
策略模式类似于 delegation 模式:两种模式都依赖于协议而不是具体对象来提高灵活性。 因此,任何实现策略协议的对象都可以在运行时用作策略。
与 delegation 不同,策略模式使用一系列对象。
delegation 经常在运行时就已经确定了, 例如,可以从Interface Builder 设置 UITableView 的 dataSource 和delegate,并且在运行时很少更改它们。
但是,策略旨在在运行时轻松互换。
- 注意:
- 策略模式定义了一系列可在运行时设置或切换的可互换对象。
- 此模式包含三个部分:使用策略的对象,策略协议和一系列策略对象。
- 策略模式类似于委托模式:两种模式都使用协议来实现灵活性。 然而,与委托模式不同,策略意味着在运行时切换,而委托通常是固定不变的。
参考: