怎么去写好一段优雅的程序

叁叁肆2018-10-22 11:35

此文已由作者吴维伟授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。


写好一段优雅程序的必要条件是良好的设计。


写程序就像在走一个迷宫。编写之初,有若干个可能的解决方案萦绕在我们的脑海。我们选择一个继续深入,可能达到终点——实现了功能需求,但更大的可能是进入了一个死胡同或者一个新的岔路口,需要重新进行抉择,如此反复。


想起一年前的自己,仅凭着生物的本能去写着代码:我依照着以往的经验,先写了一段。然后刷新一下页面,查看是否离实现需求更近了一步。幻想着程序可以完美运行的我看到最多的是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+款云产品! 

更多网易技术、产品、运营经验分享请点击


相关文章:
【推荐】 3分钟带你了解负载均衡服务
【推荐】 SQLite FTS3/FTS4与一些使用心得