《代码精进之路,从码工到工匠》读书笔记 一

418 阅读7分钟

前言

今年出了一本代码方面的集大成之作,代码精进之路,从码工到工匠。可以看出是作者的用心之作,把很多软件设计名著的核心内容做了总结和提炼,阅读本书可以大大节省时间。我也拜读了大作,将前三章读书笔记分享一下,加上自己的心得,希望读者不吝赐教。以后如果有时间,会陆续放出后面章节的内容。

命名

  1. 如果你想不出一个好名字,通常意味着代码的坏味道,或者坏的设计。
  2. 变量名中最主要的成分要放在前面,如: RevenueTotal 比 totalRevenue好。
  3. 好的代码是最好的注释。
    • 加入中间变量可以起到注释代码的作用。
    put(macher.group1, matcher.group2);
    
    可以写成:
    key = matcher.group1;
    value = matcher.group2;
    put(key, value)
    
    • 尽量不要注释:

      • 如果注释是复述功能,要删掉。
      • 如果注释是解释意图,可以保留,但最好是写一个函数,用函数名来解释。
  4. 可以用命名工具来辅助命名。 unbug.github.io/codelf/

函数

  1. 职责单一(Single Responsibility Principle, SRP)
  2. 优化
    • 如果有多处地方出现重复代码,应该用一个小框架(类,函数, 宏)把重复代码包裹起来。
    • 当实现一个功能时,用一系列组合/私有/静态函数将实现的细节隐藏起来,函数名同时也是注释。
    • 抽象层次的一致性(Single Level of Abstraction Principle , SLAP)。实现上述一系组合函数时,这些函数应该是同一层次的抽象。这个层次概念,是程序对这个客观世界的反映。很难用原理说明,可以用举例来理解。比如说茶壶和茶杯是同一层次的,但是茶水可以考虑为下一个层次的。但是分层不是绝对的。所谓应用之妙,存乎一心。

模块的设计原则 (SOLID)

  1. 单一职责原则(Single Responsibility Principle, SRP)

这个原则讲得很多,以至于看到有些麻木,其实这是一个基本而重要的原则。它的核心就是设计一个模块或者函数时,应该能使得函数或者模块的名字可以概况函数的功能,而不要产生歧义,更不能产生误解。比如说,一个名为name_check的函数,我们期待值是真或者假。但是这个函数其实还实现了名字的创建或者修改功能。但是如果叫name_check_update也不好,这违反了单一职责的原则。正确的做法是写两个函数,name_check和name_update。这样的好处是在以后代码修改中,如果希望name_update扩充一些功能,name_check可以保持不变。代码的BGU就是在这些细微之处避免的。

  1. 开闭原则(Open Close Principle, OCP): 对扩展开放,对修改关闭。

在软件演化的过程中,我们常常发现,如果加一些功能是比较安全的,但是删除一些功能或代码,则可能引起很多不可预期的结果。 这可能是这个原则的理由之一。我认为这是一个理想的状态,不修改是不可能的,但是一个好的架构,少修改而实现平滑的演化是我们的目标。

  1. 里氏替换原则(Liskov Subsitutioin Principle, LSP): 父类型可以被子类型正确替换。

这主要是用来检验继承设计的合理程度。现在提倡用组合代替继承,似乎这个原则用得不多了。

  1. 接口隔离原则(Interface Segregation Princile, ISP): 多个特定的接口好过一个通用的接口。

这个原则的好处是依赖关系和语义表达更加明确(也有注释接口的作用),有助于程序的演化和维护。坏处是需要更多的接口。实际上C语言中,我们常常写一个静态函数来实现一个通用的功能,再用若干全局函数来实现一些具体的接口。伪代码如下:

static int generic_function(int arg1, int arg2, int gar3, void *arg4);
int speicial_function1_for_arg1(int arg2, int arg3, void *arg4)
{
    return generic_function(1, arg2, arg3, arg4);
}
int special_function2_for_arg34(int arg1, int arg2)
{
    return generic_function(arg1, arg2, 100, “words");
}
  1. 依赖倒置原则(Dependancy Inversion Priciple, DIP): 模块之间的交互应该依赖抽象,而非实现。

这个原则的核心就是模块接口应该抽象化,分为概念层和实现层。用概念层和其他模块进行交互,实现层来实现具体业务。实际做法是定义一组接口函数。这些函数就是模块接口的抽象。函数的实现可以很方便改动,但是函数的名字,参数,返回值修改的代价很大,其他相关的模块可能都要改变。因此定义的时候要很小心。所谓架构师的经验就体现于此。模块是对现实世界某个实体的抽象,接口函数则反映模块设计者对这个现实实体的理解。理解越深,接口设计得就越准确,越能适应未来的变化。这也需要设计者对模块针对的领域(Domain)或业务(Business)的深入。领域驱动设计(DDD)的本质就在这里。

  1. 避免重复代码原则(Don't Repeart Yourself, DRY):

主要的好处是体现的代码修改上,尽量只修改一处地方。避免所谓的散弹式修改(Shotgun Surgery)。记得以前为了省时省力,有时侯会Copy& Paste 一些 代码。后来我发现如果能用一个函数将这些代码概括起来,以后在每次需要这段代码的地方,调用这个函数,会在以后系统维护的时候节省很多时间和精力。如果你在一个公司待的时间足够长,这个原则给你带来的收益会很可观。

  1. 刚刚好原则 (You Ain't Gonna Need it,YAGNI): 不要过渡设计。

这个原则的中文名字是我加的。它的英文名缩写是YANGI,你不需要它。这其实是模块演化的一个重要指导原则。在模块设计的最初版本里,需求是确定的,特定的代码就可以了,根据经验抽象出的一些公共功能无法保证以后用到,反而程序冗余,不好维护。当程序的功能增多时,公共功能就自然可以归纳出来。这样的抽象是坚实的。这个原则带来的副作用是进行归纳抽象的时候,需要将原来的代脉重构,增加了出BUG的几率。在工作中,我们可以在新的功能用这个抽象,老的代码保持不变。以后再加上新的代码,就都用这个抽象的公共模块了。老的代码在某个安全的时刻,比如说,业务不忙的时候或者整个系统需要大改的时候,再进行修改,这样可以做到尽量平滑过渡。

本节还提到了DRY和YAGNI是矛盾的,前者要抽象,后者似乎不要抽象,为此还提到了三次原则(Rule of Three):

1 第一次用到某个功能时,写一个特定的解决方法。

2 第二次又用到的时候,复制上一次的代码。

3 第三次出现的时候,才着手抽象化,写出通用的解决方案。

这里有个问题:为什么是三次,而不是两次?从我的经验看来,二次比较好。三次的话,前两次的重构代价会比较大。

  1. KISS (Keep it Simple and Stupid):尽可能简单。

应该说,这是我们设计的一个最重要的原则了。所谓大道至简,就是这个意思。它是所有设计的基石和出发点,也是我们设计和代码可以持续工作的保证。在 Uinx的系统设计里面,还有着为了保持设计的简单性,牺牲一些正确性和性能的例子。

  1. 最小惊奇原则(Pinciple of Least Astomishment, PLA)

这个原则是尽量把程序写得比较平庸,不要别出新裁。因为你可能过了一年之后会忘掉你的技巧。当程序演化的时候,这些别出新裁的代码往往是BUG的温床。记住一句话,你的代码要使自己不要犯错,也要使别人不会犯错