代码简洁之道

Posted 枫子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了代码简洁之道相关的知识,希望对你有一定的参考价值。

内容概要

本书后几章主要讲了java相关的类、系统、和并发的设计介绍,较粗略,与简洁之道不是特别融合,故而省略,想要详细了解的建议去看更优质的详细讲解。
本书主要站在代码的可读性上讨论。可读性? 顾名思义,代码读起来简洁易懂, 让人心情愉悦,大加赞赏。
在N年以后,自己或者他人仍然能够稍加阅读就能明白其中的意思。什么是整洁代码?看看程序员鼻祖们怎么说的,

  1. 整洁代码只做好一件事。—— Bjarne Stroustrup, C++语言发明者

  2. 整洁代码从不隐藏设计者的意图。—— Grady Booch, 《面向对象分析与设计》作者

不能想着,这代码我能看懂就好了, 即使当下能看懂,那几个月甚至一年以后呢。更不能说为了体现自己编程“高大上”,故意用一些鲜为人知的语法,如

1
2
const LIMIT = 10
const LIMIT = Number(++[[]][+[]]+[+[]])
  1. 尽可能少的依赖关系,模块提供尽量少的API。—— Dave Thoms, OTI创始人, Eclipse战略教父
  2. 代码应该通过其字面表达含义,命名和内容保持一致。 —— Michael Feathers, 《修改代码的艺术》作者
  3. 减少重复代码,甚至没有重复代码。—— Ron Jeffries, 《C#极限编程探险》作者
  4. 让编程语言像是专门为解决那个问题而存在。 —— Ward Counningham, Wiki发明者

​​

有意义的命名

  • 名副其实

好的变量、函数或类的名称应该已经答复了所有的大问题,如果需要注释补充,就不算名副其实。
工具函数内部的临时变量可以稍微能接收。

1
2
3
4
5
6
7
// what\'s the meaning of the \'d\'?
int d;
// what\'s list ?
List<int []> list;
 
int daysSinceCreation
int daysSinceModification

  

此处的重定向,起名为“redirection”会不会更好一点,

1
2
3
4
5
6
7
8
/**
 * 重定向
 */
public function forward(Request $request, $controller, $action) {}
/**
 * 重定向
 */
public function default(Request $request,  $controller, $action) {}

  

既是注册帐号,为何不直接命名为 register 呢?也许会说,注册就是新增帐号,create也是新增帐号,自然,create可以代表注册。可新增帐号可能是自己注册,也可能是系统分配,还可能是管理员新增帐号,业务场景不一样,实现也很可能不一样。所以,建议取一个明确的,有意义的,一语道破函数干了啥的名称。

1
2
//注册账号
public function create($data) {}

  

  • 避免误导

程序员必须避免留下掩藏代码本意的错误线索。变量命名包含数据类型单词(array/list/num/number/str/string/obj/Object)时,需保证该变量一定是该类型,包括变量函数中可能的改变。更致命的误导是命名和内容意义不同,甚至完全相反。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 确定是 List?
accountList = 0
// 确定是 Number?
nodeNum = \'1\'
 
//确定所有情况返回值都是list吗?
function getUserList (status) {
    if (!status) return false
    let userList = []
    ...
    return userList
}
.align-left {
  text-align: "right";
}

  

  • 做有意义的区分

product/productIno/productData 如何区分?哪个代表哪个意思? Info 和 Data就像 a / an / the 一样,是意义含糊的废话。如下函数命名也是没有意义的区分,

1
2
3
getActiveAccount()
getActiveAccounts()
getActiveAccountInfo()

  

  • 使用读的出来的名称

读不出来就不方便记忆,不方便交流。大脑中有很大一块地方用来处理语言,不利用起来有点浪费了。

  • 使用可搜索的名称

让IDE帮助自己更便捷的开发。假如在公共方法里面起个变量名叫value,全局搜索,然后一脸懵逼地盯着这上百条搜索结果。 (value vs districts)

  • 每个概念对应一个词

媒体资源叫media resources 还是 publisher?

  • 添加有意义的语境

firstName/lastName/street/city/state/hourseNumber
=>
addrFirstName/addrLastName/addrStreet/addrCity/addrState/hourseNumber

注释

什么也比不上放置良好的注释来的有用。
什么也不会比乱七八糟的注释更有本事搞乱一个模块。
什么也不会比陈旧、提供错误信息的注释更有破坏性。
若编程语言足够有表达力,或者我们长于用这些语言来表达意图,就不那么需要注释——也根本不需要。

  • 作者为什么极力贬低注释?

注释会撒谎。由于程序员不能坚持维护注释,其存在的时间越久,离其所描述的代码越远,甚至最后可能全然错误。不准确的注释比没有注释坏的多,净瞎说,真实只在一处地方存在:代码。

  • 注释的恰当用法是弥补我们在用代码表达意图时遭遇的失败。
1
2
3
4
// 禁用、解冻
public function option(Request $request) {}
// 记录操作日志
protected function writeLog($optType,$optObjectName, $optId, $optAction) {}

  

=>

1
protected function recordOperationLog($optType,$optObjectName, $optId, $optAction) {}

  

将上面的 注释 + 代码 合成下方纯代码,看着更简洁,且不会读不懂。
再者,可以在函数定义的地方添加说明性注释,可不能在每个用到这个函数的地方也添加注释,这样,在阅读函数调用的环境时,还得翻到定义的地方瞅瞅是什么意思。但如果函数本身的名称就能描述其意义,就不存在这个问题了。
别担心名字太长,能准确描述函数本身的意义才是更重要的。

  • 注释不能美化糟糕的代码。
    对于烂透的代码,最好的方法不是写点儿注释,而是把它弄干净。与其花时间整一大堆注释,不如花时间整好代码,用代码来阐述。
1
2
3
4
5
6
// check the obj can be modified
if (obj.flag || obj.status === \'EFFECTIVE\' && user.info.menu === 1) {
    // todo
}
if (theObjCanBeModified()) {}
function theObjCanBeModified () {}

  

好注释

  1. 少许公司代码规范要求写的法律相关注释。

1
2
3
4
5
6
7
8
9
10
/**
 * Laravel IDE Helper Generator
 *
 * @author    Barry vd. Heuvel <barryvdh@gmail.com>
 * @copyright 2014 Barry vd. Heuvel / Fruitcake Studio (http://www.fruitcakestudio.nl)
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
 * @link      https://github.com/barryvdh/laravel-ide-helper
 */
 
namespace Barryvdh\\LaravelIdeHelper;

  

  2. 对意图的解释,如,

1
2
3
4
5
6
7
8
function testConcurrentAddWidgets() {
...
// this is our best attempt to get a race condition
// by creating large number of threads.
for (int i = 0; i < 25000; i++) {
 // to handle thread
}
}

  3. 阐释
  有时,对于某些不能更改的标准库,使用注释把某些晦涩难懂的参数或返回值的意义翻译为某种可读的形式,也会是有用的。

1
2
3
4
5
6
7
8
9
10
function compareTest () {
  // bb > ba
  assertTrue(bb.compareTo(ba) === 1)
  // bb = ba
  assertTrue(bb.compareTo(ba) === 0)
  // bb < ba
  assertTrue(bb.compareTo(ba) === -1)
}
// could not find susan in students.
students.indexOf(\'susan\') === -1

  

  4. 警示
  注释用于警告其他程序员某种后果,也是被支持的。

  函数,

1
2
// Don\'t run unless you have some time to kill
function _testWithReallyBigFile () {}

  文件顶部注释,

1
2
3
/**
 * 文件来内容源于E:\\Git_Workplace\\tui\\examples\\views\\components\\color\\tinyColor.js,需要新增/编辑/删除内容请更改源文件。
 */

  5. TODO

  来不及做的,使用TODO进行注释。虽然这个被允许存在,但不是无限书写TODO的理由,需要定期清理。

  6. 放大

  注释可以用来放大某些看着不合理代码的重要性。

  不就是个trim()么?

1
2
3
4
// the trim is real importan. It removes the starting
// spaces that could casuse the item to be recoginized
// as another list
String listItemContent = match.group(3).trim()

  

  没引入任何编译后的js和css,代码如何正常工作的呢?请看注释。

1
2
3
4
<body>
  <div id="app"></div>
  <!-- built files will be auto injected -->
</body>

  

  7. 公共API中的DOC
  公共文档的doc一般会用于自动生成API帮助文档,试想如果一个公共库没有API说明文档,得是一件多么痛苦的事儿,啃源码花费时间实在太长。

 

坏注释

  1. 喃喃自语
    写了一些除了自己别人都看不懂的文字。

  2. 多余的注释
    简单的函数,头部位置的注释全属多余,读注释的时间比读代码的时间还长,完全没有任何实质性的作用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Utility method that returns when this.closed is true.
    // Throws an exception if the timeout is reached.
    public synchronized void waitForClose(final long timeoutMillis)
    throw Exception {
    if (!closed)
    {
     wait(timeoutMillis);
     if (!closed)
       throw new Exception("MockResponseSender could not be closed");
    }
    }

      

  3. 误导性注释
    代码为东,注释为西。

  4. 多余的注释

  
1
2
3
4
5
6
7
8
// 创建
public function create(Request $request) {}
// 更新
public function update(Request $request) {}
// 查询
public function read(Request $request) {}
// 删除
public function delete(Request $request) {}

  

  $table已经初始化过了,@var string 这一行注释看上去似乎就没那么必要了。

1
2
3
4
5
/**
 * The table name for the model.
 * @var string
 */
protected $table = \'order_t_creative\';

  

  5. 括号后面的注释

  只要遵循函数只做一件事,尽可能地短小,就不需要如下代码所示的尾括号标记注释。

1
2
3
4
5
6
7
8
9
10
try {
  ...
  while () {
   ...
  // while
  ...
// try
catch () {
  ...
// catch

  

  一般不在括号后方添加注释,代码和注释不混在一行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function handleKeydown (e) {
  if (keyCode === 13) { // Enter
    e.preventDefault()
    if (this.focusIndex !== -1) {
      this.inputValue = this.options[this.focusIndex].value
    }
    this.hideMenu()
  }
  if (keyCode === 27) { // Esc
    e.preventDefault()
    this.hideMenu()
  }
  if (keyCode === 40) { // Down
    e.preventDefault()
    this.navigateOptions(\'next\')
  }
  if (keyCode === 38) { // Up
    e.preventDefault()
    this.navigateOptions(\'prev\')
  }
}

  

现作出如下调整,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function handleKeydown (e) {
  const Enter = 13
  const Esc = 27
  const Down = 40
  const Up = 38
  e.preventDefault()
  switch (keycode) {
    case Enter:
      if (this.focusIndex !== -1) {
        this.inputValue = this.options[this.focusIndex].value
      }
      this.hideMenu()
      break
    case Esc:
      this.hideMenu()
      break
    case Down:
      this.navigateOptions(\'next\')
      break
    case Up:
      this.navigateOptions(\'prev\')
      break
  }
}

  

  通过定义数字变量,不仅去掉了注释,各个数字也有了自己的意义,不再是魔法数字,根据代码环境,几乎不会有人问,“27是什么意思?” 诸如此类的问题。再者,if情况过多,用switch代替,看着稍显简洁。最后,每一个都有执行了e.preventDefault(),可以放在switch外层,进行一次书写。

  6. 归属和署名
  源码控制系统非常善于记住谁在何时干了什么,没有必要添加签名。新项目可以清除地知道该和谁讨论,但随着时间的推移,签名将越来越不准确。
当然,这个也见仁见智,支付宝小程序抄袭微信小程序事件的触发便是因为代码里面出现开发小哥的名字。如果为了版权需要,法律声明,我想写上作者也是没有什么大问题的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * Created by PhpStorm.
 * User: XXX
 * Date: 2017/9/29
 * Time: 14:14
 */
 
namespace App\\Services;
use Cache;
class CacheService implements CacheServiceInterface
{
}
/**
 * 功能: 广告位管理
 * User: xxx@tencent.com
 * Date: 17-8-2
 * Time: 下午4:47
 */
class PlacementController extends BaseController
{
}

  

  7. 注释掉的代码
  直接把代码注释掉是讨厌的做法。Don’t do that! 其他人不敢删除注释掉的代码,可能会这么想,代码依然在那儿,一定有其原因,或者这段代码很重要,不能删除。
其他人因为某些原因不敢删可以理解,但如果是自己写的注释代码,有啥不敢删呢?再重要的注释代码,删掉后,还有代码控制系统啊,这个系统会记住人为的每一次改动,还担心啥呢?放心地删吧!管它谁写的。

1
2
3
4
5
6
7
8
9
10
11
12
13
// $app->middleware([
//    App\\Http\\Middleware\\DemoMiddleware::class
// ]);
 
// $app->routeMiddleware([
//     \'auth\' => App\\Http\\Middleware\\Authenticate::class,
// ]);
 
if (APP_MODE == \'dev\') {
    $app->register(Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider::class);
}
$app->register(\\App\\Providers\\UserServiceProvider::class);
$app->register(\\App\\Providers\\UserRoleServiceProvider::class);

  8. 信息过多

  9. 别在注释中添加有趣的历史性话题或无关的细节描述。

  10. 注释和代码没有明显的联系

  11. 注释和代码之间的联系应该显而易见,如果注释本身还需要解释,就太糟糕了。

1
2
3
4
5
/**
* start with an array that is big enough to hold all the pixels
* (plus filter biytes), and extra 200 bytes for header info
*/
this.pngBytes = new byte[((this.width + 1) + this.height * 3) + 200];

  

  12. 非公共代码的doc类注释

  有些doc类的注释对公共API很有用,但如果代码不打算作公共用途,就没有必要了。

下面的四行注释,除了第一行,其它的都显得很多余,无疑在重复函数参数已经描述过的内容。倘若阅读代码的人花了时间看注释,结果啥也没有,沮丧;知道没用自动掠过,没有花时间看注释,那这注释还留着干啥。

1
2
3
4
5
6
7
/**
 * 根据媒体ID获取广告位ID
 * @param PlacementService $service
 * @param Request $request
 * @return Msg
 */
public function getPublisherPlacementIds(PlacementService $service, Request $request) {}

  

函数

  • 短小

函数第一规则是要短小,第二规则是还要更短小。if语句,else语句,while语句等,其中的代码块应该只有一行。函数代码行建议不要超过20行,每行代码长度建议150个字符左右。如下代码片段,建议换行。

1
2
3
export default function checkPriv (store, path) {
  return store.state.user.privileges && (store.state.user.privileges.includes(path) || store.state.user.privileges.includes(`/${path.split(\'/\')[1]}/*`) || isAll(store))
}

  

  • 函数应该只做一件事,做好这件事。

  如下函数,executeSqlContent() 很明显不止做一件事, 前半部分实现了连接配置的获取,后半部分根据config执行sql。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

(c)2006-2024 SYSTEM All Rights Reserved IT常识