- 原文地址:Keep it Simple with the Strategy Design Pattern
- 原文做者:Chidume Nnamdi
- 译文出自:阿里云翻译小组
- 译文连接:github.com/dawn-plex/t…
- 译者:灵沼
- 校对者:也树,照天
面向对象编程是一种编程范式,这种范式围绕使用对象和类声明的方式来为咱们的程序提供简单且可重用的设计。git
根据维基百科:github
“面向对象编程(OOP)是一种基于“对象”概念的编程范式,对象可能包含字段形式的数据,一般称为属性;还有程序形式的代码,一般称为方法。”算法
但 OOP 概念自己不是重点,如何构建你的类以及它们之间的关系才是重点所在。像大脑、城市、蚂蚁窝、建筑这种复杂的系统都充满了各类模式。为了实现稳定持久的状态,它们采用告终构良好的架构。软件开发也不例外。typescript
设计一个大型应用须要对象和数据之间错综复杂的联系和协做。编程
OOP 为咱们提供了这样作的设计,可是正如我以前所说,咱们须要一个模式来达到一个持久稳定的状态。不然在咱们的 OOP 设计应用里可能会出现问题致使代码腐烂。设计模式
所以,这些问题已经被记录归类,而且经验丰富的早期软件开发者已经描述了每类问题的优雅解决方案。这些方案就被称为设计模式。数组
迄今为止,已经有 24 种设计模式,如书中所描述的,设计模式:可复用面向对象软件的基础
。这里每一种模式都为一个特定问题提供了一组解决方案。安全
在这篇文章里,咱们将走进策略模式,去理解它怎样工做,在软件开发中,什么时候去应用它,如何去应用它。bash
提示:在 Bit 上能够更快地构建 JavaScript 应用。在这里能够轻松地共享项目和应用中的组件、与您的团队协做,而且使用它们就像使用Lego同样。这是一个改善模块化和大规模保持代码 DRY 的好方法。架构
策略模式是一种行为型设计模式,它封装了一系列算法,在运行时,从算法池中选择一个使用。算法是可交换的,这意味着它们能够互相替代。
策略模式是一种行为型模式,它能够在运行时选择算法 ——维基百科
关键的想法是建立表明各类策略的对象。这些对象会造成一个策略池,上下文对象能够根据策略进行选择来改变它的行为。这些对象(策略)功能相同、职责单一,而且共同组成策略模式的接口。
以咱们已有的排序算法为例。排序算法有一组彼此特别的规则,来有效地对数字类型的数组进行排序。咱们有一下的排序算法:
仅举几例。
而后,在咱们的计划中,咱们在执行期间同时须要几种不一样的排序算法。使用策略模式容许咱们队这些算法进行分组,而且在须要的时候能够从算法池中进行选择。
这更像一个插件,好比 Windows 中的 PlugnPlay 或者设备驱动程序。全部插件都必须遵循一种签名或规则。
举个例子,一个设备驱动程序能够是任何东西,电池驱动程序,磁盘驱动程序,键盘驱动程序......
它们必须实现:
NTSTATUS DriverEntry (_In_ PDRIVER_OBJECT ob, _In_ PUNICODE_STRING pstr) {
//...
}
VOID DriverUnload(PDRIVER_OBJECT DriverObject)
{
RtlFreeUnicodeString(&servkey);
}
NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo)
{
return STATUS_SOMETHING; // e.g., STATUS_SUCCESS
}
复制代码
每个驱动程序必须实现上面的函数,操做系统使用 DriverEntry加载驱动程序,从内存中删除驱动程序时使用DriverUnload,AddDriver 用于将驱动程序添加到驱动程序列表中。
操做系统不须要知道你的驱动程序作了什么,它所知道的就是因为你称它为驱动程序,它会假设这些全部都存在,并在须要的时候调用它们。
若是咱们把排序算法都集中在一个类中,咱们会发现咱们本身在编写条件语句来选择其中一个算法。
最重要的是,全部的策略必须有相同的签名。若是你使用面向对象语言,必须保证全部的策略都继承自一个通用接口,若是不是使用面向对象语言,好比 JavaScript,请保证全部的策略都有一个上下文环境能够调用的公共方法。
// In an OOP Language -
// TypeScript
// interface all sorting algorithms must implement
interface SortingStrategy {
sort(array);
}
// heap sort algorithm implementing the `SortingStrategy` interface, it implements its algorithm in the `sort` method
class HeapSort implements SortingStrategy {
sort() {
log("HeapSort algorithm")
// implementation here
}
}
// linear search sorting algorithm implementing the `SortingStrategy` interface, it implements its algorithm in the `sort` method
class LinearSearch implements SortingStrategy {
sort(array) {
log("LinearSearch algorithm")
// implementation here
}
}
class SortingProgram {
private sortingStrategy: SortingStrategy
constructor(array: Array<Number>) {
}
runSort(sortingStrategy: SortingStrategy) {
return this.sortingStrategy.sort(this.array)
}
}
// instantiate the `SortingProgram` with an array of numbers
const sortProgram = new SortingProgram([9,2,5,3,8,4,1,8,0,3])
// sort using heap sort
sortProgram.runSort(new HeapSort())
// sort using linear search
sortProgram.runSort(new LinearSearch())
复制代码
SortingProgram
在它的 runSort 方法中,使用 SortingStrategy
做为参数,并调用了 sort
方法。SortingStrategy
的任何具体实现都必须实现 sort
方法。
您能够看到,SP 支持了 SOLID principles,并强制咱们遵循它。SOLID 中的 D 表示咱们必须依赖抽象,而不是具体实现。这就是 runSort
方法中发生的事情。还有 O,它表示实体应该是开放的,而不是扩展的。
若是咱们采用了子类化做为排序算法的替代方案,会获得难以理解和维护的代码,由于咱们会获得许多相关类,它们的差距只在于它们所拥有的算法。SOLID 中的 I,表示对于要实现的具体策略,咱们有一个特定的接口。
这不是针对某一个特定工做虚构的,由于每个排序算法都须要运用排序来排序:)。SOLID 中的 S,表示了实现该策略的全部类都只有一个排序工做。L 则表示了某一个策略的全部子类对于他们的父类都是可替换的。
如上图所示,Context
类依赖于 Strategy
。在执行或运行期间,Strategy
类型不一样的策略被传递给 Context
类。Strategy
提供了策略必须实现的模板。
在上面的 UML 类图中,Concrete
类依赖于抽象,Strategy
接口。它没有直接实现算法。Context
从 runStrategy
方法中调用了 Strategy
传递来的 doAlgorithm
。Context
类独立于 doAlgorithm
方法,它不知道也不必知道 doAlgorithm
是如何实现的。根据 Design by Contract
,实现 Strategy
接口的类必须实现 doAlgorithm
方法。
在策略设计模式中,这里有三个实体:Context、Strategy 和 ConcreteStrategy。
Context 是组成具体策略的主体,策略在这里发挥着它们各自的做用。
Strategy 是定义如何配置全部策略的模板。
ConcreteStrategy 是策略模板(接口)的实现。
使用 Steve Fenton 的示例 Car Wash program
,你知道洗车分不一样的清洗等级,这取决于车主支付的金额,付的钱越多,清洗等级越高。让咱们看一下提供的洗车服务:
基础车轮车身清洗仅仅是常规的清洗和冲洗和刷刷车身。
高档清洗就不只仅是这些,他们会为车身和车轮上蜡,让整个车看起来光彩照人并提供擦干服务。清洗等级取决于车主支付的金额。一级清洗只给你提供基础清洗车身和车轮:
interface BodyCleaning {
clean(): void;
}
interface WheelCleaning {
clean(): void;
}
class BasicBodyCleaningFactory implements BodyCleaning {
clean() {
log("Soap Car")
log("Rinse Car")
}
}
class ExecutiveBodyCleaningFactory implements BodyCleaning {
clean() {
log("Wax Car")
log("Blow-Dry Car")
}
}
class BasicWheelCleaningFactory implements BodyCleaning {
clean() {
log("Soap Wheel")
log("Rinse wheel")
}
}
class ExecutiveWheelCleaningFactory implements BodyCleaning {
clean() {
log("Brush Wheel")
log("Dry Wheel")
}
}
class CarWash {
washCar(washLevel: Number) {
switch(washLevel) {
case 1:
new BasicBodyCleaningFactory().clean()
new BasicWheelCleaningFactory().clean()
break;
case 2:
new BasicBodyCleaningFactory().clean()
new ExecutiveWheelCleaningFactory().clean()
break;
case 3:
new ExecutiveBodyCleaningFactory().clean()
new ExecutiveWheelCleaningFactory().clean()
break;
}
}
}
复制代码
如今你看到了,一些模式出现了。咱们在许多不一样的条件下重复使用相同的类,这些类都相关可是在行为上不一样。此外,咱们的代码变得杂乱且繁重。
更重要的是,咱们的程序违反了 S.O.L.I.D 的开闭原则,开闭原则指出模块应该对 extension
开放而不是 modification
。
对于每个新的清洗等级,就会新增另外一个条件,这就是 modification
。
使用策略模式,咱们必须解除洗车程序与清洗等级的耦合关系。
要作到这一点,咱们必须分离清洗操做。首先,咱们建立一个接口,全部的操做都必须实现它:
interface ValetFaactory {
getWheelCleaning();
getBodyCleaning();
}
复制代码
全部的清洗策略:
class BronzeWashFactory implements ValetFactory {
getWheelCleaning() {
return new BasicWheelCleaning();
}
getBodyCleaning() {
return new BasicBodyCleaning();
}
}
class SilverWashFactory implements ValetFactory {
getWheelCleaning() {
return new BasicWheelCleaning();
}
getBodyCleaning() {
return new ExecutiveBodyCleaning();
}
}
class GoldWashFactory implements ValetFactory {
getWheelCleaning() {
return new ExecutiveWheelCleaning();
}
getBodyCleaning() {
return new ExecutiveBodyCleaning();
}
}
复制代码
接下来,咱们开始改造 CarWashProgram
:
// ...
class CarWashProgram {
constructor(private cleaningFactory: ValetFactory) {
}
runWash() {
const wheelWash = this.cleaningFactory.getWheelCleaning();
wheelWash.cleanWheels();
const bodyWash = this.cleaningFactory.getBodyCleaning();
bodyWash.cleanBody();
}
}
复制代码
如今,咱们把全部所需的清洗策略传递给 CarWashProgram
中,
// ...
const carWash = new CarWashProgram(new GoldWashFactory())
carWash.runWash()
const carWash = new CarWashProgram(new BronzeWashFactory())
carWash.runWash()
复制代码
假设咱们有一个软件,咱们为了安全想为它添加一个身份认证。咱们有不一样的身份验证方案和策略:
咱们也许会试着像下面同样实现:
class BasicAuth {}
class DigestAuth {}
class OpenIDAuth {}
class OAuth {}
class AuthProgram {
runProgram(authStrategy:any, ...) {
this.authenticate(authStrategy)
// ...
}
authenticate(authStrategy:any) {
switch(authStrategy) {
if(authStrategy == "basic")
useBasic()
if(authStrategy == "digest")
useDigest()
if(authStrategy == "openid")
useOpenID()
if(authStrategy == "oauth")
useOAuth()
}
}
}
复制代码
一样的,又是一长串的条件。此外,若是咱们想认证。对于咱们程序中特定的路由,咱们会发现咱们面对相同的状况。
class AuthProgram {
route(path:string, authStyle: any) {
this.authenticate(authStyle)
// ...
}
}
复制代码
若是咱们在这里应用策略设计模式,咱们将建立一个全部认证策略都必须实现的接口:
interface AuthStrategy {
auth(): void;
}
class Auth0 implements AuthStrategy {
auth() {
log('Authenticating using Auth0 Strategy')
}
}
class Basic implements AuthStrategy {
auth() {
log('Authenticating using Basic Strategy')
}
}
class OpenID implements AuthStrategy {
auth() {
log('Authenticating using OpenID Strategy')
}
}
复制代码
AuthStrategy
定义全部策略都必须构建于之上的模板。任何具体认证策略都必须实现这个认证方法,来为咱们提供身份认证的方式。咱们有 Auth0、Basic 和 OpenID 这几个具体策略。
接下来,咱们须要对 AuthProgram 类进行改造:
// ...
class AuthProgram {
private _strategy: AuthStrategy
use(strategy: AuthStrategy) {
this._strategy = strategy
return this
}
authenticate() {
if(this._strategy == null) {
log("No Authentication Strategy set.")
}
this._strategy.auth()
}
route(path: string, strategy: AuthStrategy) {
this._strategy = strategy
this.authenticate()
return this
}
}
复制代码
如今能够看到,authenticate
方法再也不包含一长串的 switch case 语句。use
方法设置要使用的身份验证策略,authenticate
只须要调用 auth
方法。它不关心 AuthStrategy
如何实现的身份认证。
log(new AuthProgram().use(new OpenID()).authenticate())
// Authenticating using OpenID Strategy
复制代码
策略模式能够防止将全部算法都硬编码到程序中。硬编码的方式使得咱们的程序复杂且难以维护和理解。
反过来,硬编码的方式进而让咱们的程序包含一些历来不用的算法。
假设咱们有一个 Printer
类,能够打印不一样的风格和特点。若是咱们在 Printer
类中包含全部的风格和特点:
class Document {...}
class Printer {
print(doc: Document, printStyle: Number) {
if(printStyle == 0 /* color printing*/) {
// ...
}
if(printStyle == 1 /* black and white printing*/) {
// ...
}
if(printStyle == 2 /* sepia color printing*/) {
// ...
}
if(printStyle == 3 /* hue color printing*/) {
// ...
}
if(printStyle == 4 /* oil printing*/) {
// ...
}
// ...
}
}
复制代码
或者
class Document {...}
class Printer {
print(doc: Document, printStyle: Number) {
switch(printStyle) {
case 0 /* color priniting strategy*/:
ColorPrinting()
break;
case 0 /* color priniting strategy*/:
InvertedColorPrinting()
break;
// ...
}
// ...
}
}
复制代码
看吧,咱们最后获得了一个不正宗的类,这个类有太多条件了,是不可读、不可维护的。
可是应用策略模式的话,咱们将打印方式分解为不一样的任务。
class Document {...}
interface PrintingStrategy {
printStrategy(d: Document): void;
}
class ColorPrintingStrategy implements PrintingStrategy {
printStrategy(doc: Document) {
log("Color Printing")
// ...
}
}
class InvertedColorPrintingStrategy implements PrintingStrategy {
printStrategy(doc: Document) {
log("Inverted Color Printing")
// ...
}
}
class Printer {
private printingStrategy: PrintingStrategy
print(doc: Document) {
this.printingStrategy.printStrategy(doc)
}
}
复制代码
所以,每一个条件都转移到了一个单独的策略类中,而不是一大串条件。对 Printer
类来讲,它没有必要知道不一样打印方式是怎么实现的。
在策略模式中,组合一般优于继承。它建议对抽象进行编程而不是对实体编程。你会看到策略模式与 SOLID 原则的完美结合。
例如,咱们有一个 DoorProgram
,它有不一样的锁定机制来锁门。因为不一样的锁定机制在门的子类之间能够改变。咱们也许会试图像下面这样来应用门的锁定机制到 Door
类:
class Door {
open() {
log('Opening Door')
// ...
}
lock() {
log('Locking Door')
}
lockingMechanism() {
// card swipe
// thumbprint
// padlock
// bolt
// retina scanner
// password
}
}
复制代码
只看起来还不错,可是每一个门的行为不一样。每一个门都有本身的锁定和开门机制。这是不一样的行为。
当咱们建立不一样的门:
// ...
class TimedDoor extends Door {
open() {
super.open()
}
}
复制代码
而且尝试为它实现打开/锁定机制,你会发现咱们在实现它本身的打开/锁定机制以前,必须调用父类的方法。
若是咱们像下面同样建立了一个接口 Door
:
interface Door {
open()
lock()
}
复制代码
你会看到必须在每一个类或模型或 Door
类型的类中声明打开/锁定的行为。
class GlassDoor implements Door {
open() {
// ...
}
lock() {
// ...
}
}
复制代码
这很不错,可是随着应用程序的增加,这里会暴露许多弊端。一个 Door 模型必须有一个打开/锁定机制。一个门必须能打开/关闭吗?不是的。一扇门也许根本就没必要关上。因此会发现咱们的 Door 模型将会被强制
设置打开/锁定机制。
接下来,接口不会对接口做为模型使用和做为打开/锁定机制使用作区分。注意:在 S in SOLID 中,一个类必须拥有一个能力。
玻璃门必须具备做为玻璃门的惟一特征,木门、金属门、陶瓷门也是一样的。另外的类应该负责打开/锁定机制。
使用策略模式,咱们将咱们相关的东西都分开,在这个例子中,就是将打开/锁定机制分开。进入类中,而后在运行期间,咱们为 Door 模型传递它所须要使用的锁定/打开机制。Door 模型可以从锁定/打开策略池中选择一个锁定/打开装置来使用。
interface LockOpenStrategy {
open();
lock();
}
class RetinaScannerLockOpenStrategy implements LockOpenStrategy {
open() {
//...
}
lock() {
//...
}
}
class KeypadLockOpenStrategy implements LockOpenStrategy {
open() {
if(password != "nnamdi_chidume"){
log("Entry Denied")
return
}
//...
}
lock() {
//...
}
}
abstract class Door {
public lockOpenStrategy: LockOpenStrategy
}
class GlassDoor extends Door {}
class MetalDoor extends Door {}
class DoorAdapter {
openDoor(d: Door) {
d.lockOpenStrategy.open()
}
}
const glassDoor = new GlassDoor()
glassDoor.lockOpenStrategy = new RetinaScannerLockOpenStrategy();
const metalDoor = new MetalDoor()
metalDoor.lockOpenStrategy = new KeypadLockOpenStrategy();
new DoorAdapter().openDoor(glassDoor)
new DoorAdapter().openDoor(metalDoor)
复制代码
每个打开/锁定策略都在一个继承自基础接口的类中定义。策略模式支持这一点,由于面向接口编程能够实现高内聚性。
接下来,咱们会有 Door 模型,每一个 Door 模型都是 Door 类的一个子类。咱们有一个 DoorAdapter
,它的工做就是打开传递给它的门。咱们建立了一些 Door 模型的对象,而且设置了它们的锁定/打开策略。玻璃门经过视网膜扫描来进行锁定/打开,金属门有一个输入密码的键盘。
咱们在这里关注的分离,是相关行为的分离。每一个 Door 模型不知道也不关心一个具体锁定/打开策略的实现,这个问题由另外一个实体来关注。咱们按照策略模式的要求面向接口编程,由于这使得在运行期间切换策略变得很容易。
这可能不会持续好久,可是这是一种经由策略模式提供的更好的方式。
一扇门也许会有不少锁定/打开策略,而且可能会在锁定和打开运行期间使用到一个或多个策略。不管如何,你必定要在脑海中记住策略模式。
咱们的大部分示例都是基于面向对象编程语言。JavaScript 不是静态类型而是动态类型。因此在 JavaScript 中没有像 接口、多态、封装、委托这样的面向对象编程的概念。可是在策略模式中,咱们能够假设他们存在,咱们能够模拟它们。
让咱们用咱们的第一个示例来示范如何在 JavaScript 中应用策略模式。
第一个示例是基于排序算法的。如今,SortingStrategy
接口有一个 sort
方法,全部实现的策略都必须定义。SortingProgram类将
SortingStrategy 做为参数传递给它的
runSort方法,而且调用了
sort` 方法。
咱们对排序算法进行建模:
var HeapSort = function() {
this.sort(array) {
log("HeapSort algorithm")
// implementation here
}
}
// linear search sorting algorithm implementing its alogrithm in the `sort` method
var LinearSearch = function() {
this.sort(array) {
log("LinearSearch algorithm")
// implementation here
}
}
class SortingProgram {
constructor(array) {
this.array=array
}
runSort(sortingStrategy) {
return sortingStrategy.sort(this.array)
}
}
// instantiate the `SortingProgram` with an array of numbers
const sortProgram = new SortingProgram([9,2,5,3,8,4,1,8,0,3])
// sort using heap sort
sortProgram.runSort(new HeapSort())
// sort using linear search
sortProgram.runSort(new LinearSearch())
复制代码
这里没有接口,但咱们实现了。可能会有一个更好更健壮的方法,可是对如今来讲,这已经足够了。
这里我想的是,对于咱们想要实现的每个排序策略,都必须有一个排序方法。
当你开始注意到反复出现的算法,可是又互相有不一样的时候,就是策略模式使用的时机了。经过这种方式,你须要将算法拆分红不一样的类,并按需提供给程序。
而后就是,若是你注意到在相关算法中反复出现条件语句。
当你的大部分类都有相关的行为。是时候将它们拆分到各类类中了。
策略模式是许多软件开发设计模式的其中一种。在本文中,咱们看到了许多关于如何使用策略模式的示例,而后,咱们看到了它的优点和弊端。
记住了,你没必要按照描述来实现一个设计模式。你须要彻底理解它并知道应用它的时机。若是你不理解它,不要担忧,屡次使用它以加深理解。随着时间的推移,你会掌握它的窍门,最后,你会领略到它的好处。
接下来,在咱们的系列中,咱们将会研究 模板方法设计模式,请继续关注:)
若是你对此有任何疑问,或者我还应该作些补充、订正、删除,请随时发表评论、邮件或 DM me。感谢阅读!👏