- 原文地址:4 Useful JavaScript Design Patterns You Should Know
- 原文作者:bitfish
- 译文出自:掘金翻译计划
- 本文永久链接:github.com/xitu/gold-m…
- 译者:Zavier Tang
- 校对者:niayyy-S、CuteSunLee
你是否像我以前一样对设计模式感到非常困惑呢?就我自身而言,我以前的个人项目,几乎没有考虑过设计模式。因此,存在很多设计缺陷和代码缺陷,给以后的维护和迭代带来了不少的麻烦。
但是要成为一名优秀的程序员,其基本要求之一就是精通设计模式。为此,我阅读了大量关于设计模式的书籍和博客,但一开始只学习到了一些理论知识。从来没有在实际项目中使用到。
直到后来我开始去阅读一些知名开源项目的源码,并在实际工作中请教了许多前辈,我才逐渐掌握了设计模式的使用。于是,我总结了一些自己的经验并写下了这篇文章,希望能对大家有所帮助。
本文将讨论以下设计模式的使用:
引用维基百科:
在软件工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解決方案。
这可能有点抽象,但你可以这样去理解:现在假设你正在玩一个电子游戏,你花了五个小时才完成通关。但是在你下一次玩的时候,因为你之前已经玩过了,积累了一些技巧,所以这次只需要两个小时就可以完成通关。这时你的朋友问你玩这个游戏的技巧,于是你告诉了他一些自己的经验,这就是所谓的设计模式。
我们开发的软件的功能总是随着时间而变化。设计模式的价值在于更好地组织我们当前的代码,并使其在未来更易于迭代。
现在让我们通过真实的例子来学习设计模式吧。
假设我们有这样的需求,当用户试图打开一个页面时,只有满足以下的条件才能看到对应的内容:
现在我们需要编写判断逻辑,以确保只有符合条件的用户才能看到内容。你会怎么做?许多初学编程的人可能会简单地选择 if-else
并像下面这样编写代码:
function checkAuth(data) { if (data.role !== 'registered') { console.log('该用户不是注册用户'); return false; } if (data.grade < 1) { console.log("用户级别小于1"); return false; } if (data.job !== 'FE') { console.log('该用户不是前端开发工程师'); return false; } if (data.type !== 'active user') { console.log('该用户不是活跃用户'); return false; } } 复制代码
我相信大家以前都写过类似的代码,但它有以下问题:
checkAuth
是臃肿的那么我们如何解决这个问题呢?这就是策略模式发挥作用的时候了。
它是一种允许封装用于特定任务的备选算法的设计模式。它可以定义一系列算法,并以这样一种方式封装它们。它们在运行时可以互换调用顺序,而不需要编写额外的代码。
现在让我们使用策略模式来改写上面的代码。
const jobList = ['FE', 'BE']; var strategies = { checkRole: function(value) { if (value === 'registered') { return true; } return false; }, checkGrade: function(value) { if (value >= 1) { return true; } return false; }, checkJob: function(value) { if (jobList.indexOf(value) > 1) { return true; } return false; }, checkType: function(value) { if (value === 'active user') { return true; } return false; } }; 复制代码
上面的代码是我们将使用的策略列表,我们将继续完成验证逻辑。
var Validator = function() { // Store strategies this.cache = []; // add strategy to cache this.add = function(value, method) { this.cache.push(function() { return strategies[method](value); }); }; // check all strategies this.check = function() { for (let i = 0; i < this.cache.length; i++) { let valiFn = this.cache[i]; var data = valiFn(); if (!data) { return false; } } return true; }; }; 复制代码
好了,现在让我们来实现前面的需求。
var compose1 = function() { var validator = new Validator(); const data1 = { role: 'register', grade: 3, job: 'FE', type: 'active user' }; validator.add(data1.role, 'checkRole'); validator.add(data1.grade, 'checkGrade'); validator.add(data1.type, 'checkType'); validator.add(data1.job, 'checkJob'); const result = validator.check(); return result; }; 复制代码
看了上面的代码之后,你可能会想:代码量似乎增加了。
但正如我们之前所说的,设计模式的价值在于它能使你更容易地应对变化的需求。如果你的需求从头到尾都没有改变,那么使用设计模式真的没有多大价值。但是,如果当项目的需求发生了变化,那么设计模式的价值就可以得到体现。
例如,在另一个页面,我们对用户的验证逻辑是不同的,比如我们只需要确保:
在这里,我们发现我们可以很容易地重用以前的代码:
var compose2 = function() { var validator = new Validator(); const data2 = { role: 'register', job: 'FE' }; validator.add(data2.role, 'checkRole'); validator.add(data2.job, 'checkJob'); const result = validator.check(); return result; }; 复制代码
我们可以看到,通过使用策略模式,我们的代码变得更易于维护。现在可以考虑将策略模式应用到你自己的项目中了,例如在处理表单验证时。
当你的需求基本上满足以下条件时,你可以考虑使用策略模式来优化代码。
现在我们来看另一个需求:当用户成功提交申请时,后台需要触发相应的订单、消息和审计模块。
如何编写代码呢?许多程序员可能会这样写:
function applySuccess() { // 通知消息中心获取最新内容 MessageCenter.fetch(); // 更新订单信息 Order.update(); // 通知负责人审核 Checker.alert(); } 复制代码
这看起来还不错。
当然,代码本身并没有什么直接的错误,但是在实践中,它很可能会发生:
MessageCenter
最初是由 Jon 开发的,他后来因为某些原因将 MessageCenter.fetch
重新命名为MessageCenter.request
。这将导致你需要更改 applySuccess
函数,否则代码会报错。Order.update
方法。这使得你的代码不可用,并且只能临时删除该函数。更糟糕的是,你的项目常常不只依赖于这三个模块。例如,当申请成功时,我们需要提交一个日志。你如何处理这种情况?可能需要再次修改原始函数。
function applySuccess() { // 通知消息中心获取最新内容 MessageCenter.fetch(); // 更新订单信息 Order.update(); // 通知负责人审核 Checker.alert(); Log.write(); // 更多... // ... } 复制代码
随着涉及的模块越来越多,我们的代码变得越来越臃肿,维护起来也越来越困难。这时,发布-订阅模式就可以优雅地解决问题。
你是否觉得 EventEmitter 很熟悉?没错,在面试中经常会出现?
发布-订阅是一种消息传递范例,消息的发布者不直接将消息发送给特定的订阅者,而是通过消息通道广播消息,订阅者通过订阅获得他们想要的消息。
首先,让我们写一个 EventEmit 函数:
const EventEmit = function() { this.events = {}; this.on = function(name, cb) { if (this.events[name]) { this.events[name].push(cb); } else { this.events[name] = [cb]; } }; this.trigger = function(name, ...arg) { if (this.events[name]) { this.events[name].forEach(eventListener => { eventListener(...arg); }); } }; }; 复制代码
上面我们写了一个 EventEmit 函数,然后我们的代码可以改为:
let event = new EventEmit(); MessageCenter.fetch() { event.on('success', () => { console.log('update MessageCenter'); }); } Order.update() { event.on('success', () => { console.log('update Order'); }); } Checker.alert() { event.on('success', () => { console.log('Notify Checker'); }); } event.trigger('success'); 复制代码
这样是不是更好呢?所有的事件都是相互独立的。我们可以在不影响其他模块的情况下随时添加、修改和删除事件。
当你负责一个基本上满足以上条件的模块时,可以考虑使用发布-订阅模式。
让我们直接来看一个例子吧。
了解 React 的人都知道,高阶组件实际上只是一个函数。它接受一个组件作为参数并返回一个新组件。
所以,让我们来编写一个高阶组件(HOC),并使用它来装饰 TargetComponent。
import React from 'react'; const yellowHOC = WrapperComponent => { return class extends React.Component { render() { <div style={{ backgroundColor: 'yellow' }}> <WrapperComponent {...this.props} /> </div>; } }; }; export default yellowHOC; 复制代码
在上面的代码中,我们定义了一个装饰黄色背景的高阶组件,我们使用它来装饰目标组件。
让我们看看,这里是如何使用这个高阶组件:
import React from 'react'; import yellowHOC from './yellowHOC'; class TargetComponent extends Reac.Compoment { render() { return <div>hello world</div>; } } export default yellowHOC(TargetComponent); 复制代码
在上面的例子中,我们设计了组件 yellowHOC 来包装其他组件。这就是装饰者模式。
你是否会感到困惑呢?让我们看一下装饰者模式的另一个例子。
// Jon was originally a Chinese speaker const jonWrite = function() { this.writeChinese = function() { console.log('I can only write Chinese'); }; }; // Add the ability to write English to Jon through the decorator const Decorator = function(old) { this.oldWrite = old.writeChinese; this.writeEnglish = function() { console.log('Give Jon the ability to write English'); }; this.newWrite = function() { this.oldWrite(); this.writeEnglish(); }; }; const oldJonWrite = new jonWrite(); const decorator = new Decorator(oldJonWrite); decorator.newWrite(); 复制代码
假设我们在向公司申请购买一件设备时,必须遵循以下流程:
许多初学者将此视为非常简单的需求,并开始编写这样的代码。
function applyDevice(data) { // some code to apply device // ... // Then go to the next step selectAddress(nextData); } function selectAddress(data) { // some code to select address // ... // Then go to the next step selectChecker(nextData); } function selectChecker(data) { // Some code to select a person to review // ... } 复制代码
看起来已经满足了要求,但实际上,上面有一个非常大的缺点:我们的采购流程可能会改变,比如增加一个库存检查流程。然后,你必须彻底地更改原始代码,这对于维护代码设计是非常困难的。
在这一点上,我们可以考虑使用责任链模式。
我们可以这样改写代码:
const Chain = function(fn) { this.fn = fn; this.setNext = function() {} this.run = function() {} } const applyDevice = function() {} const chainApplyDevice = new Chain(applyDevice); const selectAddress = function() {} const chainSelectAddress = new Chain(selectAddress); const selectChecker = function() {} const chainSelectChecker = new Chain(selectChecker); chainApplyDevice.setNext(chainSelectAddress).setNext(chainSelectChecker); chainApplyDevice.run(); 复制代码
这样有什么好处呢?我们做的第一件事是解耦,我们之前的方法是在函数 A 中调用函数 B,然后在函数 B 中调用函数 C。但是现在它不同了,每个函数都是相互独立的。
现在,假设我们需要在申请设备后选择地址之前检查库存。在代码中使用责任链模式,我们可以通过简单地修改代码来完成需求。
当你负责的模块满足以下条件时,请考虑使用责任链模式。
如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。