「譯」使用策略設計模式來簡化代碼

使用策略設計模式來簡化代碼

面向對象編程是一種編程範式,這種範式圍繞使用對象和類聲明的方式來爲咱們的程序提供簡單且可重用的設計。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 接口。它沒有直接實現算法。ContextrunStrategy 方法中調用了 Strategy 傳遞來的 doAlgorithmContext 類獨立於 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()
複製代碼

另外一個示例:認證策略

假設咱們有一個軟件,咱們爲了安全想爲它添加一個身份認證。咱們有不一樣的身份驗證方案和策略:

  • Basic
  • Digest
  • OpenID
  • OAuth

咱們也許會試着像下面同樣實現:

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 原則

在策略模式中,組合一般優於繼承。它建議對抽象進行編程而不是對實體編程。你會看到策略模式與 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 中沒有像 接口、多態、封裝、委託這樣的面向對象編程的概念。可是在策略模式中,咱們能夠假設他們存在,咱們能夠模擬它們。

讓咱們用咱們的第一個示例來示範如何在 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。感謝閱讀!👏

參考

相關文章
相關標籤/搜索