為什麼使用工廠模式
解答問題前,瞭解什麼是工廠模式我覺得更重要些。 工廠模式其實也稱建立模式,是用於建立物件的一種方式。可以說就是用來代替 new 例項化物件,決定了例項化哪一個類,從而解決解耦問題。
舉個例子:
- 程式設計中,在一個 A 類中通過 new 的方式例項化了類 B,那麼 A 類和 B 類之間就存在關聯(耦合);
- 後期因為需要修改了 B 類的程式碼和使用方式,比如建構函式中傳入引數,那麼 A 類也要跟著修改,一個類的依賴可能影響不大,但若有多個類依賴了 B 類,那麼這個工作量將會相當的大,容易出現修改錯誤,也會產生很多的重複程式碼,這無疑是件非常痛苦的事;
- 這種情況下,就需要將建立例項的工作從呼叫方(A類)中分離,與呼叫方解耦,也就是使用工廠方法建立例項的工作封裝起來(減少程式碼重複),由工廠管理物件的建立邏輯,呼叫方不需要知道具體的建立過程,只管使用,而降低呼叫者因為建立邏輯導致的錯誤;
擬物化解讀
一個工廠接到一筆訂單(傳參),然後根據這個訂單型別(引數)來安排產品線(例項化哪個類),當然客戶可以要求一些產品的工藝屬性(抽象工廠)。這其中廠長(工廠模式)只負責排程,即安排產品零件流水線。你應該知道的是,工廠有個特點就是產出體量大、相似度高的產品。如果你要做單一定製化的產品,那這筆訂單給工廠就不適用了。
其作用(利)
- 解耦,通過使用工程方法而不是
new
關鍵字; - 將所有例項化的程式碼集中在一個位置減少程式碼重複,降低出錯;
具體實現
- 分步建立一個複雜的物件,解耦封裝過程和具體建立元件(分解為零件流水線);
- 無需關心元件如何組裝(廠長在排程);
- 不暴露建立物件的具體邏輯,將邏輯封裝在一個函式中(客戶只需要告訴工廠做什麼和提一些要求);
適用場景
- 處理大量具有相同屬性的小物件;
- 物件的構建十分複雜,需要依賴具體環境建立不同例項;
分類(抽象程度)
不暴露建立物件的具體邏輯,而是將將邏輯封裝在一個函式中,那麼這個函式就可以被視為一個工廠。
簡單工廠模式
也可以叫靜態工廠模式,用一個工廠物件建立同一類物件類的例項。現實生活中,使用者在平臺還是分等級的,角色不同,許可權也不同。
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個例項對應不同的許可權,呼叫工廠函式時,只需傳遞 admin
或 developer
就可獲取對應的例項物件。
1.簡單工廠函式適用場景
- 正確傳參,就可以獲取所需要的物件,無需知道內部實現細節;
- 內部邏輯(工廠函式)通過傳入引數判斷例項化還是使用哪些類;
- 建立物件數量少(穩定),物件的建立邏輯不復雜;
2.簡單工廠函式不適用場景
- 當需要新增新的類時,就需要修改工廠方法,這違背了開放封閉原則(OCP, 對擴充套件開放、對原始碼修改封閉)。正所謂成也蕭何敗也蕭何。函式
create
內包含了所有建立物件(建構函式)的判斷邏輯程式碼,如果要增加新的建構函式還需要修改函式create
(判斷邏輯程式碼),當可選引數role
變得更多時,那函式create
的判斷邏輯程式碼就變得臃腫起來,難以維護。 - 不適用建立多類物件;
工廠方法模式
將實際建立物件工作推遲到子類當中,核心類就成了抽象類。這樣新增新的類時就無需修改工廠方法,只需要將子類註冊進工廠方法的原型物件中即可。
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)意義
- 學習如何實現功能(招式)
- 學習設計思路(心法)
- 刻意模擬學習
- 寫出愉悅的程式碼