前端js用得上的设计模式

以下内容来源于《JavaScript设计模式与开发实践》

经典的书籍,值得反复去学习和品味,每一次看都能得到新的认识,因为我们一直在进步,观察问题的视角也一直在上升。

单例模式

单例模式的核心是确保只有一个实例,并提供全局访问。

vvar obj;
if (!obj) {
 obj = xxx;
}

惰性单例指的是在需要的时候才创建对象实例。惰性单例是单例模式的重点,这种技术在实
际开发中非常有用,有用的程度可能超出了我们的想象

我们把如何管理单例的逻辑从业务的代码中抽离出来,这些逻辑被封装在 getSingle 函数内部,创建对象的方法 fn 被当成参数动态传入 getSingle 函数

var getSingle = function (fn) {
 var result;
 return function () {
 return result || (result = fn.apply(this, arguments));
 };
};
var createLoginLayer = function () {
 var div = document.createElement("div");
 div.innerHTML = "我是登录浮窗";
 div.style.display = "none";
 document.body.appendChild(div);
 return div;
};
var createSingleLoginLayer = getSingle(createLoginLayer);

总结:单例模式是一种简单但非常实用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。更奇妙的是,创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模式的威力。

策略模式

策略模式的定义是:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

奖金计算

// before
var calculateBonus = function (performanceLevel, salary) {
 if (performanceLevel === "S") {
 return salary * 4;
 }
 if (performanceLevel === "A") {
 return salary * 3;
 }
 if (performanceLevel === "B") {
 return salary * 2;
 }
};
calculateBonus("B", 20000); // 输出:40000
calculateBonus("S", 6000); // 输出:24000
// after
var strategies = {
 S: function (salary) {
 return salary * 4;
 },
 A: function (salary) {
 return salary * 3;
 },
 B: function (salary) {
 return salary * 2;
 },
};
var calculateBonus = function (level, salary) {
 return strategies[level](salary);
};
console.log(calculateBonus("S", 20000)); // 输出:80000
console.log(calculateBonus("A", 10000)); // 输出:30000

表单校验

<html>
<body>
 <form action="http:// xxx.com/register" id="registerForm" method="post">
 请输入用户名:<input type="text" name="userName" />
 请输入密码:<input type="text" name="password" />
 请输入手机号码:<input type="text" name="phoneNumber" />
 <button>提交</button>
 </form>
 <script>
 /***********************策略对象**************************/
 var strategies = {
 isNonEmpty: function (value, errorMsg) {
 if (value === '') {
 return errorMsg;
 }
 },
 minLength: function (value, length, errorMsg) {
 if (value.length < length) {
 return errorMsg;
 }
 },
 isMobile: function (value, errorMsg) {
 if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
 return errorMsg;
 }
 }
 };
 /***********************Validator 类**************************/
 var Validator = function () {
 this.cache = [];
 };
 Validator.prototype.add = function (dom, rules) {
 var self = this;
 for (var i = 0, rule; rule = rules[i++];) {
 (function (rule) {
 var strategyAry = rule.strategy.split(':');
 var errorMsg = rule.errorMsg;
 self.cache.push(function () {
 var strategy = strategyAry.shift();
 strategyAry.unshift(dom.value);
 strategyAry.push(errorMsg);
 return strategies[strategy].apply(dom, strategyAry);
 });
 })(rule)
 }
 };
 Validator.prototype.start = function () {
 for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
 var errorMsg = validatorFunc();
 if (errorMsg) {
 return errorMsg;
 }
 }
 };
 /***********************客户调用代码**************************/
 var registerForm = document.getElementById('registerForm');
 var validataFunc = function () {
 var validator = new Validator();
 validator.add(registerForm.userName, [{
 strategy: 'isNonEmpty',
 errorMsg: '用户名不能为空'
 }, {
 strategy: 'minLength:10',
 errorMsg: '用户名长度不能小于10 位'
 }]);
 validator.add(registerForm.password, [{
 strategy: 'minLength:6',
 errorMsg: '密码长度不能小于6 位'
 }]);
 validator.add(registerForm.phoneNumber, [{
 strategy: 'isMobile',
 errorMsg: '手机号码格式不正确'
 }]);
 var errorMsg = validator.start();
 return errorMsg;
 }
 registerForm.onsubmit = function () {
 var errorMsg = validataFunc();
 if (errorMsg) {
 alert(errorMsg);
 return false;
 }
 };
 </script>
</body>
</html>

优缺点

  1. 策略模式利用组合、委托和多态等技术和思想,可以有效地避免多重条件选择语句。
  2. 策略模式提供了对开放—封闭原则的完美支持,将算法封装在独立的strategy 中,使得它们易于切换,易于理解,易于扩展。
  3. 策略模式中的算法也可以复用在系统的其他地方,从而避免许多重复的复制粘贴工作。
  4. 首先,使用策略模式会在程序中增加许多策略类或者策略对象,但实际上这比把它们负责的逻辑堆砌在Context 中要好。

缺:
要使用策略模式,必须了解所有的strategy,必须了解各个strategy 之间的不同点,
这样才能选择一个合适的strategy。比如,我们要选择一种合适的旅游出行路线,必须先了解选
择飞机、火车、自行车等方案的细节。此时strategy 要向客户暴露它的所有实现,这是违反最少
知识原则的。

代理模式

代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。

代理模式是一种非常有意义的模式,在生活中可以找到很多代理模式的场景。比如,明星都有经纪人作为代理。如果想请明星来办一场商业演出,只能联系他的经纪人。经纪人会把商业演出的细节和报酬都谈好之后,再把合同交给明星签。

代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象

图片预加载

var myImage = (function () {
 var imgNode = document.createElement("img");
 document.body.appendChild(imgNode);
 return {
 setSrc: function (src) {
 imgNode.src = src;
 },
 };
})();
var proxyImage = (function () {
 var img = new Image();
 img.onload = function () {
 myImage.setSrc(this.src);
 };
 return {
 setSrc: function (src) {
 myImage.setSrc("file:// /C:/Users/svenzeng/Desktop/loading.gif");
 img.src = src;
 },
 };
})();
proxyImage.setSrc("http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg");

单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。

给img 节点设置src 和图片预加载这两个功能,被隔离在两个对象里,它们可以自变化而不影响对方。何况就算有一天我们不再需要预加载,那么只需要改成请求本体而不是请求代理对象即可。

发布—订阅模式

发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。在JavaScript 开发中,我们一般用事件模型来替代传统的发布—订阅模式。

全局的发布订阅对象

const globalEvent = (function () {
 const listener = {};
 const listen = (key, fn) => {
 if (!listener[key]) {
 listener[key] = [];
 }
 listener[key].push(fn);
 }
 const remove = (key, fn) => {
 const fns = listener[key];
 if (!fns) {
 // 如果 key 对应的消息没有被人订阅,则直接返回
 return;
 }
 if (!fn) {
 fns = []
 return;
 }
 for (let i = fns.length - 1; i >= 0; i--) {
 // 反向遍历订阅的回调函数列表
 const _fn = fns[ i ];
 if (_fn === fn) {
 fns.splice(i, 1);
 }
 }
 }
 const trigger = (key, val) => {
 const fns = listener[key];
 if (!fns || fns.length === 0) {
 return;
 }
 for (let i = 0, fn; (fn = fns[i++]); ) {
 fn(val);
 }
 }
 return {
 listen,
 remove,
 trigger,
 }
}());
globalEvent.listen('squareMeter88', (fn1 = function (price) {
 console.log('价格= ' + price);
 })
);
globalEvent.listen('squareMeter88', (fn2 = function (price) {
 console.log('价格= ' + price);
 })
);
globalEvent.remove('squareMeter88', fn1); // 删除订阅
globalEvent.trigger('squareMeter88', 2000000); // 输出:2000000

先订阅后发布,并且加了命名空间的处理

const eventGlobal = (function () {
 const event = (function () {
 const namespaceMap = {};
 const defaultName = "__default__";
 const _listen = (key, cache, fn) => {
 if (!cache[key]) {
 cache[key] = [];
 }
 cache[key].push(fn);
 };
 const _remove = (key, cache, fn) => {
 const fns = cache[key];
 // 没有已监听函数
 if (!fns || !fns.length) {
 return;
 }
 if (!fn) {
 cache[key] = [];
 return;
 }
 for (let i = fns.length - 1; i >= 0; i--) {
 const _fn = fns[i];
 if (_fn === fn) {
 fns.splice(i, 1);
 }
 }
 };
 const _trigger = (key, cache, val) => {
 const fns = cache[key];
 each(fns, function () {
 this.call(null, val);
 });
 };
 const each = (fns, fn) => {
 fns.map((_fn) => fn.call(_fn));
 };
 const _create = (name) => {
 const nameSpace = name || defaultName;
 let offlineStack = [];
 const cache = {};
 const ret = {
 listen: (key, fn) => {
 _listen(key, cache, fn);
 if (!offLineStack) {
 return;
 }
 each(offLineStack, function () {
 this();
 });
 offlineStack = false;
 },
 remove: (key, fn) => _remove(key, cache, fn),
 trigger: (key, val) => {
 const fn = () => _trigger(key, cache, val);
 if (!offLineStack) {
 return fn();
 }
 offLineStack.push(fn);
 },
 };
 return namespaceMap[nameSpace] || (namespaceMap[nameSpace] = ret);
 };
 return {
 create: _create,
 listen: (key, fn) => {
 const event = _create();
 event.listen(key, fn);
 },
 remove: (key, fn) => {
 const event = _create();
 event.remove(key, fn);
 },
 trigger: (key, fn) => {
 const event = _create();
 event.trigger(key, fn);
 },
 };
 })();
 return event;
})();
/************** 先发布后订阅 ********************/
eventGlobal.trigger("click", 1);
eventGlobal.trigger("click", 12);
eventGlobal.listen("click", function (a) {
 console.log(a); // 输出:1
});
/************** 使用命名空间 ********************/
eventGlobal.create("namespace1").trigger("click", 8);
eventGlobal.create("namespace1").listen(
 "click",
 (fn1 = function (a) {
 console.log(a); // 输出:1
 })
);
eventGlobal.create("namespace1").remove("click", fn1);
eventGlobal.create("namespace1").trigger("click", 10);

发布—订阅模式在实际开发中非常有用。发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。它的应用非常 广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。发布—订阅模式还可 以用来帮助实现一些别的设计模式,比如中介者模式。 从架构上来看,无论是MVC 还是MVVM, 都少不了发布—订阅模式的参与,而且JavaScript 本身也是一门基于事件驱动的语言。 当然,发布—订阅模式也不是完全没有缺点。创建订阅者本身要消耗一定的时间和内存,而 且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。另外, 发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联 系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一 起的时候,要跟踪一个bug 不是件轻松的事情。

职责链模式

职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

var order500 = function (orderType, pay, stock) {
 if (orderType === 1 && pay === true) {
 console.log("500 元定金预购,得到100 优惠券");
 } else {
 return "nextSuccessor"; // 我不知道下一个节点是谁,反正把请求往后面传递
 }
};
var order200 = function (orderType, pay, stock) {
 if (orderType === 2 && pay === true) {
 console.log("200 元定金预购,得到50 优惠券");
 } else {
 return "nextSuccessor"; // 我不知道下一个节点是谁,反正把请求往后面传递
 }
};
var orderNormal = function (orderType, pay, stock) {
 if (stock > 0) {
 console.log("普通购买,无优惠券");
 } else {
 console.log("手机库存不足");
 }
};
// Chain.prototype.setNextSuccessor 指定在链中的下一个节点
// Chain.prototype.passRequest 传递请求给某个节点
var Chain = function (fn) {
 this.fn = fn;
 this.successor = null;
};
Chain.prototype.setNextSuccessor = function (successor) {
 return (this.successor = successor);
};
Chain.prototype.passRequest = function () {
 var ret = this.fn.apply(this, arguments);
 if (ret === "nextSuccessor") {
 return (
 this.successor &&
 this.successor.passRequest.apply(this.successor, arguments)
 );
 }
 return ret;
};
// 现在我们把3 个订单函数分别包装成职责链的节点:
var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);
// 然后指定节点在职责链中的顺序:
chainOrder500.setNextSuccessor(chainOrder200);
chainOrder200.setNextSuccessor(chainOrderNormal);
// 最后把请求传递给第一个节点:
chainOrder500.passRequest(1, true, 500); // 输出:500 元定金预购,得到100 优惠券
chainOrder500.passRequest(2, true, 500); // 输出:200 元定金预购,得到50 优惠券
chainOrder500.passRequest(3, true, 500); // 输出:普通购买,无优惠券
chainOrder500.passRequest(1, false, 0); // 输出:手机库存不足
/* 通过改进,我们可以自由灵活地增加、移除和修改链中的节点顺序,假如某天网站运营人员
又想出了支持300 元定金购买,那我们就在该链中增加一个节点即可:
*/
var order300 = function () {
 // 具体实现略
};
chainOrder300 = new Chain(order300);
chainOrder500.setNextSuccessor(chainOrder300);
chainOrder300.setNextSuccessor(chainOrder200);

小结:在JavaScript 开发中,职责链模式是最容易被忽视的模式之一。实际上只要运用得当,职责链模式可以很好地帮助我们管理代码,降低发起请求的对象和处理请求的对象之间的耦合性。职责链中的节点数量和顺序是可以自由变化的,我们可以在运行时决定链中包含哪些节点。

中介者模式

中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系

和发布—订阅模式的区别:<br />

发布—订阅模式模式:只能从从一的一方循环的通知,属于单向。
中介者模式:可以从任一方循环通知,属于双向。

泡泡堂游戏

function Player(name, teamColor) {
 this.name = name; // 角色名字
 this.teamColor = teamColor; // 队伍颜色
 this.state = "alive"; // 玩家生存状态
}
Player.prototype.win = function () {
 console.log(this.name + " won ");
};
Player.prototype.lose = function () {
 console.log(this.name + " lost");
};
/*******************玩家死亡*****************/
Player.prototype.die = function () {
 this.state = "dead";
 playerDirector.reciveMessage("playerDead", this); // 给中介者发送消息,玩家死亡
};
/*******************移除玩家*****************/
Player.prototype.remove = function () {
 playerDirector.reciveMessage("removePlayer", this); // 给中介者发送消息,移除一个玩家
};
/*******************玩家换队*****************/
Player.prototype.changeTeam = function (color) {
 playerDirector.reciveMessage("changeTeam", this, color); // 给中介者发送消息,玩家换队
};
var playerFactory = function (name, teamColor) {
 var newPlayer = new Player(name, teamColor); // 创造一个新的玩家对象
 playerDirector.reciveMessage("addPlayer", newPlayer); // 给中介者发送消息,新增玩家
 return newPlayer;
};
var playerDirector = (function () {
 var players = {}, // 保存所有玩家
 operations = {}; // 中介者可以执行的操作
 /****************新增一个玩家***************************/
 operations.addPlayer = function (player) {
 var teamColor = player.teamColor; // 玩家的队伍颜色
 players[teamColor] = players[teamColor] || []; // 如果该颜色的玩家还没有成立队伍,则新成立一个队伍;
 players[teamColor].push(player); // 添加玩家进队伍
 };
 /****************移除一个玩家***************************/
 operations.removePlayer = function (player) {
 var teamColor = player.teamColor, // 玩家的队伍颜色
 teamPlayers = players[teamColor] || []; // 该队伍所有成员
 for (var i = teamPlayers.length - 1; i >= 0; i--) {
 // 遍历删除
 if (teamPlayers[i] === player) {
 teamPlayers.splice(i, 1);
 }
 }
 };
 /****************玩家换队***************************/
 operations.changeTeam = function (player, newTeamColor) {
 // 玩家换队
 operations.removePlayer(player); // 从原队伍中删除
 player.teamColor = newTeamColor; // 改变队伍颜色
 operations.addPlayer(player); // 增加到新队伍中
 };
 operations.playerDead = function (player) {
 // 玩家死亡
 var teamColor = player.teamColor,
 teamPlayers = players[teamColor]; // 玩家所在队伍
 var all_dead = true;
 for (var i = 0, player; (player = teamPlayers[i++]); ) {
 if (player.state !== "dead") {
 all_dead = false;
 break;
 }
 }
 if (all_dead === true) {
 // 全部死亡
 for (var i = 0, player; (player = teamPlayers[i++]); ) {
 player.lose(); // 本队所有玩家lose
 }
 for (var color in players) {
 if (color !== teamColor) {
 var teamPlayers = players[color]; // 其他队伍的玩家
 for (var i = 0, player; (player = teamPlayers[i++]); ) {
 player.win(); // 其他队伍所有玩家win
 }
 }
 }
 }
 };
 var reciveMessage = function () {
 var message = Array.prototype.shift.call(arguments); // arguments 的第一个参数为消息名称
 operations[message].apply(this, arguments);
 };
 return {
 reciveMessage: reciveMessage,
 };
})();
// 红队:
var player1 = playerFactory("皮蛋", "red"),
 player2 = playerFactory("小乖", "red"),
 player3 = playerFactory("宝宝", "red"),
 player4 = playerFactory("小强", "red");
// 蓝队:
var player5 = playerFactory("黑妞", "blue"),
 player6 = playerFactory("葱头", "blue"),
 player7 = playerFactory("胖墩", "blue"),
 player8 = playerFactory("海盗", "blue");
player1.die();
player2.die();
player3.die();
player4.die();

小结:中介者模式是迎合迪米特法则的一种实现。迪米特法则也叫最少知识原则,是指一个对象应该尽可能少地了解另外的对象(类似不和陌生人说话)。如果对象之间的耦合性太高,一个对象发生改变之后,难免会影响到其他的对象,跟“城门失火,殃及池鱼”的道理是一样的。而在中介者模式里,对象之间几乎不知道彼此的存在,它们只能通过中介者对象来互相影响对方。

中介者模式可以非常方便地对模块或者对象进行解耦,但对象之间并非一定需要解耦。在实际项目中,模块或对象之间有一些依赖关系是很正常的。毕竟我们写程序是为了快速完成项目交付生产,而不是堆砌模式和过度设计。关键就在于如何去衡量对象之间的耦合程度。一般来说,如果对象之间的复杂耦合确实导致调用和维护出现了困难,而且这些耦合度随项目的变化呈指数增长曲线,那我们就可以考虑用中介者模式来重构代码。

装饰者模式

装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象
动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式,比如天冷了就多穿一件外套,需要飞行时就在头上插一支竹蜻蜓,遇到一堆食尸鬼时就点开AOE(范围攻击)技能。

数据统计上报

页面中有一个登录button,点击这个button 会弹出登录浮层,与此同时要进行数据上报,来统计有多少用户点击了这个登录button

<html>
 <button tag="login" id="button">点击打开登录浮层</button>
 <script>
 Function.prototype.after = function (afterfn) {
 var __self = this;
 return function () {
 var ret = __self.apply(this, arguments);
 afterfn.apply(this, arguments);
 return ret;
 };
 };
 var showLogin = function () {
 console.log("打开登录浮层");
 };
 var log = function () {
 console.log("上报标签为: " + this.getAttribute("tag"));
 };
 showLogin = showLogin.after(log); // 打开登录浮层之后上报数据
 document.getElementById("button").onclick = showLogin;
 </script>
</html>

与代理模式的区别

代理模式和装饰者模式最重要的区别在于它们的意图和设计目的。代理模式的目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者。本体定义了关键功能,而代理提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。装饰者模式的作用就是为对象动态加入行为。换句话说,代理模式强调一种关系(Proxy 与它的实体之间的关系),这种关系可以静态的表达,也就是说,这种关系在一开始就可以被确定。而装饰者模式用于一开始不能确定对象的全部功能时。代理模式通常只有一层代理本体的引用,而装饰者模式经常会形成一条长长的装饰链。

总结

学以致用,扩充思维的广度和深度。
理想很美好,现实很残酷,这句话只有在亲身经历了项目重构才能深得体会,项目的历史债务,终究有一代人要还的。

作者:introvert-y

%s 个评论

要回复文章请先登录注册