1. 程式人生 > 實用技巧 >《JavaScript 模式》讀書筆記(7)— 設計模式2

《JavaScript 模式》讀書筆記(7)— 設計模式2

  這一篇我們主要來學習裝飾者模式、策略模式以及外觀模式。其中裝飾者模式稍微複雜一點,大家認真閱讀,要自己動手去實現一下哦。

四、裝飾者模式

  在裝飾者模式中,可以在執行時動態新增附加功能到物件中。當處理靜態類時,這可能是一個挑戰。在JavaScript中,由於物件是可變的,因此,新增功能到物件中的過程本身並不是問題。

  裝飾者模式的一個比較方便的特徵在於其預期行為的可定製和可配置特性。可以從僅具有一些基本功能的普通物件開始,然後從可用裝飾資源池中選擇需要用於增強普通物件的那些功能,並且按照順序進行裝飾,尤其是當裝飾順序很重要的時候。

用法

  讓我們仔細看一下該模式的使用示例。假設正在開發一個銷售商品的web應用,每一筆新銷售都是一個新的sale物件。該物件“知道”有關專案的價格,並且可以通過呼叫sale.getPrice()方法返回其價格。根據不同的情況,可以用額外的功能裝飾這個物件。試想這樣一個場景,銷售客戶在加拿大的魁北克省。在這種情況下,買方需要支付聯邦稅和魁北克省稅。遵循這種裝飾者模式,您會說需要使用聯邦稅和魁北克省稅裝飾者來“裝飾”這個物件。然後,可以用價格格式化功能裝飾該物件。這種應用場景看起來如下所示:

var sale = new Sale(100); //該價格為100美元
sale = sale.decorate('fedtax'); //增加聯邦稅
sale = sale.decorate('quebec'); //增加省級稅
sale = sale.decorate('money'); //格式化成美元貨幣形式
sale.getPrice(); //"$112.88"

  在另一種情況下,買方可能在一個沒有省稅的省份,並且您可能也想使用加元的形式對其價格進行格式化,因此,您可以按照下列方式這樣做:

var sale = new Sale(100); //該價格為100美元
sale = sale.decorate('fedtax'); //
增加聯邦稅 sale = sale.decorate('CDN'); //格式化為CDN貨幣形式 sale.getPrice(); //"CDN$ 105.00"

  正如您所看到的,這是一種非常靈活的方法,可用於增加功能以及調整執行時物件。讓我們來看看如何處理該模式的實現。

實現

  實現裝飾者模式的其中一個方法是使得每個裝飾者成為一個物件,並且該物件包含了應該被過載的方法。每個裝飾者實際上繼承了目前已經被前一個裝飾者進行增強後的物件。每個裝飾方法在uber(繼承的物件)上呼叫了相同的方法並獲取其值,此外它還繼續執行了一些操作。

  最終的結果是,當您在第一個用法示例中執行sale.getPrice()時,呼叫了money裝飾者的方法(見下圖)。但是由於每個裝飾方法首先呼叫父物件的方法,money的getPrice()將會首先呼叫quebec的getPrice(),這有需要依次呼叫fedtax的getPrice()等。該呼叫鏈一路攀升到由Sale()建構函式所實現的原始未經修飾的getPrice()。

  該實現是從一個建構函式和一個原型方法開始的:

function Sale(price) {
    this.price = price || 100;
}

Sale.prototype.getPrice = function() {
    return this.price;
};

  裝飾者物件都將以建構函式的屬性這種方式來實現:

Sale.decorators = {};

  讓我們看一個裝飾者示例。它是一個實現了自定義getPrice()方法的物件。請注意,該方法首先會從父物件的方法中獲取值,然後再修改該值:

Sale.decorators.fedtax = {
    getPrice: function () {
        var price = this.uber.getPrice();
        price += price * 5 / 100;
        return price;
    }
}

  同樣的,我們可以實現其他裝飾者,其數量由需求來定。它們可以是核心Sale()功能的擴充套件,也可以實現成外掛。它們設定可以“駐留”在其他檔案中,並且可被第三方開發人員所開發和共享。

Sale.decorators.quebec = {
    getPrice: function () {
        var price = this.uber.getPrice();
        price += price * 7.5 / 100;
        return price;
    }
}

Sale.decorators.money = {
    getPrice: function () {
        return '$' + this.uber.getPrice().toFixed(2);
    }
}

Sale.decorators.cdn = {
    getPrice: function () {
        return 'CDN$ ' + this.uber.getPrice().toFixed(2);
    }
}

  最後,讓我們看看稱之為decorate()的“神奇”方法,它可以將所有的塊拼接在一起。請記住,呼叫該方法的方式如下:

sale = sale.decorate("fedtax");

  "fedtax"字串將對應於Sale.decorators.fedtax中實現的物件。新裝飾的物件newobj將繼承目前我們所擁有的物件(無論是原始物件,還是已經添加了最後的修飾者的物件),這也就是物件this。為了完成繼承部分的程式碼,我們使用了前一章中的臨時建構函式模式。首先,我們也設定了newobj的uber屬性,以便於子物件可以訪問父物件,然後,我們從裝飾者中將所有的額外屬性複製到新裝飾的物件newobj中。最後,返回newobj,而在我們具體的用法例子中,它實際上成為了更新的sale物件。

Sale.prototype.decorate = function (decorator) {
    var F = function () {},
        overrides = this.constructor.decorators[decorator],
        i,newobj;
    
    F.prototype = this;
    newobj = new F();
    newobj.uber = F.prototype;
    for(i in overrides) {
        if(overrides.hasOwnProperty(i)) {
            newobj[i] = overrides[i];
        }
    }
    return newobj;
}

使用列表實現

  現在讓我們探討一個稍微不同的實現方法,它利用來JavaScript語言的動態性質,並且根本不需要使用繼承。此外,並不是使每個裝飾方法呼叫鏈中前面的方法,我們可以簡單將前面方法的結果作為引數傳遞到下一個方法。

  這種實現方法還可以很容易的支援反裝飾(undecorating)或撤銷裝飾,這意味著可以簡單的從裝飾者列表中刪除一個專案。

  下面的用法示例將略微簡單一點,這是由於我們沒有將從decorate()返回的值賦給物件。在這個實現中,decorate()並沒有對該物件執行任何操作,它只是將返回的值追加到列表中:

var sale = new Sale(100); //該價格為100美元
sale.decorate('fedtax'); //增加聯邦稅
sale.decorate('quebec'); //增加省級稅
sale.decorate('money'); //格式化為美元貨幣形式
console.log(sale.getPrice()); //"$112.88"

  現在,Sale()建構函式中有一個裝飾者列表並以此作為自身的屬性:

function Sale(price) {
    this.price = (price > 0) || 100;
    this.decorators_list = [];
}

  可用裝飾者將再次以Sale.decorators屬性的方式實現。請注意,現在getPrice()方法變得更為簡單了,這是因為它們並沒有呼叫父物件的getPrice()以獲得中間結果,而這個結果將作為引數傳遞給它們:

Sale.decorators = {};

Sale.decorators.fedtax = {
    getPrice: function (price) {
        return price + price * 5 / 100;
    }
};

Sale.decorators.quebec = {
    getPrice: function (price) {
        return price + price * 7.5 / 100;
    }
};

Sale.decorators.money = {
    getPrice: function (price) {
        return "$" + peice.toFixed(2);
    }
};

  在下面的程式碼中,有趣的部分發生在父物件的decorate()和getPrice()方法中。在以前的實現中,decorate()具有一定的複雜性,而getPrice()卻是相當的簡單。然而,在本實現中卻採用了恰好相反的方式:decorate()僅用於追加列表,而getPrice()卻完成所有工作。這些工作包括遍歷當前新增的裝飾者以及呼叫每個裝飾者的getPrice()方法、傳遞從前一個方法中獲得的結果。

  下面是一個完成的例子:

function Sale(price) {
    this.price = (price > 0) || 100;
    this.decorators_list = [];
}

Sale.decorators = {};

Sale.decorators.fedtax = {
    getPrice: function (price) {
        return price + price * 5 / 100;
    }
};

Sale.decorators.quebec = {
    getPrice: function (price) {
        return price + price * 7.5 / 100;
    }
};

Sale.decorators.money = {
    getPrice: function (price) {
        return "$" + price.toFixed(2);
    }
}

Sale.prototype.decorate = function(decorator) {
    this.decorators_list.push(decorator);
};

Sale.prototype.getPrice = function () {
    var price = this.price,
        i,
        max = this.decorators_list.length,
        name;
    
    for(i = 0; i < max;i += 1) {
        name = this.decorators_list[i];
        price = Sale.decorators[name].getPrice(price);
    }
    return price;
};

var sale = new Sale(100); //該價格為100美元
sale.decorate('fedtax'); //增加聯邦稅
sale.decorate('quebec'); //增加省級稅
sale.decorate('money'); //格式化為美元貨幣形式
console.log(sale.getPrice());

  裝飾者模式的第二種實現方法更為簡單,並且也不涉及繼承。此外,裝飾方法也是非常簡單的。所有這些工作都是通過“同意”被裝飾的那個方法來完成的。在這個實現示例中,getPrice()是唯一允許裝飾的方法。如果想擁有更多可以被裝飾的方法,那麼每個額外的裝飾方法都需要重複遍歷裝飾者列表這一部分的程式碼。然而,這很容易抽象成一個輔助方法,通過它來接受方法並使其成為“可裝飾”的方法。在這樣的實現中,sale中的decorators_list屬性變成了一個物件,且該物件中的每個屬性都是以裝飾物件陣列中的方法和值命名。

五、策略模式

  策略模式支援您在執行時選擇演算法。程式碼的客戶端可以使用同一個介面來工作,但是它卻根據客戶正在試圖執行任務的上下文,從多個演算法中選擇用於處理特定任務的演算法。

  使用策略模式的其中一個例子是解決表單驗證的問題。可以建立一個具有validate()方法的驗證器(validator)物件。無論表單的具體型別是什麼,該方法都將會被呼叫,並且總是返回相同的結果,一個未經驗證的資料列表以及任意的錯誤訊息。

  但是根據具體的表單形勢以及待驗證的資料,驗證其的客戶端可能選擇不同型別的檢查方法。驗證其將選擇最佳的策略(strategy)以處理任務,並且將具體的資料驗證委託給適當的演算法。

資料驗證示例

  假設有以下資料塊,他可能來自於網頁上的一個表單,而您需要驗證它是否有效:

var data = {
    first_name: 'Super',
    last_name: 'Man',
    age: 'unknown',
    username: 'o_O'
};

  在這個具體的例子中,為了使驗證器知道什麼是最好的策略,首先需要配置該驗證器,並且設定認為是有效的且可接受的規則。

  比如說在表單驗證中,您對姓氏不作要求且接受任意字元作為名字,但是要求年齡必須為數字,並且使用者名稱中僅出現字母和數字且無特殊符號。該配置如下所示:

validator.config = {
    first_name: 'isNonEmpty',
    age: 'isNumber',
    username: 'isAlphaNum'
};

  現在,validator物件已經配置完畢並可用於資料處理,您可以呼叫validator物件的validate()方法並將任意驗證錯誤資訊列印到控制檯中:

validator.validate(data);
if(validator.hasErrors()) {
    console.log(validator.messages.join("\n"));
}

  上述語句將會打印出下列錯誤訊息:

Invalid value for *age*, the value can only be a valid number,e.g. 1, 3.14 or 2010
Invalud value fror *username*, the value can only contain characters and numbers, no special symols

  現在,讓我們看看驗證程式是如何實現該validator的。用於檢查的可用演算法也是物件,它們具有一個預定義的介面, 提供了一個validate()方法和一個可用於提示錯誤訊息的一行幫助資訊。

// 非空值的檢查
validator.types.isNonEmpty = {
    validate: function(value) {
        return value !== '';
    },
    instructions: 'the value cannot be empty'
};

// 檢查值是否是一個數字
validator.types.isNumber = {
    validate: function(value) {
        return !isNaN(value);
    },
    instructions: 'the value can only be a valid number,e.g. 1, 3.14 or 2010'
};

// 檢查該值是否只包含字母和數字
validator.types.isAlphaNum = {
    validate: function(value) {
        return !/[^a-z0-9]/i.test(value);
    },
    instructions: 'the value can only contain characters and numbers, no special symols'
};

  最後,核心的validator物件如下所示(這是一個完整的例子):

var validator = {
    // 所有可用的檢查
    types: {},
    // 在當前驗證會話中的錯誤訊息
    messages: [],
    // 當前驗證配置 
    // 名稱:驗證型別
    config: {},

    // 介面方法
    // ‘data’為鍵值對
    validate: function (data) {
        var i, msg, type, checker, result_ok;

        // 重置所有訊息
        this.messages = [];
        for (i in data) {
            if (data.hasOwnProperty(i)) {
                type = this.config[i];
                checker = this.types[type];

                if (!type) {
                    continue; // 不需要驗證
                }
                if (!checker) { //uh-oh
                    throw {
                        name: 'ValidationError',
                        messgae: 'No handler to validate type' + type
                    };
                }
                result_ok = checker.validate(data[i]);
                if (!result_ok) {
                    msg = "Invalid value for *" + i + "*," + checker.instructions;
                    this.messages.push(msg);
                }
            }
        }
        return this.hasErrors();
    },
    // 幫助程式
    hasErrors: function () {
        return this.messages.length !== 0;
    }
}

// 非空值的檢查
validator.types.isNonEmpty = {
    validate: function (value) {
        return value !== '';
    },
    instructions: 'the value cannot be empty'
};

// 檢查值是否是一個數字
validator.types.isNumber = {
    validate: function (value) {
        return !isNaN(value);
    },
    instructions: 'the value can only be a valid number,e.g. 1, 3.14 or 2010'
};

// 檢查該值是否只包含字母和數字
validator.types.isAlphaNum = {
    validate: function (value) {
        return !/[^a-z0-9]/i.test(value);
    },
    instructions: 'the value can only contain characters and numbers, no special symols'
};

var data = {
    first_name: 'Super',
    last_name: 'Man',
    age: 'unknown',
    username: 'o_O'
};

validator.config = {
    first_name: 'isNonEmpty',
    age: 'isNumber',
    username: 'isAlphaNum'
};

validator.validate(data);
if (validator.hasErrors()) {
    console.log(validator.messages.join("\n"));
}

  正如您所看到的,validator物件是通用的,可以像這樣將其儲存下來以用於驗證用例。增強validator物件的方法是新增更多的型別檢查。如果在多個頁面中使用它,很快就會有一個優良的特定檢查集合。以後,針對每個新的用例,所需要做的就是配置該驗證器並執行validate()方法。

六、外觀模式

  外觀(facade)模式是一種簡單的模式,它為物件提供了一個可供選擇的介面。這是一種非常好的設計實踐,可保持方法的簡潔性並且不會使他們處理過多的工作。如果原來有許多接受多個引數的uber方法,相比而言,按照本實現方法,最終將會建立更多數量的方法。有時候,兩個或更多的方法可能普遍的被一起呼叫。在這樣的情況下,建立另一個方法以包裝重複的方法呼叫時非常有意義的。

  例如,當處理瀏覽器事件時,您有以下方法:

  stopPropagation():中止事件以避免其冒泡上升到父節點。

  preventDefault():阻止瀏覽器執行預設動作(例如,阻止下面的連結或提交表單)。

  以上是兩個單獨的方法且各自具有不同的目標,他們之間應保持互相獨立,但在同一時間,他們經常被一起呼叫。為此,並不需要在程式中到處複製這兩個方法呼叫,可以建立一個外觀方法從而同時呼叫這兩個方法:

var myevent = {
    // ...
    stop: function (e) {
        e.preventDefault();
        e.stopPropagation();
    }
}

  外觀模式非常適合於瀏覽器指令碼處理,據此可將瀏覽器之間的差異隱藏在外觀之後。繼續返回到前面的例子,可以新增程式碼來處理在IE的事件API中的差異。

var myevent = {
    // ...
    stop: function (e) {
        // 其他
        if(typeof e.preventDefault === 'function') {
            e.preventDefault();
        }
        if(typeof e.stopPropagation === 'function') {
            e.stopPropagation();
        }

        // IE
        if(typeof e.returnValue === 'boolean') {
            e.returnValue = false;
        }
        if(typeof e.cancelBubble === 'boolean') {
            e.cancelBubble = true;
        }
    }
}

  外觀模式對於重新設計和重構的工作也很有幫助。當需要替換一個具有不同實現的物件時,不得不花費一段時間對他重新進行修改(這是一個複雜的物件),而且同時還要編寫使用該物件的新程式碼。通過使用外觀模式,可以首先考慮新物件的API,然後繼續在原有物件的前面建立一個外觀。這樣,當您著手完全取代原有物件的時候,僅需修改更少的客戶端程式碼,這是由於任何最新的客戶端程式碼都已經使用了這個新API。

  這一篇,我們學習了裝飾者模式,策略模式,外觀模式。從這兩篇文章的學習,我們發現,這些設計模式,實際上是非常貼近我們的工作生活的。這些設計模式,都可以在我們的開發工作中找到現存的影子。這回,你是不是能瞭解到一點,為什麼設計模式這麼重要。其實,設計模式,個人理解,就是貼合業務場景的某一種應用的具體實現方式。

  下一篇,我們學習最後三個設計模式。