javascript中的設計模式

jsxpang發表於2021-06-17

什麼是設計模式

設計模式(Design Pattern)是一套被反覆使用、多數人知曉的、經過分類的、程式碼設計經驗的總結。

使用設計模式的目的:為了程式碼可重用性、讓程式碼更容易被他人理解、保證程式碼可靠性。 設計模式使程式碼編寫真正工程化;設計模式是軟體工程的基石脈絡,如同大廈的結構一樣。

設計模式(Design pattern)代表了最佳的實踐,通常被有經驗的物件導向的軟體開發人員所採用。設計模式是軟體開發人員在軟體開發過程中面臨的一般問題的解決方案。這些解決方案是眾多軟體開發人員經過相當長的一段時間的試驗和錯誤總結出來的。

在合適的時機使用設計模式能夠讓我們的程式變得更加易用和安全。

在下面的程式碼中,將通過js來講解一些常用的設計模式。

設計模式分類

通常情況下,設計模式可以分成三個種類,一共23種。

可以將設計模式分為建立型模式結構型模式行為型模式

建立型模式當中包含有以下的內容:

* 單例模式
* 抽象工廠模式
* 建造者模式
* 工廠模式
* 原型模式

結構型模式分為以下幾種:

* 介面卡模式
* 橋接模式
* 裝飾模式
* 組合模式
* 外觀模式
* 享元模式
* 代理模式

行為型模式分為以下幾類:

* 模板方法模式
* 命令模式
* 迭代器模式
* 觀察者模式
* 中介者模式
* 備忘錄模式
* 直譯器模式
* 狀態模式
* 策略模式
* 職責鏈模式
* 訪問者模式

下面聊聊幾個常見的模式

單例模式

單例就是保證一個類只有一個例項,實現的方法一般是先判斷例項存在與否,如果存在直接返回,如果不存在就建立了再返回,這就確保了一個類只有一個例項物件。

而在js當中,單例則是一個名稱空間的創造者,通過單例模式來創造一個獨立的名稱空間,本質上是通過單例模式在全域性名稱空間當中建立一個唯一的訪問點來訪問該物件。

最簡單的實現方式:

通過js當中的物件直接量來實現單例模式是最簡單的實現方式:

例如:

var mySingleton = {
    property1: "something",
    property2: "something else",
    method1: function () {
        console.log('hello world');
    }
};

通過物件直接量建立的物件裡面可以儲存很多的屬性和方法。

當然,也可以採用下面的這種形式。

例如:

let single = function(){

    // 建立私有的屬性和方法
    let name = "zhangsan";

    function sayHello(){
        console.log(`hello,${name}`);
    }
    // 對外暴露的方法和屬性
    return {
        sayGoodBye:()=>{
            console.log("再見....");
        },
        like:"吃喝玩樂"
    }

}

如果想要在使用的時候進行初始化,而不是提前初始化好,並且更加的節約資源,可以使用建構函式的寫法:

例如:

var Singleton = (function () {
    var instantiated;
    function init() {
        /*這裡定義單例程式碼*/
        return {
            publicMethod: function () {
                console.log('hello world');
            },
            publicProperty: 'test'
        };
    }

    return {
        getInstance: function () {
            if (!instantiated) {
                instantiated = init();
            }
            return instantiated;
        }
    };
})();

如果上面的程式碼難以理解,那麼可以嘗試著看看下面的程式碼,效果一致!

// 通過建構函式來模擬單例模式 
function Construct(){
    // 確保只有單例
    if( Construct.unique !== undefined ){
        return Construct.unique; 
    }
    // 其他程式碼
    this.name = "張三";
    this.age="24";
    Construct.unique = this;
}

// 使用單例模式
let a = new Construct();
let b = new Construct();

console.log(a===b); // true

在上面的程式碼中,只不過是利用了資料快取的原理儲存了狀態,然後再次進行判斷操作即可,下面的寫法也類似如此:

function Universe() {

    // 判斷是否存在例項
    if (typeof Universe.instance === 'object') {
        return Universe.instance;
    }

    // 其它內容
    this.start_time = 0;
    this.bang = "Big";

    // 快取
    Universe.instance = this;

    // 隱式返回this
}

// 測試
var uni = new Universe();
var uni2 = new Universe();
console.log(uni === uni2); // true

組合模式(Composite)

組合模式(Composite)將物件組合成樹形結構以表示“部分-整體”的層次結構,組合模式使得使用者對單個物件和組合物件的使用具有一致性。

這種組合模式的資料結構在js當中是屬於非常少見的,不過我們常用的虛擬dom卻是與其類似,一個DOM節點可以包含子節點,不管是父節點還是子節點都有新增、刪除、遍歷子節點的通用功能。所以說組合模式的關鍵是要有一個抽象類,它既可以表示子元素,又可以表示父元素,並且無論是什麼層級都具備著相同的屬性和方法。

例如:

var MenuComponent = function () {
};
MenuComponent.prototype.getName = function () {
    throw new Error("該方法必須重寫!");
};
MenuComponent.prototype.getDescription = function () {
    throw new Error("該方法必須重寫!");
};
MenuComponent.prototype.getPrice = function () {
    throw new Error("該方法必須重寫!");
};
MenuComponent.prototype.isVegetarian = function () {
    throw new Error("該方法必須重寫!");
};
MenuComponent.prototype.print = function () {
    throw new Error("該方法必須重寫!");
};
MenuComponent.prototype.add = function () {
    throw new Error("該方法必須重寫!");
};
MenuComponent.prototype.remove = function () {
    throw new Error("該方法必須重寫!");
};
MenuComponent.prototype.getChild = function () {
    throw new Error("該方法必須重寫!");
};


// 建立基本菜品
var MenuItem = function (sName, sDescription, bVegetarian, nPrice) {
    MenuComponent.apply(this);
    this.sName = sName;
    this.sDescription = sDescription;
    this.bVegetarian = bVegetarian;
    this.nPrice = nPrice;
};
MenuItem.prototype = new MenuComponent();
MenuItem.prototype.getName = function () {
    return this.sName;
};
MenuItem.prototype.getDescription = function () {
    return this.sDescription;
};
MenuItem.prototype.getPrice = function () {
    return this.nPrice;
};
MenuItem.prototype.isVegetarian = function () {
    return this.bVegetarian;
};
MenuItem.prototype.print = function () {
    console.log(this.getName() + ": " + this.getDescription() + ", " + this.getPrice() + "euros");
};

var Menu = function (sName, sDescription) {
    MenuComponent.apply(this);
    this.aMenuComponents = [];
    this.sName = sName;
    this.sDescription = sDescription;
    this.createIterator = function () {
        throw new Error("This method must be overwritten!");
    };
};
Menu.prototype = new MenuComponent();
Menu.prototype.add = function (oMenuComponent) {
    // 新增子菜品
    this.aMenuComponents.push(oMenuComponent);
};
Menu.prototype.remove = function (oMenuComponent) {
    // 刪除子菜品
    var aMenuItems = [];
    var nMenuItem = 0;
    var nLenMenuItems = this.aMenuComponents.length;
    var oItem = null;

    for (; nMenuItem < nLenMenuItems; ) {
        oItem = this.aMenuComponents[nMenuItem];
        if (oItem !== oMenuComponent) {
            aMenuItems.push(oItem);
        }
        nMenuItem = nMenuItem + 1;
    }
    this.aMenuComponents = aMenuItems;
};
Menu.prototype.getChild = function (nIndex) {
    //獲取指定的子菜品
    return this.aMenuComponents[nIndex];
};
Menu.prototype.getName = function () {
    return this.sName;
};
Menu.prototype.getDescription = function () {
    return this.sDescription;
};
Menu.prototype.print = function () {
    // 列印當前菜品以及所有的子菜品
    console.log(this.getName() + ": " + this.getDescription());
    console.log("--------------------------------------------");

    var nMenuComponent = 0;
    var nLenMenuComponents = this.aMenuComponents.length;
    var oMenuComponent = null;

    for (; nMenuComponent < nLenMenuComponents; ) {
        oMenuComponent = this.aMenuComponents[nMenuComponent];
        oMenuComponent.print();
        nMenuComponent = nMenuComponent + 1;
    }
};

var DinnerMenu = function () {
    Menu.apply(this);
};
DinnerMenu.prototype = new Menu();

var CafeMenu = function () {
    Menu.apply(this);
};
CafeMenu.prototype = new Menu();

var PancakeHouseMenu = function () {
    Menu.apply(this);
};
PancakeHouseMenu.prototype = new Menu();

var Mattress = function (aMenus) {
    this.aMenus = aMenus;
};
Mattress.prototype.printMenu = function () {
    this.aMenus.print();
};

var oPanCakeHouseMenu = new Menu("Pancake House Menu", "Breakfast");
var oDinnerMenu = new Menu("Dinner Menu", "Lunch");
var oCoffeeMenu = new Menu("Cafe Menu", "Dinner");
var oAllMenus = new Menu("ALL MENUS", "All menus combined");

oAllMenus.add(oPanCakeHouseMenu);
oAllMenus.add(oDinnerMenu);

oDinnerMenu.add(new MenuItem("Pasta", "Spaghetti with Marinara Sauce, and a slice of sourdough bread", true, 3.89));
oDinnerMenu.add(oCoffeeMenu);

oCoffeeMenu.add(new MenuItem("Express", "Coffee from machine", false, 0.99));

var oMattress = new Mattress(oAllMenus);
console.log("---------------------------------------------");
oMattress.printMenu();
console.log("---------------------------------------------");

觀察者模式

觀察者模式又叫釋出訂閱模式(Publish/Subscribe),它定義了一種一對多的關係,讓多個觀察者物件同時監聽某一個主題物件,這個主題物件的狀態發生變化時就會通知所有的觀察者物件,使得它們能夠自動更新自己。

使用觀察者模式的好處:

  • 支援簡單的廣播通訊,自動通知所有已經訂閱過的物件。
  • 頁面載入後目標物件很容易與觀察者存在一種動態關聯,增加了靈活性。
  • 目標物件與觀察者之間的抽象耦合關係能夠單獨擴充套件以及重用。

在js當中,觀察者模式主要是通過回撥函式來實現的。

例如:

// 觀察者模式

// 建立一個pubsub物件

var pubsub = {};

(function(q){
    var topics = {}, // 回撥函式存放的陣列
    subUid = -1;

    // 釋出方法
    q.publish = function (topic, args) {

        if (!topics[topic]) {
            return false;
        }

        setTimeout(function () {
            var subscribers = topics[topic],
                len = subscribers ? subscribers.length : 0;

            while (len--) {
                subscribers[len].func(topic, args);
            }
        }, 0);

        return true;

    };
    //訂閱方法
    q.subscribe = function (topic, func) {

        if (!topics[topic]) {
            topics[topic] = [];
        }

        var token = (++subUid).toString();
        topics[topic].push({
            token: token,
            func: func
        });
        return token;
    };
    //退訂方法
    q.unsubscribe = function (token) {
        for (var m in topics) {
            if (topics[m]) {
                for (var i = 0, j = topics[m].length; i < j; i++) {
                    if (topics[m][i].token === token) {
                        topics[m].splice(i, 1);
                        return token;
                    }
                }
            }
        }
        return false;
    };

})(pubsub);

使用方式如下:

// 使用方式
//來,訂閱一個
pubsub.subscribe('example1', function (topics, data) {
    console.log(topics + ": " + data);
});

//釋出通知
pubsub.publish('example1', 'hello world!');
pubsub.publish('example1', ['test', 'a', 'b', 'c']);
pubsub.publish('example1', [{ 'color': 'blue' }, { 'text': 'hello'}]);

使用場景:
觀察者的使用場合就是:當一個物件的改變需要同時改變其它物件,並且它不知道具體有多少物件需要改變的時候,就應該考慮使用觀察者模式。

假如我們想在網頁面裡插入一些元素,而這些元素型別不固定,可能是圖片,也有可能是連線,甚至可能是文字,根據工廠模式的定義,我們需要定義工廠類和相應的子類,我們先來定義子類的具體實現(也就是子函式):
例如:

var page = page || {};
page.dom = page.dom || {};
//子函式1:處理文字
page.dom.Text = function () {
    this.insert = function (where) {
        var txt = document.createTextNode(this.url);
        where.appendChild(txt);
    };
};

//子函式2:處理連結
page.dom.Link = function () {
    this.insert = function (where) {
        var link = document.createElement('a');
        link.href = this.url;
        link.appendChild(document.createTextNode(this.url));
        where.appendChild(link);
    };
};

//子函式3:處理圖片
page.dom.Image = function () {
    this.insert = function (where) {
        var im = document.createElement('img');
        im.src = this.url;
        where.appendChild(im);
    };
};

那麼如何定義工廠處理函式呢?

page.dom.factory = function (type) {
    return new page.dom[type];
}

使用方式如下:

var o = page.dom.factory('Link');
o.url = 'http://www.cnblogs.com';
o.insert(document.body);

代理模式

代理,顧名思義就是幫助別人做事,GoF對代理模式的定義如下:

代理模式(Proxy),為其他物件提供一種代理以控制對這個物件的訪問。

代理模式使得代理物件控制具體物件的引用。代理幾乎可以是任何物件:檔案,資源,記憶體中的物件,或者是一些難以複製的東西。

假如張三想要李四替它送花給小紅,那麼就可以通過如下的寫法:

// 建立一個物件小紅
var gril = function (name){
    this.name = name;
}

// 張三
var zhangsan = function (gril){
    this.gril = gril;
    this.sendGift = function(gift){
        alert("Hi " + girl.name + ", zhangsan送你一個禮物:" + gift);
    }
}

// 李四就是代理
var lisi = function (girl) {
    this.girl = girl;
    this.sendGift = function (gift) {
        (new zhangsan(girl)).sendGift(gift); // 替張三送花
    }
}

// 呼叫 
var lisi = new proxyTom(new girl("小紅"));
proxy.sendGift("999朵玫瑰");

相關文章