JavaScript設計模式(二)
六、Command
var CommandFactory = (function ()
{
var Command = function ()
{
}
Command.prototype.execute = function ()
{
throw new Error("must impl the execute function");
}
var createCommand = function (params)
{
var C = function (receiver)
{
this .temp = null;
this.receiver = receiver;
}
C.prototype = new Command();
for (var i in params)
{
C.prototype[i] = params[i];
}
return C;
}
return {
createCommand: createCommand
};
})();
var Calculator = function ()
{
var result = 0;
console.log("result is " + result);
return {
add: function (num)
{
result += num;
console.log("result is " + result);
},
sub: function (num)
{
result -= num;
console.log("result is " + result);
}
}
}
var commandList = [];
var CommandAdd = CommandFactory.createCommand(
{
execute: function (num)
{
this.temp = num;
this.receiver.add(num);
commandList.push(this);
},
undo: function ()
{
this.receiver.sub(this.temp);
}
}
);
var CommandSub = CommandFactory.createCommand(
{
execute : function (num)
{
this.temp = num;
this.receiver.sub(num);
commandList.push(this);
},
undo : function ()
{
this.receiver.add(this.temp);
}
}
);
var calculator = new Calculator();
window.onload = function ()
{
createButton("1", function ()
{
var ca = new CommandAdd(calculator);
ca.execute(1);
});
createButton("2", function ()
{
var cs = new CommandSub(calculator);
cs.execute(1);
});
createButton("undo", function ()
{
if (commandList.length === 0)
{
console.log("can't undo now");
return;
}
var command = commandList.pop();
command.undo();
});
}
這裡通過一個簡單的計算器來演繹命令模式。JavaScript 可以用高階函式非常方便地實現命令模式。命令模式在 JavaScript 語言中也是一種隱形的模式。為了實現偽繼承,這裡用了一點小小的技巧,如果“子類”Command沒有實現execute方法,執行時將直接丟擲異常。
七、Composite
var Folder = function (name)
{
var parentDir = "/";
var name = name;
var files = [];
var parent = null;
return {
add : function (file)
{
file.setParentDir(parentDir + name + "/");
file.setParent(this);
files.push(file);
},
scan : function ()
{
console.log(parentDir + name);
for(var i = 0; i < files.length; i++)
{
files[i].scan();
}
},
setParentDir : function (_parentDir)
{
parentDir = _parentDir;
},
setParent : function (_parent)
{
parent = _parent;
},
getFiles : function ()
{
return files;
},
remove : function ()
{
if(!parent)
{
return;
}
var filesInParent = parent.getFiles();
for(var i = 0; i < filesInParent.length; i++)
{
if(filesInParent[i] === this)
{
parent.getFiles().splice(i, 1);
break;
}
}
}
};
};
var File = function (name)
{
var parentDir = "/";
var name = name;
var parent = null;
return {
add : function ()
{
throw new Error("Unable to add file to the file");
},
scan : function ()
{
console.log(parentDir + name);
},
setParentDir : function (_parentDir)
{
parentDir = _parentDir;
},
setParent : function (_parent)
{
parent = _parent;
},
remove : function ()
{
if(!parent)
{
return;
}
var filesInParent = parent.getFiles();
for(var i = 0; i < filesInParent.length; i++)
{
if(filesInParent[i] === this)
{
parent.getFiles().splice(i, 1);
break;
}
}
}
};
};
組合模式最經典的例子莫過於檔案和檔案夾了,而組合模式的作用就是可以把相同的操作應用在組合物件和單個物件上。在大多數情況下,我們都可以忽略掉組合物件和單個物件之間的差別,從而用一致的方式來處理它們。
八、Template Method
var Template = function (param)
{
var func = function ()
{
console.log("func invoked");
}
var func1 = param.func1 || function()
{
throw new Error("must deliver func1");
}
var func2 = param.func2 || function ()
{
throw new Error("must deliver func2");
}
var needFunc2 = param.needFunc2 || function ()
{
return false;
}
var F = function (){};
F.prototype.invoke = function ()
{
func();
func1();
if(needFunc2())
{
func2();
}
}
return F;
};
var Imp1 = Template({
func1 : function ()
{
console.log("func1 invoked in Imp1");
}
});
var Imp2 = Template({
func1 : function ()
{
console.log("func1 invoked in Imp2");
},
needFunc2 : function ()
{
return true;
},
func2 : function ()
{
console.log("func2 invoked in Imp2");
}
});
模板方法模式是好萊塢原則的一個典型使用場景,它與好萊塢原則的聯絡非常明顯,當我們用模板方法模式編寫一個程式時,就意味著子類放棄了對自己的控制權,而是改為父類通知子類,哪些方法應該在什麼時候被呼叫。作為子類,只負責提供一些設計上的細節。同樣用到好萊塢原則的還有觀察者模式。在 JavaScript 中,我們很多時候都不需要依樣畫瓢地去實現一個模版方法模式,高階函式是更好的選擇。
九、Flyweight
var ObjPoolFactory = function (objCreateFn)
{
var objPool = [];
return {
create : function ()
{
var obj = objPool.length === 0 ? objCreateFn.apply(this, arguments) : objPool.shift();
return obj;
},
recover : function (obj)
{
objPool.push(obj);
}
};
}
var iframeFactory = ObjPoolFactory(function ()
{
var iframe = document.createElement("iframe");
document.body.appendChild( iframe );
iframe.onload = function()
{
iframe.onload = null; // 防止 iframe 重複載入的 bug
iframeFactory.recover( iframe ); // iframe 載入完成之後回收節點
}
return iframe;
});
var iframe1 = iframeFactory.create();
iframe1.src = 'http://www.baidu.com';
var iframe2 = iframeFactory.create();
iframe2.src = 'http://www.QQ.com';
//這裡是false,因為1在自己載入完成後才會把自己的例項放到物件池中
console.log(iframe1 === iframe2);
setTimeout(function()
{
var iframe3 = iframeFactory.create();
iframe3.src = 'http://www.163.com';
//這裡是true,此時物件池中已經有了可用的物件,不再會繼續建立新的物件
console.log(iframe3 === iframe1);
}, 3000 );
享元模式是為解決效能問題而生的模式,這跟大部分模式的誕生原因都不一樣。在一個存在大量相似物件的系統中,享元模式可以很好地解決大量物件帶來的效能問題。這裡的物件池就是一個非常典型的應用。
十、Chain of responsibility
/**
*考慮到實際開發中完全同步的場景很少,這裡提供了一個自己寫的全callback的例子,同步可以通過callback實現,但反過來就不行了
*/
var ChainPro = function (fn, callback)
{
var nextProcessor;
var callback = callback;
return {
setNextProcessor : function (processor)
{
nextProcessor = processor;
return nextProcessor;
},
request : function ()
{
fn.apply(this, arguments);
},
next : function ()
{
if(!nextProcessor)
{
callback("unable to process the request");
return;
}
nextProcessor.request.apply(nextProcessor, arguments);
},
getNextProcessor : function ()
{
return nextProcessor;
},
getCallback : function ()
{
return callback;
}
};
}
var order500pro = function (orderType, hasPaid)
{
var self = this;
if(orderType === 1 && hasPaid === true)
{
setTimeout(function ()
{
var stock = 100;
if(stock > 0)
{
self.getCallback().call(self, "route1-success");
}
else
{
self.getCallback.call(self, "route1-failed");
}
}, 1000);
}
else
{
self.next.apply(self.getNextProcessor(), arguments);
}
};
var order200pro = function (orderType, hasPaid)
{
var self = this;
if(orderType === 2 && hasPaid === true)
{
setTimeout(function ()
{
var stock = 100;
if(stock > 0)
{
self.getCallback().call(self, "route2-success");
}
else
{
self.getCallback.call(self, "route2-failed")
}
}, 1000);
}
else
{
self.next.apply(self.getNextProcessor(), arguments);
}
};
var orderNormalPro = function ()
{
var self = this;
setTimeout(function ()
{
var stock = 0;
if(stock > 0)
{
self.getCallback().call(self, "route3-success");
}
else
{
self.next.apply(self.getNextProcessor(), arguments);
}
}, 1000);
}
var orderNoStockPro = function ()
{
var self = this;
self.getCallback().call(self, "route4");
}
var callback = function (result)
{
console.log("result is " + result);
};
var c1 = ChainPro(order500pro, callback);
var c2 = ChainPro(order200pro, callback);
var c3 = ChainPro(orderNormalPro, callback);
var c4 = ChainPro(orderNoStockPro, callback);
c1.setNextProcessor(c2).setNextProcessor(c3).setNextProcessor(c4);
c1.request(3, false);
責任鏈模式也可以通過前文所述的AOP來實現,職責鏈中的節點數量和順序是可以自由變化的,我們可以在執行時決定鏈中包含哪些節點。無論是作用域鏈、原型鏈,還是 DOM 節點中的事件冒泡,我們都能從中找到職責鏈模式的影子。職責鏈模式還可以和組合模式結合在一起,用來連線部件和父部件,或是提高組合物件的效率。
十一、Mediator
function Player(name, teamColor)
{
this.id = -1;
this.name = name;
this.teamColor = teamColor;
this.state = "alive";
}
Player.prototype.win = function ()
{
console.log(this.name + " win");
}
Player.prototype.lose = function ()
{
console.log(this.name + " lose");
}
Player.prototype.die = function ()
{
this.state = "dead";
}
var playerFactory = (function ()
{
var id = 0;
var addPlayer = function (name, teamColor)
{
id++;
var player = new Player(name, teamColor);
player.id = id;
playerDirector.receiveMsg("addPlayer", player);
}
return {
addPlayer : addPlayer
}
})();
var playerDirector = (function ()
{
var players = {};
var operations = {};
operations.showPlayers = function ()
{
for(var teamColor in players)
{
var arr = players[teamColor];
for(var i = 0; i < arr.length; i++)
{
var player = arr[i];
console.log(teamColor + ": " + player.name);
}
}
}
operations.addPlayer = function(player)
{
var teamColor = player.teamColor;
if(!players[teamColor])
{
players[teamColor] = [];
}
players[teamColor].push(player);
}
operations.removePlayer = function (id)
{
var player = null;
for(var teamColor in players)
{
var arr = players[teamColor];
for(var i = 0; i < arr.length; i++)
{
player = arr[i];
if(player.id === id)
{
arr.splice(i, 1);
break;
}
}
}
return player;
}
operations.changeTeam = function (id, newTeamColor)
{
var player = operations.removePlayer(id);
if(player)
{
player.teamColor = newTeamColor;
operations.addPlayer(player);
}
}
var receiveMsg = function ()
{
var operation = Array.prototype.shift.call(arguments);
operations[operation].apply(this, arguments);
}
return {
receiveMsg : receiveMsg
}
})();
playerFactory.addPlayer("amuro", "white");
playerFactory.addPlayer("kamiu", "white");
playerFactory.addPlayer("judo", "white");
playerFactory.addPlayer("char", "red");
playerFactory.addPlayer("haman", "red");
playerDirector.receiveMsg("showPlayers");
playerDirector.receiveMsg("changeTeam", 4, "blue");
playerDirector.receiveMsg("showPlayers");
中介者模式是迎合迪米特法則的一種實現。迪米特法則也叫最少知識原則,是指一個物件應該儘可能少地瞭解另外的物件(類似不和陌生人說話)。如果物件之間的耦合性太高,一個物件發生改變之後,難免會影響到其他的物件,跟“城門失火,殃及池魚”的道理是一樣的。而在中介者模式裡,物件之間幾乎不知道彼此的存在,它們只能通過中介者物件來互相影響對方。所以,副作用就是因此中介者本身會變得異常複雜,從理論上說複雜一個總比複雜一堆好。但是本文一開始所說的,設計的度才是最難把握的東西,在實際專案中,模組或物件之間有一些依賴關係是很正常的。畢竟我們寫程式是為了快速完成專案交付生產,而不是堆砌模式和過度設計。關鍵就在於如何去衡量物件之間的耦合程度。一般來說,如果物件之間的複雜耦合確實導致呼叫和維護出現了困難,而且這些耦合度隨專案的變化呈指數增長曲線,那我們就可以考慮用中介者模式來重構程式碼。
十二、Decorator
Function.prototype.before = function (beforeFn)
{
var self = this;
return function ()
{
if(beforeFn.apply(this, arguments) === false)
{
return;
}
var ret = self.apply(this, arguments);
return ret;
}
}
Function.prototype.after = function (afterFn)
{
var self = this;
return function ()
{
var ret = self.apply(this, arguments);
afterFn.apply(this, arguments);
return ret;
}
}
var login = function (username, password)
{
console.log("log with " + username + " and " + " password");
}
var validate = function (username, password)
{
if(!username)
{
console.log("username is empty");
return false;
}
if(!password)
{
console.log("password is empty");
return false;
}
}
login = login.before(validate);
login("admin", "123");
這裡我們又用到了JS的AOP,其實裝飾者模式和代理模式的結構看起來非常相像,這兩種模式都描述了怎樣為物件提供一定程度上的間接引用,它們的實現部分都保留了對另外一個物件的引用,並且向那個物件傳送請求。
代理模式和裝飾者模式最重要的區別在於它們的意圖和設計目的。代理模式的目的是,當直接訪問本體不方便或者不符合需要時,為這個本體提供一個替代者。本體定義了關鍵功能,而代理提供或拒絕對它的訪問,或者在訪問本體之前做一些額外的事情。裝飾者模式的作用就是為物件動態加入行為。換句話說,代理模式強調一種關係(Proxy 與它的實體之間的關係),這種關係可以靜態的表達,也就是說,這種關係在一開始就可以被確定。而裝飾者模式用於一開始不能確定物件的全部功能時。代理模式通常只有一層代理本體的引用,而裝飾者模式經常會形成一條長長的裝飾鏈。
十三、State
var UploadObj = function (fileName)
{
this.fileName = fileName;
this.deleteState = new DeleteState(this);
this.stopState = new StopState(this);
this.startState = new StartState(this);
this.pauseState = new PauseState(this);
this.currentState = this.stopState;
}
UploadObj.prototype.setState = function(newState)
{
this.currentState = newState;
}
UploadObj.prototype.delete = function ()
{
console.log("real delete");
}
UploadObj.prototype.start = function ()
{
console.log("real start");
}
UploadObj.prototype.pause = function ()
{
console.log("real pause");
}
UploadObj.prototype.onFunction = function ()
{
this.currentState.functionButtonClick();
}
UploadObj.prototype.onDelete = function ()
{
this.currentState .deleteButtonClick();
}
//上傳,兩個button,開始暫停,刪除
var stateFactory = (function ()
{
var State = function (){}
State.prototype.functionButtonClick = function ()
{
throw new Error("must impl the function button click");
}
State.prototype.deleteButtonClick = function ()
{
throw new Error("must impl the delete button click");
}
var createState = function (params)
{
var F = function (uploadObj)
{
this.uploadObj = uploadObj;
}
F.prototype = new State();
for(var i in params)
{
F.prototype[i] = params[i];
}
return F;
}
return {
createState : createState
}
})();
var DeleteState = stateFactory.createState(
{
functionButtonClick : function ()
{
console.log("illegal operation: the file has been deleted");
},
deleteButtonClick : function ()
{
console.log("illegal operation: the file has been deleted");
}
}
);
var StopState = stateFactory.createState(
{
functionButtonClick : function ()
{
console.log("stop -> start");
this.uploadObj.start();
this.uploadObj.setState(this.uploadObj.startState);
},
deleteButtonClick : function ()
{
console.log("delete when stop");
this.uploadObj.delete();
this.uploadObj.setState(this.uploadObj.deleteState);
}
}
);
var StartState = stateFactory.createState(
{
functionButtonClick : function ()
{
console.log("start -> pause");
this.uploadObj.pause();
this.uploadObj.setState(this.uploadObj.pauseState);
},
deleteButtonClick : function ()
{
console.log("unable to delete file when upload start");
}
}
);
var PauseState = stateFactory.createState(
{
functionButtonClick : function ()
{
console.log("pause -> start");
this.uploadObj.start();
this.uploadObj.setState(this.uploadObj.startState);
},
deleteButtonClick : function ()
{
console.log("delete when pause");
this.uploadObj.delete();
this.uploadObj.setState(this.uploadObj.deleteState);
}
}
);
var uploadObj = new UploadObj("aBook.pdf");
createButton("function", function ()
{
uploadObj.onFunction();
});
createButton("delete", function ()
{
uploadObj.onDelete();
});
以上為標準實現,模擬一個上傳檔案和刪除檔案的功能,也可以通過狀態機的方式來實現。
var delegate = function (obj, state)
{
return {
buttonPressed : function ()
{
state.buttonPressed.apply(obj, arguments);
}
};
}
var Fsm = {
offState : {
buttonPressed : function ()
{
console.log("turn on");
this.currentState = this.onState;
}
},
onState : {
buttonPressed : function ()
{
console.log("turn off");
this.currentState = this.offState;
}
}
};
var LightX = function ()
{
this.offState = delegate(this, Fsm.offState);
this.onState = delegate(this, Fsm.onState);
this.currentState = this.offState;
}
var lightX = new LightX();
lightX.currentState.buttonPressed();
推薦一個JS狀態機的開源專案https:// github.com/jakesgordon/
javascript-state-machine。
狀態模式和策略模式像一對雙胞胎,它們都封裝了一系列的演算法或者行為,它們的類圖看起來幾乎一模一樣,但在意圖上有很大不同,因此它們是兩種迥然不同的模式。
策略模式和狀態模式的相同點是,它們都有一個上下文、一些策略或者狀態類,上下文把請求委託給這些類來執行。它們之間的區別是策略模式中的各個策略類之間是平等又平行的,它們之間沒有任何聯絡,所以客戶必須熟知這些策略類的作用,以便客戶可以隨時主動切換演算法;而在狀態模式中,狀態和狀態對應的行為是早已被封裝好的,狀態之間的切換也早被規定完成,“改變行為”這件事情發生在狀態模式內部。對客戶來說,並不需要了解這些細節。這正是狀態模式的作用所在。
十四、Adapter
var getGuangDongCity = function ()
{
var guangDongCity = [
{
name : "guangzhou",
id : 11
},
{
name : "shenzhen",
id : 12
}
];
return guangDongCity;
}
var newGetGuangDongCity = {
guangzhou : 11,
shenzhen : 12,
zhuhai : 13
};
var cityInfoAdapter = function ()
{
var resultArr = [];
for(var i in newGetGuangDongCity)
{
var result = {};
result.name = i;
result.id = newGetGuangDongCity[i];
resultArr.push(result);
}
return resultArr;
}
var render = function (fn)
{
console.log("start rendering map of GuangDong");
var arr = fn();
for(var i = 0; i < arr.length; i++)
{
var element = arr[i];
for(var j in element)
{
console.log(j + " : " + element[j]);
}
}
}
render(cityInfoAdapter);
介面卡模式比較簡單,它主要用來解決兩個已有介面之間不匹配的問題,它不考慮這些介面是怎樣實現的,也不考慮它們將來可能會如何演化。介面卡模式不需要改變已有的介面,就能夠使它們協同作用。
最後再嘮叨一下設計模式六大原則:
單一職責,裡式替換,依賴倒置,介面隔離,迪米特,開閉