美文网首页
编写可读艺术的代码

编写可读艺术的代码

作者: Yue_Q | 来源:发表于2020-06-28 15:21 被阅读0次

    前言

    编写代码,实质是在梳理逻辑,为了完善整个逻辑流程,我们借用编程语言的变量、函数、流程控制、循环、注释、方法等串接起来,完善一套系统的逻辑。

    为了完善这套逻辑,我们借助了许多工具:设计方法、架构设计、项目组织等。
    意识到没有,代码的好坏一定程度上可以从逻辑层面评判。

    • 符合逻辑,不一定是最优的代码
    • 不符合逻辑,一定不是好的代码

    逻辑的串接靠的是编程语言的变量、函数、流程控制、循环、注释等。

    一、 规范

    绝大多数的人,不会从零完整的完成一个复杂的项目,大多是团队共同合作,完成一个大的项目。

    这个时候,假如你是中途参与进来。你在实现逻辑的时候,你是照着自己的逻辑来还是依照团队的风格来。

    比如项目组织,命名等...

    按照团队的命名风格来

    1.1 编程语言的规范

    每门编程语言,都存在一定的规范,比如 Python 采用的下划线的变量命令规则,Go 则采用驼峰式的变量命令规则等。
    Effective Dart: 代码风格
    Kotlin 编码规范

    另外,Dart语言默认80个字符换行,在团队中需要设置100个字符换行。

    二、 命名

    给变量,函数,方法命名时易于理解

    • 专业的单词:使用领域内的单词
    • 避免空泛的名字
    • 具体的名字
    • 变量名带上更多细节
    • 不使用令人误解的名字
    • 布尔值命名

    2.1 领域内单词

    天星银行的领域单词分为几大类

    类型 不常见的单词
    account(账户) biometric(生物识别)、faceID(面容识别)、touchID(指纹识别)
    deposit(存款) timeDeposit(定期)、currentDeposit (活期)、
    kyc(开户) employment(职业) 、questionnaire(调查问卷)、liveness(活体检测)、document(证件)
    loan(贷款) anti frau(反欺诈)、partial repay(部分还款)、credit(信用)
    personal(个人) language(语言)、fileMaintain(资料维护)、promotion(促销)
    security(安全) softToken(安全令牌)、remain(提醒)
    transfer(转账) fps(转数快)、tupta(分享)、

    2.2 避免空泛的名字

    变量的命名一般要赋予一定的意义,极少情况下可以使用没有什么意义的单词。比如最常见的:

    var i int
    
    for (i=0;i<10;i++){
        fmt.Println(i)
    }
    

    这种没什么意义的单词,一般适用于局部作用域。

    2.3 具体的名字

    完成什么任务就使用什么单词。一般变量使用名词居多,函数使用动词开头居多。

    String toString() => json.encode(toJson());
     /// 设备硬件支持的生物识别类型 (指纹,面容)
    static Future<BiometricType> hardwareDetectedBiometric() async {} 
    
    int pages = 0;
    String userName = "";
    

    2.4 带上更多细节

    一般命名不建议过长,也不建议过短,最长三个单词的长度吧

    如何带上更多的细节。

    • 尝试使用后缀
    • 尝试使用单位
    • 尝试指向具体的细节

    比如:

    String currentFlowName;
    
    VoidCallback onFinishLoad,
    
    List<OtpType> otpTypes;
    
    final BiometricType openedBiometricType;
    

    几组对仗的后缀:

    • max/min
    • first/last
    • begin/end
      ...
    ServerCanStart() 不如 CanListenOnPort()
    

    2.5 不使用令人误解的词

    比如:Filter 在数据库操作中容易使用这个单词,这个单词没有带上更多的细节,实质上在使用的过程中,还是需要查看编写的SQL 语句等才能知道具体的过滤细节。整体思考多了几步。不易让人理解。

    建议多读几遍自己命名的单词

    2.6 布尔值

    提到布尔值,因为就存在两种结果。所有,一般使用是否这样意思的词。

    通常使用 is,has,can,should这样的词,可以把布尔值变得更明确。

    final bool canBack;
    final bool backOnFinish;
    
    // 是否有更多收支历史
    @JsonKey(name: 'hasMoreRecord', required: true)
    final bool hasMoreRecord;
    

    2.7 不建议使用的单词

    get、read、util 恰恰这几个单词,在写代码中最容易使用。选择替代方案。

    单词 更多选择
    send deliver(传送)、dispatch(派遣)、announce(宣布)、distribute(分配)、route(路线)
    find search(搜寻)、extract(提取)、locate(位于)、recover(恢复)
    make launch(发动)、create(创建)、begin(开始)、open(打开)
    start create、set up(建立)、build(建立)、generate(发送)、compose(构成)、add(添加)、new(新)

    2.8 带单位的值

    static const int resultTimeoutInSecs = 10;
    
    • 使用专用的单词 - 例如:不用 Get ,使用 Fetch、Download 。由上下文决定
    • 避免使用空泛的单词 - tmp 和 retval 。除非有特定的理由

    3 设计

    好的源代码应当“看上去养眼”。如何使用好的留白,对齐及顺序来让你的代码变得更易读。

    • 使用一致的布局,让读者很快习惯这种风格
    • 让相似的代码看上去相似
    • 把相关的代码分组,形成代码块

    这里以设计的四个规范类比代码的组织。

    3.1 对齐

    编程语言为什么强调缩进?难道不是为了阅读代码的人更容易看懂代码吗?写代码的人更容易组织代码吗?仅仅是设计者为了好玩?

    static Future<void> start(FlutterDriverWrapper driver) async {
        driver.log('register test begin...');
    
        /// home page
        await driver.waitFor(KeyManager.homeBtnLogin);
        await driver.takeScreenshot();
        await driver.tap(KeyManager.homeBtnLogin);
    
        /// login page
        await driver.waitFor(KeyManager.loginBtnRegister);
        await driver.takeScreenshot();
        await driver.tap(KeyManager.loginBtnRegister);
    
        /// validate new username page
        await driver.enterText(KeyManager.validateNewUsernameInputUsername, TestConfig.username);
        await driver.takeScreenshot();
        await driver.tap(KeyManager.validateNewUsernameBtnNext);
    」
    
    • 放眼望去,确实知道实现什么任务
    • 风格统一
    • 整整齐齐

    3.2 重复、亲密、比较、审美

    最应该避免的其实就是写重复的代码,一般的做法往往是提炼,将重复的抽象出一个函数之类的。这里的重复,是风格的统一。

    • 重复的代码使用方法去提炼
    • 选择一个有意义的顺序,始终一致地使用它
    • 把声明按照块组织起来
      static double get s1 => s(3);
    
       /// Loan pages
      static const String loanCreditPrepare = '/loanCreditPrepare';
      ...
    
      /// Coupon
      static const String couponList = '/couponList';
       ...
    
      /// Transfer pages
      static const String transferPrepare = '/transferPrepare';
       ...
    }
    

    可以结合比较下 student.go 和 teacher.go

    这样的组织方式,讲道理,并不太会给阅读代码的人带来太多的认知负担。

    3.3 留白

    设计领域页面的设计,并不强调内容越多越好,恰当的在页面上留有空白,使整体设计有呼吸感。

    那编程如何实现留白?

    • 恰当的换行,使相似的内容更紧凑
    • 提取,使用方法来组织不规范的东西
    • 代码分段

    假如你一个函数需要写 100 多行,不好意思,我可能建议你,不要这么做。

    • 拆分,逻辑梳理、提取方法
    • 尽量维持最长 30~50行左右(这样使屏幕能装载下,一次就能完成的阅读整个函数的逻辑)
    static Future<void> start(FlutterDriverWrapper driver) async {
        driver.log('kyc test begin...');
    
        /// home page
        await driver.tap(KeyManager.notifyBtnOk);
    
        /// prepare page
        await driver.tap(KeyManager.bottomBtnRight);
    
        /// kyc terms page
        await driver.delayed(TestConfig.pageStartDelay);
        await driver.scrollIntoView(KeyManager.bottomBtnRight);
        await driver.tap(KeyManager.bottomBtnRight);
        ...
    
    

    4 注释

    帮助阅读代码的人对代码了解的和写代码的人一样多

    4.1 什么时候不需要注释

    • 好的命名不需要注释

    4.2 什么时候需要注释

    • 关键点
    • 缺陷点
    • 常量
    • 全局注释
    • 总结性注释

    关键点:
    有些时候,仅仅靠之前的“表面工作” 已经不能完全能够满足让人易于理解。这个时候需要在关键点添加注释。

    缺陷点:
    是的,承认自己的代码写的不是最优的,仅仅只是实现,还存在更优的办法,所以需要在有缺点的地方加上注释。

      // TODO: move to remote config
      static const String customerServicePhoneNumber = '9288321';
    
      /// TODO: 更好的方案是:现有的图形上做动画,而不是重置动画。
      @override
      void didUpdateWidget(TotalAssetsWidget oldWidget) {
        super.didUpdateWidget(oldWidget);
        ...
      }
    

    常量:
    给常量注释,赋予了更多的意义。

      ///
      /// Area codes
      ///
      static const String areaCodeHK = '852';
    

    全局注释:
    一般在文件开头,表明文件内代码完成的任务。

    class TestConfig {
      TestConfig._();
      ///
      /// Environment variables are used to configure the tests.
      /// It's more convenient to modify the env than to change the code.
      ///
      /// They are just key value pairs like following:
      ///
      /// ------------------------------
      ///   username = "bob010"
      ///   password = "bob0100"
      ///
      ///   accountGroup = "login"
      ///   kycGroup = "login, kyc"
      /// ------------------------------
      ///
      /// Note: line 'accountGroup = "login"' means which tests should be run
      /// for [accountGroup] test group.
      ///
      static void loadEnv() {
        load();
        env.removeWhere((k, v) => Platform.environment.containsKey(k));
        env.forEach((k, v) => log('[ENV] $k = $v'));
      }
    

    5 流程控制

    5.1 条件参数的顺序

    涉及流程控制的话,一般涉及条件判断,条件判断语句中的参数的顺序。

    if (number < 10) {} // A
    
    if (10 > number) {} // B
    
    if (receivedNumber < expectedNumber) // C
    
    if (expectNumber > receivedNumber) // D
    

    通常我们会选择 A,C
    那么应该准从什么样的尊则?

    左边倾向于变量,右边倾向于常量;

    5.2 if...else 语句块的顺序

    可以参照下面的下面准则:

    • 先判断正向逻辑的,再判断负向逻辑
    • 先处理简单
    • 先处理有趣的或者可疑的
      void _listenScrollPosition() {
        if (widget.bottomButtonText == null) return;
        ...
      }
    

    5.2 避免使用三目运算符

    三目运算符一定程度上能够精简代码,减少代码的行数,但是却存在另外一个缺点,即:不容易理解(虽然大学教材总会考这类题目,判断执行的顺序和结果)

    只在简单场景下使用三目运算符。

    5.4 函数什么时候返回

    经常我们编写函数的时候,喜欢声明一个变量用来存储结果,到所有的逻辑结束后返回这个变量作为函数的返回值。

    • 可以提前进行函数返回值,多几个 return, 没关系
    • 最好函数都要有返回值,Golang 里建议至少返回一个 错误信息

    5.5 减少多层级的嵌套

    层级的增多,增加了认知的负担。而且容易出现不容易发现的 bug。

    如何减少嵌套:

    • 提前函数返回
    • 在循坏内使用 continue

    5.6 表达式

    建议使用短表达式

    if createParam.Data.ShopType == RegionEntrances {}
    // 感觉表达式长了,怎么做:
    
    var shopTypeEqual = createParam.Data.ShopType == RegionEntrances
    
    if shopTypeEqual {}
    

    6. 重新组织代码,持续迭代

    有下面几条准则:

    不相干的任务,提取出来
    一次只专注干一件事
    梳理逻辑时,如果你能使用自然语言表述出来,对你写出逻辑清晰的代码很有帮助
    单函数行数不宜过长 30 ~ 50 为佳。再一个评判方法是,查看函数的内容无需滚动鼠标进行翻页。
    少些代码:每写一行都需要维护;不需要的功能,砍掉,不需要的代码,删掉

    相关文章

      网友评论

          本文标题:编写可读艺术的代码

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