代码整洁之道-函数

205 阅读4分钟
  1. 小(small): 函数应该尽量短小.

  2. 只做一件事(Do One Thing): 函数里面的步骤应该是是属于函数名这一层抽象下面的(Notice that the three steps of the function are one level of abstraction below the stated name of function). 函数最好都能够转换成一段以“为了”开头的文字描述.

  3. 每个函数都代表一层抽象(One Level of Abstraction per Function): 一个函数内部通过调用代表下一层抽象的不同函数来完成该层函数抽象的不同步骤. 整个函数可以转化为一段“为了”开头的描述, 而函数内部的函数调用也可以转化为一段“为了”开头的描述. 这个结构类似于文章的总—分结构.

  4. *使用说明性的名称(Use Descriptive Names): 一个长的说明性的名称比一个短的令人费解的名称要好. 一个长的说明性的名称比一段长的注释要好(A long descriptive name is better than a short enigmatic name. A long descriptive name is better than a long descriptive comment.)

  5. 函数的参数(Function Arguments):

    • 一元参数(Common Monadic Forms):
      输入一个参数, 将它转化为某种形式, 然后返回(Or you may be operating on that argument, transforming it into something else and returning it); 输入一个参数, 但是没有返回值(There is an input argument but no output argument).
      不要输出作为参数传递一个函数.

    • 标志函数(Flag Arguments):
      不要将标识符作为一个函数函数参数传递进去, 如果需要区分, 则应该拆分成两个函数.

    • 二元参数(Dyadic Functions):
      二元函数最好是同一个值有序组成部分, 例如函数的一个点有横坐标和纵坐标, 这两个坐标是构成点的有序组成部分, 不能分割, 顺序也不能颠倒.

    • 三元参数(Triads):
      尽量不要使用三元的参数, 三元参数的顺序, 捉摸, 忽略的问题都会加倍(The issues of ordering, pausing, and ignoring are more than doubled).

    • 参数对象(Argument Object):
      当参数超过三个以上时, 应该将某些参数封装为有意义的对象. 例如代表一个点的横坐标和纵坐标就可以封装在一个点的对象中进行传递.

    • 线性表对象(List Objects):
      对于可变参数来说, 它们应该被视作同一类对象的线性表参数, 应该被视作一个参数.

    • 动词和关键字(Verb And Keywords):

      assertEquals(expected, actual);
      // 这里通过函数名直接表明了参数调用顺序
      assertExpectedEqualsActual(expected, actual);
      
  6. 不要有副作用(Have No Side Effects):

    • 输出参数(Output Arguments): 函数的参数多数情况下会自然地被看作函数的输入(Arguments are naturally interpreted as inputs to a function). 应该尽量避免函数参数作为输出.
  7. 分开指令和查询(Command Query Seperation):
    函数要么做某件事, 要么回答某件事, 但不要同时做这两件事(Functions should either do something or answer something, but not both).

    /**
     * 这个函数判断有没有属性. 若没有属性则返回 false; 
     * 若有属性则设置该属性的值, 然后返回 true
     */
    public boolean set(String attribute, String value);
    
    /**
     * 分开查询和命令
     */
    if (attributesExists("username")) {
        setAttribute("username", "unclebob");
    }
    
  8. 使用异常代替返回错误码(Prefer Exceptions to Returning Error Codes):
    返回错误码会鼓励在 if 判断语句中把指令当作表达式使用(It promotes commands being used as expressions in the predicates of if statement).

    // 使用错误码
    if (deletePage(page) == E_OK) {
        if (registry.deleteRefence(page.name) == E_OK) {
            if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {
                logger.log("page deleted");
            } else {
                logger.log("configKey not deleted");
            }
        } else {
            logger.log("deleteReference from registry failed");
        }
    } else {
        logger.log("deleted failed");
        return E_ERROR;
    }
    
    // 使用异常
    try {
        deletePage(page);
        registry.deleteReference(page.name);
        configKeys.deleteKey(page.name.makeKey());
    } catch (Exception e) {
        logger.log(e.getMessage());
    }
    // 提取 try-catch 块
    public void delete(Page page) {
        try {
            deletePageAndAllReference(page);
        } catch(Exception e) {
            logError(e);
        }
    }
    
    private void deletePageAndAllReferences(Page page) throws Exception {
        deletePage(page);
        registry.deleteReference(page.name);
        configKeys.deleteKy(page.name.makeKey());
    }
    
    private void logError(Exception e) {
        logger.log(e.getMessage());
    }
    
  9. 错误处理就是一件事(Error Handling Is One Thing):
    使用错误码可能会导致依赖磁铁(Dependency Magnet)

    /**
     * 错误码枚举类
     */
    public enum Error {
        OK,
        INVALID,
        NO_SUCH,
        LOCKED,
        OUT_OF_RESOURCES,
        WAITING_FOR_EVENT
    }
    

    当我们需要增加错误码类型, 就会导致枚举类的修改, 所有使用了该枚举类的模块都需要重新编译. 但是如果使用异常的话, 直接派生出一个新的异常即可, 原有代码不会受到影响.

  10. 参考:
    [1] : 代码整洁之道