此文已由作者吴维伟授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
写好一段优雅程序的必要条件是良好的设计。
写程序就像在走一个迷宫。编写之初,有若干个可能的解决方案萦绕在我们的脑海。我们选择一个继续深入,可能达到终点——实现了功能需求,但更大的可能是进入了一个死胡同或者一个新的岔路口,需要重新进行抉择,如此反复。
想起一年前的自己,仅凭着生物的本能去写着代码:我依照着以往的经验,先写了一段。然后刷新一下页面,查看是否离实现需求更近了一步。幻想着程序可以完美运行的我看到最多的是JavaScript报错和意料之外的运行结果,那种被QA们称作BUG的东西。于是,我又凭着本能做出了修改……恍恍惚惚,不知经过了多久,程序终于运行在一个貌似正确的逻辑轨道上了。嗯?你问我程序里会不会有什么bug?这个,我还真不敢确定呢。
我需要一份迷宫的地图,避开所有的死胡同,找到一条最优的路径到达出口,这就是设计。
我们设计一段程序与PM规划一个产品的过程有些类似——首先对需求进行收集和整理,然后明确需要实现的N条功能,最后依次进行实现。不同的是,我们的用户就是我们自己,所以我们更具优势,更容易设计出一段易于使用的程序。
设计程序的第一步是明确程序中需要实现的功能点。许多的功能点罗列在面前,是把它们实现在一个模块里呢,还是分多个模块去实现?如果分多个模块,每个模块都要实现哪些功能点呢?这些问题当然不能冒然的拍脑门决定,需要考虑可复用性和维护性。
想象一个登陆功能,需求是这样的:我们需要把用户信息发送给后端进行验证,如果成功则刷新页面。功能很简单,很容易把代码写了出来:
//模块逻辑class Login { login () { this.verify(function () { window.location.reload(); }); } /** * @description 验证用户信息。 * @param callback {Function} 验证通过后执行的回调函数 */ verify (callback) { //do something }}//模块调用new Login().login();
瞬间搞定,So Easy!
弄完没多久,来了新需求。假设刚刚是页面A,现在要实现的页面B中的登陆逻辑与A有了一些不同:登陆成功后不再进行页面刷新,而是直接更新页面内关于用户信息的显示。
现在要怎么实现呢?从零开始重新实现一个页面B的登陆逻辑?首先排除这种做法,毕竟验证用户信息这部分逻辑并没有发生改变,可以复用。想了想,写下了这样的代码:
//模块逻辑class Login { /** * @param state {Number} 1 登陆后刷新 2 登陆后更新用户数据 */ login (state) { this.verify(function () { if(state === 1) window.location.reload(); else if(state === 2) doSomethingWithUserInfo(); }); } /** * @description 验证用户信息。 * @param callback {Function} 验证通过后执行的回调函数 */ verify (callback) { //do something }}//模块调用//登陆后刷新new Login().login(1);//登陆后更新用户数据new Login().login(2);
嗯,很好地满足了需求。但是这种实现方式过于僵硬,不太灵活。假设有页面C,页面D,其登陆逻辑中登陆成功后执行的操作又有不同。此时需要再次修改`Login.prototype.login`方法中的实现,那么以前能够稳定运行的逻辑就会有被改坏的可能。好的程序结构应该对扩展开放,对修改关闭。就是说,期望中,无论又增加哪些登陆成功后执行的操作,我们都不需要修改原来的代码。
所以,重构了下:
//模块逻辑class Login { login () { this.verify(function () { this.doAfterLogin(); }.bind(this)); } /** * @description 验证用户信息。 * @param callback {Function} 验证通过后执行的回调函数 */ verify (callback) { //do something } /** * @abstract */ doAfterLogin () { //子类实现具体逻辑 }}class LoginA extends Login { doAfterLogin () { window.location.reload(); }}class LoginB extends Login { doAfterLogin () { doSomethingWithUserInfo(); }}//模块调用//登陆后刷新new LoginA().login();//登陆后更新用户数据new LoginB().login();
将一些公用的逻辑提取到父类`Login`中。登陆成功后的操作每一次变化,只需要继承`Login`类,在新的子类中实现具体的逻辑。这样对已有功能不会产生任何影响。简直完美!
沾沾自喜中,又来了新需求。假设现在又要实现页面E,页面F。页面E中登陆后的操作是刷新页面,与A相同。页面F中登陆后的操作是更新用户信息展示,与B相同。但是它们不再通过自己的后端来验证用户信息,而是通过URS和VRS(不要问我VRS是什么鬼……)。现在需要复用的部分不仅仅是对用户信息进行验证的功能,还有登陆成功后执行的操作。面对这样的需求,仅仅通过继承是不能将已有功能最大化复用的,需要将登陆验证和登陆后执行的操作这2个功能点划分到不同的模块中。于是,可以这样实现:
//模块逻辑class Verify { /** * @abstract * @description 验证用户信息。 * @param callback {Function} 验证通过后执行的回调函数 */ verify (callback) { //子类实现具体逻辑 }}class VerifyNormal extends Verify { verify (callback) { //通过自己后台进行验证 }}class VerifyURS extends Verify { verify (callback) { //通过URS进行验证 }}class VerifyVRS extends Verify { verify (callback) { //通过VRS进行验证 }}class Login { /** * @param verify {Verify} */ login (verify) { verify.verify(function () { this.doAfterLogin(); }.bind(this)); } /** * @abstract */ doAfterLogin () { //子类实现具体逻辑 }}class LoginA extends Login { doAfterLogin () { window.location.reload(); }}class LoginB extends Login { doAfterLogin () { doSomethingWithUserInfo(); }}//模块调用 //普通登陆,登陆后刷新页面new LoginA().login(new VerifyNormal());//普通登陆,登陆后更新用户信息显示new LoginB().login(new VerifyNormal());//URS登陆,登陆后刷新页面new LoginA().login(new VerifyURS());//URS登陆,登陆后更新用户信息显示new LoginB().login(new VerifyURS());//VRS登陆,登陆后刷新页面new LoginA().login(new VerifyVRS());//VRS登陆,登陆后更新用户信息显示new LoginB().login(new VerifyVRS());
总结一下:如果一个模块只有一个变化的原因(只有登陆后的操作会变化时),可以通过继承来满足开闭原则(对扩展开放,对修改关闭)。但是如果一个模块有多个变化的原因(如登陆后的操作和登陆验证流程都会发生变化),我们就需要把其中一个变化原因划分到另外一个模块中。一个模块只能有一个变化的原因(单一职责原则)。将功能点友好地划分到每一个模块,那么一段好程序的雏形也就被塑造出来了,剩下的就是往里面狠狠的填充。
网易云免费体验馆,0成本体验20+款云产品!
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 一份ECMAScript2015的代码规范(下)
【推荐】 理解DDoS防护本质:基于资源较量和规则过滤的智能化系统
原文地址:https://www.cnblogs.com/163yun/p/9830234.html