【整洁之道】如何写出更整洁的代码(上)

时间:2023-01-05 00:01:55

 

如何写出更整洁的代码

 

 

  代码整洁之道不是银弹,不会立竿见影的带来收益。

  没有任何犀利的武功招式,只有一些我个人异常推崇的代码整洁之道的内功心法。它不会直接有效的提高你写代码的能力与速度,但是对于程序员的整个职业生涯必然会带来意想不到的好处。

  如果你还是一个在校学生,或者是刚工作没多久的“菜鸟”,那么很有必要接触一些这方面的知识的。很显然,它会帮助你更快的适应企业级开发的要求。

 【整洁之道】如何写出更整洁的代码(上)

1. 为什么需要代码更整洁?

  在考虑代码整洁的时候,我们需要明确的一个前提是,这里不讨论代码的对错。

  关于什么是整洁的代码,可能千人千面,但是关于为什么要写出整洁的代码是要达成共识的。

  如果今天需要出去约会,不管是男生女生一定会将自己梳妆打扮一番吧。如果是周末自己一个人宅在家里呢?可能很多人都是不修边幅的。这体现了人际交往中很重要的一点:当需要与别人接触时,会注重自己的仪容。不管咱们的颜值高低,怎么都还是得捯饬一番不是?对于代码而言,我们应该抱有一样的要求。如果只是自己写的好玩的“用后即丢“的代码,让它邋遢点其实也没什么影响。但是如果是在公司与其他人一起开发维护的代码库呢?别人会看我们写的代码,我们也需要看别人写的代码。这样咱们是不是应该也要把自己的代码好好打扮一下,毕竟代码就是咱么的面子啊!

 

  想必,对于每一个看过几本编程方面书籍的人,都看过这么两个说法:

  1) 代码是写给人看的。

  2) 开发的大部分时间都是在看代码。

 

  我们可能在看别人的代码的时候,会在心里情不自禁的飙几句WTF,以宣泄自己对别人难以理解的代码的烦躁之情。但与此同时,别人也可能在心里F着咱们的代码。有感于此,是不是觉得自己应该写出更整洁、更易读的代码?让别人对自己的代码”无F可说“。

 

  我们需要追求整洁的代码,但是代码需要整洁的什么程度呢?抱歉,好像这是一个现阶段还没有被量化的问题。在我看来,是需要找到一个平衡点。对于那些之后再也不会用到的代码,当然就不用花费大力气去追求极致的整洁。而对于那些日常中需要用到的代码,我想让它们再怎么整洁也不为过。

 

2. 怎么写出整洁的代码?

  以下将是来自于《代码整洁之道》的方法论。

 

2.1 命名

  需要被命名的有:变量、函数、参数、类、包等。

  1. 名副其实。选择体现本意的名称能让人更容易理解和修改代码。都知道类名用名词性的词,函数名用动词性的词。除了这个最基本的要求之外,名字应该能体现领域概念。使用表意更准确的词汇,而不要用模棱两可的词汇增加代码模糊度。
  2. 避免误导。应避免使用与本意相悖的词。
    1. 不要使用不同之处较小的名称。如 InformationToSet, InformationToSend ,这样会增加区分它们的时间成本。此外,在使用IDE的快捷补全功能时,可能会使用相似但错误的变量名。如果有两个类型为 String 的变量,一个为 XYZControlllerForEffcientHandlingOfStrings 另一个为 XYZControllerForEffectientStorageOfStrings ,在使用补全功能时,一不小心就可能使用错误的变量名而导致错误。
    2. 以同样的方式拼写同样的概念。也就是说,同一个概念如果前后使用不一样的名字,就会产生误导的反效果。
    3. 最应该避免的是使用小写字母l和大写字母O作为变量名,它们与数字1和0极难区分。
  3. 做有意义的区分。
    1. 不要使用数字系列的命名来区分。对于如下方法签名
      /*
      该方法签名需要注释的帮助来能理解参数 a1, a2 分别表示什么意思
      */
      public static void copyChars(char[] a1, char[] a2);

      /*
      方法签名“自说明”参数的含义
      */
      public static void copyChars(char[] source, char[] destination);

      说明一个好的名字,对整个方法可读性的提供。

    2. 只要体现有意义的区分,废话都是冗余的。对于 getActiveAccount();getActiveAccounts(); 这两个函数很难通过名字就明白它们之间的区别。
  4. 使用读得出来的名字。受制于“poor”的英文词汇,一般很少涉及这点。但毫无疑问,如果在与别人交流代码的时候,能够顺畅的读出每个变量、函数名,无疑会使得整个交流更加的高效集中。
  5. 使用可搜索的名字。使用单字母名称和数字常量的问题在于,当需要全局搜索某个名字的时候,会出现很多的重复。就好像,如果你在一个文件中全局搜索字母"e",一般会出现很多结果(某个单词中间包含的字母也会被搜索到),这样会模糊我们的搜索结果。
    1. 长名称胜于短名称。单字母名称用于短方法中的本地变量。
    2. 名称长短应该与其作用域大小相对应。
  6. 避免使用编码。不要把类型或作用域信息编码进名称里。
  7. 每个概念对应一个词。避免将多个单词用于同一个目的。给每个抽象概念选择一个词,并从一而终。例如,在DAO层中查询数据库时,很多时候会出现 get, query 等表示查询的词,对于这种情况项目组内应该要保持统一。
  8. 别用双关语。避免将同一个单词用于不同的目的。代码作者应该尽力写出易于理解的代码。
  9. 使用解决方案领域名称。使用在编程领域里大家都认同并接受的概念。比如,当使用访问者模式时,在名称中加上 Visitor 会给熟悉设计模式的更多的信息。
  10. 使用问题领域的名称。也就是在名字中使用业务领域的概念来命名。这些概念都是从你的项目正在解决的问题中提炼出来的。在你的日常开发交流中会使用到的概念。
  11. 添加有意义的语境,不要添加没有意义的语境。

  取名字最难的地方在于需要良好的描技巧和共有的文化背景。一般而言,很难一下就给所有的变量、函数取一个简单直接、表意准确的名字,所以更应该的是不断的重构改善它们。随着对业务知识理解的不断加深,我们会发现以前起的名字不那么贴切,这个时候应该果断的对其进行修改。

 

  大概就是像给自己家小孩起名字一样的态度对待每个出现在代码中的名字。

 

2.2 函数

 

  函数是业务逻辑的载体。大部分时候,我们都是在函数中进进出出、上上下下、左左右右。这里涉及到了函数之间的跳转、函数内的导航。对于函数的编写,同样有着一些应该遵守的最佳实践。

 

    1. 短小。函数的第一个规则就是要短小。短小才精悍,浓缩的都是精华。函数短小的好处:
      1. 对于大部分大脑不是特别发达的人,10个10行的函数可能比一个100行的函数理解起来更容易。如果给每个“小函数”能起一个具有说明性的名字,那么整个流程读下来会更清晰,而且可以增加代码自解释的作用。我曾经重构过一个接近1000行的函数,那绝对是噩梦。
      2. 短小的函数便于在电脑屏中完整的显示出来,不需要上下去滚动屏幕。
    2. 只做一件事。函数应该做好一件事,做好这件事,只做这件事。这个原则(单一职责原则)表述起来很简单,但实际执行过程中却困难重重。难点就在于怎么去区分“事”。你觉得“吃饭”是一件事吗?是的,它可以只是一件事。但是如果你要在更低的层次去细分,你还可以将“吃饭”拆分成“拿碗 -> 打饭 -> 吃饭 -> 洗碗”这些过程。当然,这个例子比较牵强,但还是可以说明问题的。按不同的粒度可以划分出不同的结果。那到底应该将一件事“细分到多细”呢?需要找到一个平衡。当我们将一件事划分的越细、粒度越小,那么它的灵活性就越高,复杂度也越高。反之,则灵活性降低、易用性提高。比如,“棉花”就有较高的灵活性,我们可以用它来制作“棉被、被套、布匹”等,相应的它的复杂度也较高,大部分非专业技能人士应该都不可能将一堆“棉花”加工成一块“布匹”。与“棉花”相比,“布匹”的层次高一些,高层次也导致了它灵活性的降低,“棉花”可以加工成“棉被”,而“布匹”则不应加工成“棉被”了,也就说“布匹”的可能性相比“棉花”更少。此外,从“布匹”加工成一件“衣服”的复杂度比起从“棉花”到一件“衣服”的复杂度有了极大的降低。
    3. 每个函数一个抽象层级。让代码拥有自顶向下的阅读顺序,让每个函数后面跟着位于下一抽象层级的函数。也就是说,将类中的函数按照抽象层次的顺序放置在文件中,这样的好处在于可以顺畅的从上到下的阅读整个类。函数的顺序应该像小说一样被精心安排。
      1. 函数中混杂不同抽象层级,往往让人迷惑。
      2. 如果和一群你的“领导”(抽象层级高)的人一起吃饭,你会“不自在”。
      3. 如果和一群你的“下属”(抽象层级低)的人一起吃饭,你会咋样呢?哪个领导来表达一下此种情况的心情::-)
    4. switch语句。有需要的时候可以利用多态来优化 switch 语句。
    5. 使用描述性的名称。与之前在命名中介绍的一样,我们需要使用具有描述性的名字。不要害怕长名称。
      1. 使用描述性的名称能帮助理清模块的设计思路,并帮助改进它。
      2. 命名的方式要保持一致。
      3. 函数越短小、功能越集中,就越便于取名字。在函数名字中存在  and  时一般体现来了该函数的职责不单一。
    6. 函数参数。
      1. 函数参数越少越好。
      2. 从测试角度来看,参数越多,就越难写出能确保各种参数组合正常运行的测试用例。
      3. 输出参数比输入参数难以理解。习惯认为信息通过输入参数传入函数,通过返回值从函数中输出。不太希望信息通过输入参数传出。输出参数,就是将一个对象通过参数形式传入一个函数,然后在函数中对该对象的值进行处理,在该函数外部可以通过该对象的引用使用函数处理后的值,达到函数输出的效果。
      4. 标识参数丑陋不堪。不要向函数中传入布尔值。
    7. 无副作用。
    8. 分割指令与询问。函数要么做什么事,要么回答什么事。
      1. 函数应该修改某对象的状态(指令),或是返回该对象有关的信息(询问),两者都干常会导致混乱。
    9. 使用异常替代返回错误码。使用Java的异常系统来处理错误,而不是通过自定义的错误码来处理各种异常的情况。
      1.   try / catch 代码块搞乱了代码结构,把错误流程和正常流程混为一谈。最好把  try  和  catch  代码块的主体部分抽离出来,形成函数。
      2. 使用错误码容易形成依赖磁铁 dependency magnet,依赖磁铁类会被很多类导入和使用。当这个类修改时,所有其他依赖它的类都需要重新编译和部署。
    10. 别重复自己DRY。
      1. 重复是软件中一切邪恶的根源。许多原则与实践都是为了控制与消除重复。
      2. 存在不同层次的消除重复。可以将一段语句块提取为一个函数来消除重复,可以将一些函数提取到父类来消除重复,可以将一个模块提取为公共服务来消除重复,就像开源届流行的说法:不要重复制造车轮。

 

  借助于高级开发工具的帮助,我们可以方便的在函数、类之间跳转,可以方便在函数中添加任意的参数,但是如果抛开工具的辅助,那么曾经没有在意的“细节”就会形成难以清除的“污垢”,让人难受。当然,我们完全没有必要抛弃高级的开发工具,但是如果能在日常工作中就注意并保持代码中的每一个细微之处的整洁,难道不是一件很有“工匠精神”的事情吗?

  大师级程序员把系统当作故事来讲,而不是当作程序来写。

 

2.3 注释

 

  注释就是函数、类的一种辅助说明,就是怕别人不懂自己写的代码的意图,就加上一段说明性的文字。我经历过两个极端:一个是完全不写注释,另一个是每个函数、类都需要写上注释。

  我个人更认同的观点是:如果我们擅长于用语言来表达意图,那么就不需要注释。

  关于注释特别要注意的一点是:当代码在变动的时候,注释并不总是跟着变动的。这样会导致注释常常会与它所描述的代码不同步,并越来越不准确,甚至可能会起误导的作用。

 

  1. 注释不能美化糟糕的代码。少量而准确是注释比大量而毫无意义的注释更有用。大量无用的注释会打乱阅读代码时的思路,也会增加滚动屏幕的成本。
  2. 用代码来阐述。尽量使用代码来阐述它的意图。这需要给函数、参数起恰当的名字,函数层次清晰、逻辑明确。
  3. 好注释。
    1. 法律信息。根据公司要求而定。
    2. 提供信息的注释。
    3. 对意图的解释。注释可以提供某个决定背后的故事。
    4. 阐释。注释可以解释某些难懂的参数或返回值的意义。
    5. 警示。
    6. TODO注释。TODO大多都是程序员的自我安慰、自欺欺人。
    7. 放大。可以用来放大某种不合理之物的重要性。
    8. 公共API中的Javadoc
  4. 坏注释。大多数注释都属于此列。
    1. 喃喃自语。如果决定写注释,那就花点时间确保写出最好的注释。
    2. 多余的注释。典型的就是对Bean类中的每个参数都写上注释,比如 name, length 等可以通过名字就判断意义的字段。
    3. 误导性的注释。这个最可怕。
    4. 循规蹈矩式的注释。很多IDE都会自动生成函数的注释,其中可能会包含 @param, @return 等信息的说明,大多数时候可能就是放了一个IDE生成的模板在那里,并没有任何实质性的内容。
    5. 日志式注释。在每次修改时,在模块开始处添加一条注释,注明此次的修改变动。这个工作应该是Git等版本控制系统该做的事。
    6. 废话注释。毫无意义的注释。
    7. 能用函数或变量时就别用注释。体现了要使名称更具表达性,能表达它自身的意图。
    8. 位置标记。#######################################之类的。
    9. 括号后面的注释。通过注释来表明这个括号与哪个括号是一对的。
    10. 归属与署名。同样应该是代码控制系统应该做的事。
    11. 注释掉的代码。
    12. 非本地信息。
    13. 信息过多。
    14. 不明确的联系。

 

  注释应该起着辅助的作用,而不应该“喧宾夺主”。好的注释在于提供有用信息、或者方便程序员获取有用信息。而坏的信息在于冗余,甚至是错误,一般它们对提高对系统的认识不会提供任何帮助,反而会分散注意力。

 

2.4 格式

 

  好的团队,应该有一份属于团队的编码规范。这里面需要定义代码的格式问题,目的是为了保证团队的人输出的代码就像是一个人写的。这样的好处,1)在于减少团队间相互适应不同格式的成本,2)在于提高团队对外输出的影响力。

  代码的格式关乎沟通。下面是一些格式的最佳实践:

  1. 垂直格式。
    1. 向报纸学习。源文件的顶部应该给出高层次概念和算法,细节应该往下逐次展开。
    2. 概念间垂直方向上的区隔。每个空白行都应该是一条线索,标识出新的独立概念。
    3. 垂直方向上的靠近。相互靠近的代码应该是紧密相关的代码。
    4. 垂直距离。
      1. 变量声明。应该尽可能的靠近其使用的位置。
      2. 实体变量。应该在类的顶部声明。
      3. 相关函数。调用者应该尽可能的放在被调用者的上面。
      4. 概念相关。概念间的相关性越强,彼此之间的距离就应该越短。
  2. 水平格式。
    1. 水平方向上的区隔与靠近。
      1. 在操作符周围加上空格字符,可以达到强调的目的。
      2. 不在函数名和左圆括号之间家空格。
      3. 函数参数之间用空格隔开。
    2. 缩进。缩进有助于理清代码的层次结构。  
        // 风格1
        if (condition) {
statement;
}

// 风格2
if (condition)
{
statement;
}

  关于下面的两个 if 语句块,你是哪种风格?我是风格1,曾经被人当面说风格1怎么怎么不好,应该使用风格2,什么什么的!气氛一度十分尴尬。
  其实,这也没什么大惊小怪的,每个程序员都会有自己喜欢的风格。但是,在团队中,那就应该形成一致的团队风格。

3. 总结

  

  上面介绍了关于命名、函数、注释、格式相关的一些概念性知识点,代码整洁之道不是银弹,它只是帮助我们做好代码层面的小事。如果能够坚持下去,那今天的付出可能就是一只蝴蝶开始扇动了翅膀,可能在未来的某一天带来意想不到的巨大收货。抛开这些功利主义的想法,纯粹的想要成为一个有着“工匠精神”的程序员也是一件很牛逼的事吧!