良好代码设计的10个经验

作者: 少个分号 | 来源:发表于2019-02-22 10:16 被阅读65次

    良好的代码设计可以大大减少维护的成本和潜在的bug,甚至由于积累效应会决定一个项目能否成功。在常年的codereview和查看面试作业你中发现,良好的设计和糟糕的代码设计的区别十分明显。

    总结了几条实用的经验,希望对后面的项目有所帮助。文中例子使用 JavaScript 和 Java,示例仅做演示使用,这些例子都是实际遇到过的,做了一些简化。

    快速退出

    如果在一个方法中遇到大量的条件判断,正常的情况下都会想到使用嵌套if 语句或者使用 else if,但是这样一来就会出现非常复杂的嵌套。这在维护上带来了很大的麻烦。

    function getRecommendedProductCover(user){
        const defaultCover = 'http://xxxx.jpg'
        if(user.type === 'merchant'){
            let product = getRecommendedProductByUser(user.id)
            if(product && product.picture){
                if(product.picture.src){
                    return product.picture.src
                }
            }
        }
        return defaultCover
    }
    

    我们可以使用逆向思维来解决这个问题,如果条件满足就是用 return 快速退出。

    function getRecommendedProductCover(user){
        const defaultCover = 'http://xxxx.jpg'
    
        if(user.type !== 'merchant'){
            return defaultCover
        }
    
        let product = getRecommendedProductByUser(user.id)
        if(!product || !product.picture){
            return defaultCover
        }
    
        if(product.picture.src){
            return product.picture.src
        }else{
            return defaultCover
        }
    }
    

    当然还可以使用一些中间变量等方法解决嵌套的问题,这种方式可以让代码看起来更加直观、简洁,有时候也可以避免无意义的数据获取。

    消除 if 和 else if

    我们看一个前端模板中非常常见的一种写法,我在大量的面试作业中出现。

    state = {index: 1};
    
    render() {
        const {index} = this.state;
        return (
            <div className="tab-list">
                <Navigation index={index} onChange={index => this.setState({index})}/>
                {index === 0 && <div>Tab1 content</div>}
                {index === 1 && <div>Tab2 content</div>}
                {index === 2 && <div>Tab3 content</div>}
                {index === 3 && <div>Tab4 content</div>}
            </div>
        );
    }
    

    这是我在一份前端作业中遇到的一个标签页切换的例子,短路运算符相当于 if 语句,显然作者把这个列表中每一个元素拿出来做了一次判断。这个逻辑显然使用循环就可以简单优雅的实现。

    我们再来看另外一个常见的例子。

    function handleErrorMessage(response){
        if(response.status === '401'){
            alert('没有登录')
        }else if(response.status === '403'){
            alert('没有权限')
        }else if(response.status === '500'){
            alert('内部错误')
        }else{
            alert('未知错误')
        }
    }
    
    

    这个例子在前端的项目中非常常见,但不一定是处理错误消息,处理这种场景也非常简单,使用一个Map对象来做一个映射即可大大提高程序的维护性。

    下面一个例子更加具有代表性,如果我们需要在页面上预览不同的媒体格式,在 mediaPreview 方法中可能会写的非常复杂。

    function mediaPreview(object){
        if(object.type === 'video'){
            ...
        }
        if(object.type === 'picture'){
            ...
        }
        ...
    }
    

    这个时候我们可以借助更为高级的设计模式,将不同类型的处理逻辑隔离在单独的模块中,同样可以使用一个Map对象来维护一个具体实现逻辑的列表。

    const previewMethods{
        video: function(){
          ...
        },
        picture: function(){
          ...
        }
    }
    function mediaPreview(object){
        previewMethods[object.type]()
    }
    

    是不是看起来像某种设计模式了呢。

    分层设计

    分层设计是一种经典的程序设计,Java 服务器编程中往往有 presentation、service和dao层,现代前端也会用redux等框架来对数据和视图进行分层。

    那什么样的设计是易于维护的分层设计呢?我们先看一个反面的例子。

    public class ProductService {
        public void updateProduct(HttpServletRequest httpServletRequest) {
            String queryString = httpServletRequest.getQueryString();
            ...
        }
    }
    

    在一次 codereview 中看到这样一段代码,逻辑是需要在service中根据条件更新商品,作者可能为了省事儿,直接把 HttpServletRequest 这个对象在各个参数中直接传递,虽然这样看起来避免了冗余的value object定义,以及减少了参数的数量。但这样做的后果是 ProductService 和 ServletRequest 耦合了,从而造成这个service失去了复用的能力。

    image.png

    应该尽量避免程序分层被穿透

    正确的做法是自定义一个条件类,或者直接把查询条件放到,参数列表上。

    public class ProductService {
        public void updateProduct(String productId, String userId) {
            ...
        }
    }
    

    回到我们的问题,什么是好的分层设计呢?答案是每个层能被无痛的替换。以在Java 后端中的presentation层为例,一套使用表单和JSP 模板的presentation层,如果具有良好的设计可以容易的被替换成RESTful的API 而不用对service层做任何修改。

    软件设计领域,最好的分层设计应该像 TCP/IP 协议族那样,每层都可以有不同的实现,所以我们才能做到WIFI和4G网络都能访问Google。

    image.png

    作用域隔离

    前端开发,或者其他弱类型语言如PHP,在模块和包管理上没有语言层面足够的支持。需要特别注意作用域的暴露,先给一个前不久维护一个遗留项目中的一个例子。

    其中一个 js 文件中有一个变量

    // a.js
    var isWebview = (window.navigator.userAgent.indexOf('_APP') !== -1)
    if(isWebview){
        ...
    }
    

    后来在另外一个 js文件中也定义了isWebview方法,造成冲突。

    // b.js
    function isWebview(){ 
        return (window.navigator.userAgent.indexOf('_APP') !== -1)
    }
    

    由于JavaScript的类型转换 a.js 中的条件判断,就会一直当做 true。

    在JavaScript中解决的方法其实很简单,可以使用function创建一个局部作用域空间,这也是前端包管理的基本原理。

    !(function(){
        var isWebviewg = (window.navigator.userAgent.indexOf('_APP') !== -1)
        console.log(isWebviewg)
    
        //  创建一个局部作用域,使用 window 向外部暴露接口
        window.xxx = ...
    })
    

    实际上前端开发中尽量应避免使用window对象。

    编写一个合格函数

    编写函数谁不会,然而我在面试作业中碰到了这样的例子。

    public class Utils {
        public void modifyScore(Answer answer) {
            answer.setScore(answer.getScore() + 10)
            ...
        }
    }
    
    

    这是一份 Java 作业,作者编写了一个Util 方法来修改答案的分数,但是这个方不是静态方法,甚至没有返回值,直接对传入的对象进行修改。

    var formData = {
        phone:'123456',
        ...
    }
    
    function validateForm(){
        var phoneRegx = /xxxx/
        var valid = honeRegx.test(ormData.phone)
        ...
        return valid
    }
    

    这是一份 JavaScript 作业,作者拆分了验证表单的方法,但是这个方法访问了外部变量,让这个方法的拆分变得毫无意义。

    好的函数或者方法,一定是通过参数获取输入,通过返回值输出数据,函数体内部不应该和外部有任何联系。

    程序上下文

    在软件开发领域,有一个概念一直没有得到重视,那就是我们编写应用程序就像撰写文章,是有一个上下文存在的。

    使用了Spring boot 会产生一个Spring 上下文来管理Beans并提供了其他特性。使用了 Redux 也会存在Redux这样一个上下文。

    我们再编写代码时,始终需要意识到这个上下文的存在。才能最大的利用框架和库提供的特性,但是也需要时刻警惕这个上下文中的一些限制。

    例如在前端开发中,很多UI框架提供了组件的两种引用方式。一种是全局的载入和配置,第二种是可以局部单独引入构建更为复杂的应用。

    在一些遗留的项目中,需要注意多套技术栈共同的存在,项目中往往多个上下文存在。编写一些公用的代码时,尽量剥离上下文的依赖和使用函数式编程,这样可以做到跨上下文复用,也可以未来跨项目使用。

    我们一个项目因为历史遗留的问题一些JavaScript使用的ES5编写的,另外一些使用了ES6和打包工具编写的。在一些公共模块就难以做到复用,于是我们只能将ES6的代码从上下文中剥离(例如重新引入依赖库)打包成独立的JS UMD包文件供ES5通过scripts引入。

    字符串

    在项目中大量拼接字符串,会造成代码的可读性可维护性降低。

    例如前端在处理URL时的问题:

    const urls = {
        PRODUCTS:'/products/'  
    }
    function fetchProductDetail(productID){
        return fetch(urls.PRODUCTS+productID)
    }
    

    上面的例子中需要拼接详情的URL地址,且无法体现detail资源中的path参数。字符串的处理应该尽量避免使用+操作符链接字符串,而是使用模板来处理,模板可以用任何地方而不只是输出。

    这里编写了一个处理URL path 参数的小方法。

    const urls = {
        PRODUCTS:'/products/',
        PRODUCTS_DETAIL:'/products/:id'  
    }
    function fetchProductDetail(productID){
        return fetch(paramsPath(urls.PRODUCTS_DETAIL,productID))
    }
    
    function paramsPath(pathString, ...params) {
      let result = pathString
      params.forEach((value) => {
        result = result.replace(/\:\w+/, value)
      })
      return result
    }
    
    

    JavaScript 成熟的模板库非常多,例如Ejs、Handlebars、underscore中template.不要仅仅在视图中使用模板,而是需要处理字符串的地方都可以使用。

    理解接口

    在项目中我们容易走进一个误区,看到一个场景觉得挺适合一种设计模式的,于是就引入一种设计模式。

    首先,很多设计模式之间的区别非常微妙,同一种场景可以使用不同的设计模式实现。例如策略模式和门面模式某些时候很难区分。

    其次,有时候我们不需要一个设计模式完整的实现,可以参考这种设计模式的思想做一些定制。

    在使用模式之前需要彻底理解的是面向对象,特别是继承和接口。由于JavaScript没有语言层面的的 interface,但实际山接口无处不在。

    比如,如果一组信息需要渲染给显示器和打印机等不同的设备,我们可能这样写。

    const PrintViewer = {
      render(){
        ...
      }
    }
    
    const BrowserViewer = {
      render(){
        ...
      }
    }
    
    function render(){
      // 使用工厂方法获取不同的渲染对象
      const reviewer =  this.getViewer()
      reviewer.render(this.data)
    }
    

    在前端开发中,如果不使用TypeScript 等类型工具,我们无法做到在语法层面上检查每个 viewer 都有render方法。但是如果想让程序正确运行,我们不得不在团队中约定,viewer 比如提供一个render 方法。

    在软件开发中,接口实际上是一种约定。

    image.png

    在现实世界中接口是通信设备和零件之间的契约

    复用的陷阱

    在做前端开发的时候非常容易陷入一个误区,就是把视图拆分成独立的组件,拆分代码这个实践没有问题,但是应该注意为何而拆。

    比如我们有一个数据列表页面,这个页面上大致有筛选部分、内容部分、分页部分,在codereview 中看到过一个 vue 的案例,有人把一个也买的呢头部、中部、底部拆分成不同的组件,但是这些组件是当前页面内部和业务相关的,只是不同的部分而已。

    拆分之后各个组件的数据又需要通过props和event来传递,带来了额外的负担。

    因此我们在组件拆分的时候,需要考虑以下几点:

    1. 拆分出的组件是否和业务有关联,如果和业务关联需要进行剥离
    2. 从业务代码中抽离组件,而不是提前设计组件
    3. 考虑组件拆分的成本和收益,组件拆分后会带来组件之间通信、可读性下降等潜在成本
    image.png

    起一个好名字

    曾经接手过一个遗留项目,这个项目的原作者我猜测应该是广东人,因为变量命名的风格太过于清奇。变量名中不仅有英文简写(不完整的单词例如 btn )、数字和拼音,更为恐怖的是驼峰和下划线混用,甚至拼音都不是普通话。

    $sqlus = "xxx";
    $rsus = mysql_query($sqlus);
    $countus = mysql_fetch_assoc($rsus);
    $usercxpass = $countus["cx_pass"] . ',' . $pa_cjh;//车架A
    $arr = array($countus["cx_pass"]);//编码数组
    $arrsl = explode(",",$countus["cx_shul"]);//品牌查询次数数组
    /////////////////pdcxsz////////////////////////
    $arrnull = array();//空数组
    foreach ($arrsl as $key => $values) {//查询并写入新
        if (strstr($values, $pa_pingp) !== false) {
            array_push($arrnull, $values);
        }
    }
    if ($arrnull[0] == "" and $countus["cx_date"] == $l_date1) {//日期当前不存在就写入
        $arrsl[] = $pa_pingp . '1';//写入新查询
    //  print_r($arrsl);
    } elseif ($countus["cx_date"] <> $l_date1) {//日期之前不存在就写入
        $arrsl = array($pa_pingp . '1');//写入新查询
    } elseif ($arrnull[0] <> "" and $countus["cx_date"] == $l_date1) {//存在就修改
        $czxincs = substr($arrnull[0], -1);//实已查
        $dqppkey = array_search($arrnull[0], $arrsl);//已查当前分健值
    //  echo $czxincs.'<br />';
        $arrsl[$dqppkey] = $pa_pingp . ($czxincs + 1);//更新数组
    }
    

    我想上面这段代码几乎没人看懂,所以起一个好的名字对维护性非常重要,所以在变量命名时最好遵循下面的规则:

    1. 使用一种命名风格,驼峰或者下划线
    2. 避免使用拼音和数字
    3. 避免使用缩略词
    4. 注意单词错误、时态和大小写

    另外有的时候实在想不出变量名,可以借助一些工具,例如 https://unbug.github.io/codelf/ 提供了浏览器和编辑器插件,可以从开源代码库中搜索一些有用的代码作为参考。

    另外一种我比较喜欢的命名方式是参考一些标准写法。“微格式”是一种互联网“潜规则”,用于赋予HTML元素有意义的名字便于第三方应用程序或搜索引擎抓取。所以我在编写前端代码时,参考微格式不用费脑筋设计HTML结构和命名,同时也有让HTML足够语义化。

    image.png

    图片来源:https://developer.mozilla.org/en-US/docs/Web/HTML/microformats

    相关文章

      网友评论

        本文标题:良好代码设计的10个经验

        本文链接:https://www.haomeiwen.com/subject/wgesyqtx.html