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


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

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


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


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


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

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


    相关文章:
    【推荐】 基于Impala平台打造交互查询系统
    【推荐】 abtest-system后台系统设计与搭建

  • 相关阅读:
    五个字符就能让你电脑死机
    易语言e.exe在一些系统运行出错解决方法
    检测是否联网
    JS判断设备的类型
    JavaScript判断移动端及pc端访问不同的网站
    代码片段
    WEB前端知识在乱花渐欲迷人眼的当下,如何分清主次和学习优先级呢?
    说说JSON和JSONP,也许你会豁然开朗
    HTML5 LocalStorage 本地存储
    namenode 和datanode无法启动,错误:FSNamesystem initialization failed. datanode.DataNode: Incompatible namespaceIDs
  • 原文地址:https://www.cnblogs.com/zyfd/p/9829239.html
Copyright © 2020-2023  润新知