代码整洁之道

整洁代码

细节之于架构,维护之于重构,可维护代码之于可执行代码。
无论时架构还是代码都不强求完美,只求竭诚尽力。

“代码感”,“整洁感”

稍后等于永不 Later Equals Never

除非有一个最小单位模块可能会进行重构,更多的不起眼的细节处要力求在第一次做到最好,因为没有时间留给你再发现和修改,举个例子,那就是某些特殊的页面元素的CSS样式:圆角、阴影等,在你看来差一点无所谓,这些是产品经理和UI设计师关心的细节。所谓的最小单位模块,比如一个轮播图、一个对话框。

比如硬编码、可以做成公共库的代码却在页面重复地添加。

整洁代码的手段

书中作者讲道并不从一开始就按照规则写函数(代码)

  • 消除重复
  • 关注点分离(只做一件事)
  • 统一抽象层次
  • 提高表达力(有意义的命名)
  • 小规模抽象:从直接的实现手段不断过渡为抽象封装
  • 小块代码:类、对象、方法功能不要太多,这是从代码组织上做的切分,与关注点分离类似
  • 测试驱动

命名

  1. 虽然命名变长,但描述性的名称对于理解其含义、编辑器的搜索都大有裨益。从命名就能大抵看出其要做什么,不用过多地去关注其它,能大大提升修改代码、维护代码的效率,降低出错率。
  2. 同一模块下使用类似的措辞:getBooks , updateBooks , deleteBooks , addBooks

每个概念对应一个词,没有本质上的区别,get是美式英语,fetch是英式英语,只要统一一种即可。
getUser , getItems , getShops

  1. 使用解决方案领域名称和使用源自所涉问题领域的名称

前者即计算机科学所涉领域,如component、service等,后者为具体业务领域,如我们的商品、货源、市场等 术语。

函数

当作讲故事

核心

短小、只做一件事、每个函数一个抽象层级

函数参数

原则:尽量不超过两个参数。
参数过多的坏处:声明处和调用处,参数的数量、顺序难以保证对应,对于弱类型的JavaScript,类型对应不正确,导致函数执行时出错。

输出参数

对输出参数的转/改变发生隐蔽,容易忽略。而使用输入参数用函数返回值体系,就很明朗。
尽量使用输入参数,避免输出参数。

如何减少/理清参数

  1. 某个参数是一个模块或者可以作为一个模块,则可以把它从作为函数的调用者,就像给对象的prototype进行扩展。

    1. function reviseJourney(journey, item, time) {
    2. journey[item].time = time;
    3. }
    4. // 修改后
    5. // 函数
    6. function reviseJourney(item, time) {
    7. this[item].time = time;
    8. }
    9. reviseJourney.call(journey, '成都', '2019-10-01');
    10. // 对象原型
    11. Journey.prototype.revise = function () {
    12. this[item].time = time;
    13. }
    14. journey.revise('成都', '2019-10-01');
  2. 向下过程中多次出现的参数,作为当前类的成员变量

    1. class testPromise {
    2. resolve;
    3. reject
    4. authorize() {
    5. return new Promise((resolve, reject) => {
    6. this.resolve = resolve;
    7. this.reject = reject;
    8. someAsync() {
    9. this.$axios(url).then(res => {
    10. invoke();
    11. });
    12. }
    13. });
    14. invoke() {
    15. // ...
    16. this.resolve();
    17. }
    18. }
    19. }
  3. 两个参数的场景

    1. // 单个值的有序组成部分,如对象的key, value, 笛卡尔坐标Point p = new Point(0,0)
    2. updateFilterResults(key, value) {
    3. if (value === '' && typeof this.filterResults[key] === 'undefined') {
    4. return false;
    5. }
    6. this.filterResults[key] = value;
    7. },
    8. // 有容易理解的自然顺序或层级关系: 筛选器的第几个picker,该picker的选项
    9. changeFilterStatus(filterIdx, pickedIdx) {
    10. let filterItem = this.filterPickerGroups[filterIdx];
    11. filterItem.selectIndex = pickedIdx;
    12. this.$set(this._filterPickerGroups, filterIdx, filterItem);
    13. },
  4. 把一些参数封装为类,例如(x, y)封装为Center类

    1. // http请求中,unionId作为一个参数,公司信息封装为一个对象
    2. saveCompany(unionId, companyDetail) {
    3. let url = '/market/user/company/save';
    4. let params = {
    5. unionId,
    6. ...companyDetail
    7. }
    8. return this.httpReq.post(url, params);
    9. }
  5. 函数名和参数的强烈结合,可帮助减小参数名的长度,帮助记忆参数(顺序)。如下,使用第一种即可。

    1. function changePassword(pre, cur) {}
    2. function changePassword(prePsw, curPsw) {}

论文

把相近的变量隶属到一个概念,一组变量摸不清语境,则把这组变量作为一个类的成员字段。

关注点分离(只做一件事)

个人理解,这里的一件事,是指真正地用一段代码去描述怎么完成这件事。
而调用下一抽象层级的函数、一个完整的语句(条件、循环),不算做一件事,只算作一个步骤。

  1. 如果函数只是执行了该函数名下同一 抽象层 上的步骤,则函数只做了一件事。
  2. 不能再拆出另一个函数。
  3. 能用一小段话描述出一个连贯的动作。
  4. 如果函数主要是由多个区段组成,把它们拆出为下一抽象层的函数,连续调用。

例如以下cancelPick函数,changeFilterStatus改变筛选状态,resetRelatePicker重置与该筛选项的其它筛选项, updateFilterResults更新筛选结果,看上去做了“几件事”,但都是向下的调用。
前提:无副作用, 兼顾时序性耦合 (代码整洁之道40页)

  1. cancelPick(filterIdx) {
  2. let filterItem = this.filterPickerGroups[Number(filterIdx)];
  3. this.changeFilterStatus(filterIdx, -1);
  4. this.resetRelatePicker(filterIdx);
  5. this.updateFilterResults(filterItem.name, '');
  6. },

抽象层级

抽象层级的理解

抽象层,大多还是靠“代码感”确定的。如若真要描述,可以把抽象层级理解为一个职位。

在其位,谋其政, 任其职,尽其责;不在其位,不谋其政

  • 在其位:抽象层级
  • 谋其政,任其职:这一件事(当前函数所要做的事)
  • 尽其责:这件事的实现(代码段)
  • 不在其位,不谋其政:不在职位位置上,就不去考虑那个职位上的事。可以安排下属去做(调用下一抽象层级的函数),如果需要,听取汇报结果(函数返回结果)。

这也正符合 向下规则 (自顶向下阅读代码)。

高中低抽象层级

  • 高:例如前端与视图层有关的操作。
  • 中:除了高、低之外的。
  • 低:编程语言层的操作,如对字符串的处理,对象数组的转换。

小结

  1. 多个参数封装为类
    2. 标记参数,传入和不传入分成两个方法
    3. 不用一股脑加set方法,业务需要的时候写方法