Javascript 設計模式之工廠模式

程式設計之上發表於2019-07-23

為什麼使用工廠模式

解答問題前,瞭解什麼是工廠模式我覺得更重要些。 工廠模式其實也稱建立模式,是用於建立物件的一種方式。可以說就是用來代替 new 例項化物件,決定了例項化哪一個類,從而解決解耦問題。

舉個例子:

  • 程式設計中,在一個 A 類中通過 new 的方式例項化了類 B,那麼 A 類和 B 類之間就存在關聯(耦合);
  • 後期因為需要修改了 B 類的程式碼和使用方式,比如建構函式中傳入引數,那麼 A 類也要跟著修改,一個類的依賴可能影響不大,但若有多個類依賴了 B 類,那麼這個工作量將會相當的大,容易出現修改錯誤,也會產生很多的重複程式碼,這無疑是件非常痛苦的事;
  • 這種情況下,就需要將建立例項的工作從呼叫方(A類)中分離,與呼叫方解耦,也就是使用工廠方法建立例項的工作封裝起來(減少程式碼重複),由工廠管理物件的建立邏輯,呼叫方不需要知道具體的建立過程,只管使用,而降低呼叫者因為建立邏輯導致的錯誤

擬物化解讀

Javascript 設計模式之工廠模式

一個工廠接到一筆訂單(傳參),然後根據這個訂單型別(引數)來安排產品線(例項化哪個類),當然客戶可以要求一些產品的工藝屬性(抽象工廠)。這其中廠長(工廠模式)只負責排程,即安排產品零件流水線。你應該知道的是,工廠有個特點就是產出體量大、相似度高的產品。如果你要做單一定製化的產品,那這筆訂單給工廠就不適用了。

其作用(利)

  • 解耦,通過使用工程方法而不是 new 關鍵字;
  • 將所有例項化的程式碼集中在一個位置減少程式碼重複,降低出錯;

具體實現

  • 分步建立一個複雜的物件,解耦封裝過程和具體建立元件(分解為零件流水線);
  • 無需關心元件如何組裝(廠長在排程);
  • 不暴露建立物件的具體邏輯,將邏輯封裝在一個函式中(客戶只需要告訴工廠做什麼和提一些要求);

適用場景

  • 處理大量具有相同屬性的小物件;
  • 物件的構建十分複雜,需要依賴具體環境建立不同例項;

分類(抽象程度)

不暴露建立物件的具體邏輯,而是將將邏輯封裝在一個函式中,那麼這個函式就可以被視為一個工廠。

抽象程度

簡單工廠模式

也可以叫靜態工廠模式,用一個工廠物件建立同一類物件類的例項。現實生活中,使用者在平臺還是分等級的,角色不同,許可權也不同。

Javascript 設計模式之工廠模式

1.ES5 實現

// 0.0.2/es5.sample.factory.js
function Role(options){
    this.role = options.role;
    this.permissions = options.permissions;
}
Role.prototype.show = function (){
    var str = '是一個' + this.role + ', 許可權:' + this.permissions.join(', ');
    console.log(str)
}

function sampleFactory(role){
    switch(role) {
        case 'admin':
            return new Role({ 
                role: '管理員', 
                permissions: ['設定', '刪除', '新增', '建立', '開發', '推送', '提問', '評論']
            });
            break;
        case 'developer':
            return new Role({ 
                role: '開發者', 
                permissions: ['開發', '推送', '提問', '評論']
            });
            break;
        default:
            throw new Error('引數只能為 admin 或 developer');
    }
}

// 例項
const xm = sampleFactory('admin');
xm.show();

const xh = sampleFactory('developer');
xh.show();

const xl = sampleFactory('guest');
xl.show();
複製程式碼

2.ES6 實現

// 0.0.2/sample.factory.js
class SampleFactory {
    constructor(opt) {
        this.role = opt.role;
        this.permissions = opt.permissions;
    }

    // 靜態方法
    static create(role) {
        switch (role) {
            case 'admin':
                return new SampleFactory({
                    role: '管理員',
                    permissions: ['設定', '刪除', '新增', '建立', '開發', '推送', '提問', '評論']
                });
                break;
            case 'developer':
                return new SampleFactory({
                    role: '開發者',
                    permissions: ['開發', '推送', '提問', '評論']
                });
                break;
            default:
                throw new Error('引數只能為 admin 或 developer');
        }
    }

    show() {
        const str = `是一個${this.role}, 許可權:${this.permissions.join(', ')}`;
        console.log(str);
    }

}

// 例項
const xm = SampleFactory.create('admin');
xm.show();

const xh = SampleFactory.create('developer');
xh.show();

const xl = SampleFactory.create('guest');
xl.show();
複製程式碼

// 0.0.2/sample.factory1.js
class Role {
    constructor(options) {
        this.role = options.role;
        this.permissions = options.permissions;
    }
    show() {
        const str = `是一個${this.role}, 許可權:${this.permissions.join(', ')}`;
        console.log(str);
    }
}
class SampleFactory {
    constructor(role) {
        this.role = role;
    }

    // 靜態方法
    static create(role) {
        switch (role) {
            case 'admin':
                return new Role({
                    role: '管理員',
                    permissions: ['設定', '刪除', '新增', '建立', '開發', '推送', '提問', '評論']
                });
                break;
            case 'developer':
                return new Role({
                    role: '開發者',
                    permissions: ['開發', '推送', '提問', '評論']
                });
                break;
            default:
                throw new Error('引數只能為 admin 或 developer');
        }
    }
}

// 例項
const xm = SampleFactory.create('admin');
xm.show();

const xh = SampleFactory.create('developer');
xh.show();

const xl = SampleFactory.create('guest');
xl.show();
複製程式碼

// 0.0.2/sample.factory2.js
class Role {
    constructor(options) {
        this.role = options.role;
        this.permissions = options.permissions;
    }
    show() {
        const str = `是一個${this.role}, 許可權:${this.permissions.join(', ')}`;
        console.log(str);
    }
}

class SampleFactory {
    constructor(role) {
        if(typeof this[role] !== 'function') {
            throw new Error('引數只能為 admin 或 developer');
        }
        return this[role]();
    }

    admin() {
        return new Role({
            role: '管理員',
            permissions: ['設定', '刪除', '新增', '建立', '開發', '推送', '提問', '評論']
        });
    }
    developer() {
        return new Role({
            role: '開發者',
            permissions: ['開發', '推送', '提問', '評論']
        });
    }
}


// 例項
const xm = new SampleFactory('admin');
xm.show();

const xh = new SampleFactory('developer');
xh.show();

const xl = new SampleFactory('guest');
xl.show();
複製程式碼

上例中,sampleFactory 就是一個簡單工廠,2個例項對應不同的許可權,呼叫工廠函式時,只需傳遞 admindeveloper 就可獲取對應的例項物件。

1.簡單工廠函式適用場景

  • 正確傳參,就可以獲取所需要的物件,無需知道內部實現細節;
  • 內部邏輯(工廠函式)通過傳入引數判斷例項化還是使用哪些類;
  • 建立物件數量少(穩定),物件的建立邏輯不復雜;

2.簡單工廠函式不適用場景

  • 當需要新增新的類時,就需要修改工廠方法,這違背了開放封閉原則(OCP, 對擴充套件開放、對原始碼修改封閉)。正所謂成也蕭何敗也蕭何。函式 create 內包含了所有建立物件(建構函式)的判斷邏輯程式碼,如果要增加新的建構函式還需要修改函式 create(判斷邏輯程式碼),當可選引數 role 變得更多時,那函式 create 的判斷邏輯程式碼就變得臃腫起來,難以維護。
  • 不適用建立多類物件;

工廠方法模式

將實際建立物件工作推遲到子類當中,核心類就成了抽象類。這樣新增新的類時就無需修改工廠方法,只需要將子類註冊進工廠方法的原型物件中即可。

Javascript 設計模式之工廠模式

1.安全模式類,可以遮蔽使用類的錯誤造成的錯誤

// 0.0.2/secure.function.factory.js
function Factory(){
    if(!(this instanceof Factory)) {
        return new Factory();
    }
}
Factory.prototype.show = function(){
    console.log('factory show');
}
var f = new Factory();
f.show();
複製程式碼

2.ES5 實現,ES5 沒有像傳統建立類的方式那樣建立抽象類,所以工廠方法模式只需參考其核心思想即可。可將工廠方法看做一個例項化物件工廠類(採用安全模式類),將建立物件的基類放在工廠方法類的原型中即可。當需要新增新類時,只需掛載在 FunctionFactory.prototype 上,無需修改工廠方法,也實現了 OCP 原則。

// 0.0.2/es5.function.factory.js
function FunctionFactory(role) {
    if(!(['admin', 'developer'].indexOf(role) > -1)){
        throw new Error('引數只能為 admin 或 developer');
    }
    
    // 安全的工廠方法
    if (this instanceof FunctionFactory) {
        return this[role]();
    }
    return new FunctionFactory(role);
}
FunctionFactory.prototype.show = function () {
    var str = '是一個' + this.role + ', 許可權:' + this.permissions.join(', ');
    console.log(str)
}
FunctionFactory.prototype.admin = function (permissions) {
    this.role = '管理員';
    this.permissions = ['設定', '刪除', '新增', '建立', '開發', '推送', '提問', '評論'];
}
FunctionFactory.prototype.developer = function (permissions) {
    this.role = '開發者';
    this.permissions = ['開發', '推送', '提問', '評論'];
}

var xm = FunctionFactory('admin');
xm.show();

var xh = new FunctionFactory('developer');
xh.show();

var xl = new FunctionFactory('guest');
xl.show();
複製程式碼

3.ES6 實現,由於 ES6 中還沒有 abstract,就用 new.target 來模擬出抽象類(new.target 指向被 new 執行的建構函式),判斷 new.target 是否指向了抽象類,如果是就報錯。

// 0.0.2/function.factory.js
class FunctionFactoryBase { // 抽象類
    constructor(role) {
        if (new.target === FunctionFactoryBase) {
            throw new Error('抽象類不能例項');
        }
        this.role = role;
    }
}

class FunctionFactory extends FunctionFactoryBase { // 子類
    constructor(role) {
        super(role);
    }

    static create(role) {
        switch (role) {
            case 'admin':
                return new FunctionFactory({
                    role: '管理員',
                    permissions: ['設定', '刪除', '新增', '建立', '開發', '推送', '提問', '評論']
                });
                break;
            case 'developer':
                return new FunctionFactory({
                    role: '開發者',
                    permissions: ['開發', '推送', '提問', '評論']
                });
                break;
            default:
                throw new Error('引數只能為 admin 或 developer');
        }
    }

    show() {
        const { role, permissions } = this.role;
        const str = `是一個${role}, 許可權:${permissions.join(', ')}`;
        console.log(str)
    }
}

// let xl = new FunctionFactoryBase(); // 此行會報錯,註釋後方可正常執行後面

let xm = FunctionFactory.create('admin');
xm.show()

let xh = FunctionFactory.create('developer');
xh.show()

let xl = FunctionFactory.create('guest');
xl.show()
複製程式碼

抽象工廠模式

抽象工廠只留對外的口子,不做事,留給外界覆蓋(子類重寫介面方法以便建立的時候指定自己的物件型別)。主要用於對產品類簇的建立,不直接生成例項(簡單工廠模式和工廠方法模式都是生成例項)。

  • 抽象類是一種宣告但不能使用的類,子類必須先實現其方法才能呼叫;
  • 可以在抽象類中定義一套規範,供子類去繼承實現;
// 0.0.2/abstract.factory2.js
// 抽象工廠
function AbstractFactory(subType, superType) {
    if (typeof AbstractFactory[superType] === 'function') {
        //快取類
        function F() { }
        //繼承父類屬性和方法
        F.prototype = new AbstractFactory[superType]();
        //將子類 constructor 指向子類(自己)
        subType.prototype.constructor = subType;
        //子類原型繼承快取類(父類)
        subType.prototype = new F();
    } else {
        //不存在該抽象類丟擲錯誤
        throw new Error('抽象類不存在')
    }
}

// 抽象類
AbstractFactory.Phone = function () {
    this.type = 'Phone';
}
AbstractFactory.Phone.prototype = {
    showType: function () {
        return new Error('Phone 抽象方法 showType 不能呼叫');
    },
    showPrice: function () {
        return new Error('Phone 抽象方法 showPrice 不能呼叫');
    },
    showColor: function () {
        return new Error('Phone 抽象方法 showColor 不能呼叫');
    }
}

AbstractFactory.Pad = function () {
    this.type = 'Pad';
}
AbstractFactory.Pad.prototype = {
    showType: function () {
        return new Error('Pad 抽象方法 showType 不能呼叫');
    },
    showPrice: function () {
        return new Error('Pad 抽象方法 showPrice 不能呼叫');
    },
    showColor: function () {
        return new Error('Pad 抽象方法 showColor 不能呼叫');
    }
}

// 抽象工廠實現對抽象類的繼承
function Iphone(type, price, color) {
    this.type = type;
    this.price = price;
    this.color = color;
}

//抽象工廠實現對 Phone 抽象類的繼承
AbstractFactory(Iphone, 'Phone');
Iphone.prototype.showType = function () {
    return this.type;
}
Iphone.prototype.showPrice = function () {
    return this.price;
}
Iphone.prototype.showColor = function () {
    return this.color;
}

function Ipad(type, price, color) {
    this.type = type;
    this.price = price;
    this.color = color;
}
AbstractFactory(Ipad, 'Pad');
Ipad.prototype.showType = function () {
    return this.type;
}
Ipad.prototype.showPrice = function () {
    return this.price;
}
Ipad.prototype.showColor = function () {
    return this.color;
}

// 例項
var iphone5s = new Iphone('iphone 5s', 3000, '白色');
console.log('今天剛買了' + iphone5s.showType() + ',價格是' + iphone5s.showPrice() + ',' + iphone5s.showColor())

var iphone8s = new Iphone('iphone 8s', 8000, '白色');
console.log('今天剛買了' + iphone8s.showType() + ',價格是' + iphone8s.showPrice() + ',' + iphone8s.showColor())

var ipad = new Ipad('ipad air', 2000, '騷紅色');
console.log('今天剛買了' + ipad.showType() + ',價格是' + ipad.showPrice() + ',' + ipad.showColor())
複製程式碼

實戰示例

1.jQuery原始碼-工廠模式

// 0.0.2/jquery.factory.js
// 工廠模式
class jQuery {
  constructor(selector) {
    let slice = Array.prototype.slice;
    let dom = slice.call(document.querySelectorAll(selector));
    let len = dom ? dom.length : 0;
    for (let i = 0; i < len; i++) {
      this[i] = dom[i];
    }
    this.length = len
    this.selector = selector || ''
  }
  addClass(name) {
    console.log(name)
  }
  html(data) {

  }
  // 省略多個 API
}

// 工廠模式
window.$ = function(selector) {
  return new jQuery(selector);
}

// 例項
const $li = $('li') 
$li.addClass('item');
複製程式碼

2.React.createElement 實現

// jsx
var profile = (
  <div>
    <img src='https://raw.githubusercontent.com/ruizhengyun/images/master/cover/ruizhengyun.cn_.png' className="profile" />
    <h3>{[user.firstName, user.lastName].join(' ')}</h3>
  </div>
);

// 實現
var profile = React.createElement('div', null, 
  React.createElement('img', { src: 'https://raw.githubusercontent.com/ruizhengyun/images/master/cover/ruizhengyun.cn_.png', className: 'profile' }),
  React.createElement('h3', null, [user.firstName, user.lastName].join(' '))
);

// 原始碼
class Vnode(tag, attrs, children) {
  // ...
}

React.createElement = function(tag, attrs, children) {
  return new Vnode(tag, attrs, children);
}
複製程式碼

設計原則驗證

  • 建構函式與建立者分離
  • 符合開放封閉原則

閱讀原始碼(lib)意義

  • 學習如何實現功能(招式)
  • 學習設計思路(心法)
  • 刻意模擬學習
  • 寫出愉悅的程式碼

本次程式碼 Github

你可以

目錄:Javascript 設計模式小書

上一篇:Javascript 設計模式之設計原則

下一篇:Javascript 設計模式之單例模式

相關文章