编写可读艺术的代码

前言

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

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

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

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

一、 规范

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

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

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

按照团队的命名风格来

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 hardwareDetectedBiometric() async {} 

int pages = 0;
String userName = "";

2.4 带上更多细节

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

如何带上更多的细节。

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

比如:

String currentFlowName;

VoidCallback onFinishLoad,

List 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 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 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 为佳。再一个评判方法是,查看函数的内容无需滚动鼠标进行翻页。
少些代码:每写一行都需要维护;不需要的功能,砍掉,不需要的代码,删掉

你可能感兴趣的:(编写可读艺术的代码)