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

阿里雲前端發表於2019-01-20

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

物件導向程式設計是一種程式設計正規化,這種正規化圍繞使用物件和類宣告的方式來為我們的程式提供簡單且可重用的設計。

根據維基百科:

“物件導向程式設計(OOP)是一種基於“物件”概念的程式設計正規化,物件可能包含欄位形式的資料,通常稱為屬性;還有程式形式的程式碼,通常稱為方法。”

但 OOP 概念本身不是重點,如何構建你的類以及它們之間的關係才是重點所在。像大腦、城市、螞蟻窩、建築這種複雜的系統都充滿了各種模式。為了實現穩定持久的狀態,它們採用了結構良好的架構。軟體開發也不例外。

設計一個大型應用需要物件和資料之間錯綜複雜的聯絡和協作。

OOP 為我們提供了這樣做的設計,但是正如我之前所說,我們需要一個模式來達到一個持久穩定的狀態。否則在我們的 OOP 設計應用裡可能會出現問題導致程式碼腐爛。

因此,這些問題已經被記錄歸類,並且經驗豐富的早期軟體開發者已經描述了每類問題的優雅解決方案。這些方案就被稱為設計模式

迄今為止,已經有 24 種設計模式,如書中所描述的,設計模式:可複用物件導向軟體的基礎。這裡每一種模式都為一個特定問題提供了一組解決方案。

在這篇文章裡,我們將走進策略模式,去理解它怎樣工作,在軟體開發中,何時去應用它,如何去應用它。

提示:在 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。感謝閱讀!?

參考

相關文章