Learning 《Implementation Patterns》 —— 编写可读的代码

在软件开发中有大量的开销都被用在理解现有代码上了。

  • 范围的管理对于软件开发和写书都一样重要。

好的代码是有意义的。

软件要取得商业成功或者被广泛使用,“好的代码质量”即不必要也不充分。

尽管代码质量不能保证美好的未来,但它仍然有其意义:有了质量良好的代码以后,业务需求能够被充满信心地开发和交付,软件用户能够及时调整方向以便应对机遇和竞争,开发团队能够在挑战和挫折面前保持高昂的斗志。

总而言之,比起质量低劣、错误重重的代码,好的代码更有可能帮助用户取得业务上的成功。

第 1 章 引言

用代码来沟通有几个步骤:

  • 首先,必须在编程时保持清醒。迈向沟通的第一步就是让自己慢下来,弄明白自己究竟想了些什么,不再假装自己是在凭本能编程。
  • 第二步是要承认他人的重要性。必须学会相信其他人也跟我一样重要,然后才能写出能与他人沟通的代码。
  • 第三步,使用实现模式,更有意识地编程,为他人编程,而不仅仅是为自己编程。

命名的普遍约束总是一致的:需要把变量的用途、类型和生命周期告诉给阅读者,需要挑选一个容易读懂的名字,需要挑选一个容易写、符合标准格式的名字。把这些普遍约束加诸一个具体的变量之上,然后就得到了一个合用的名字。

“给变量命名”就是一个模式:尽管每次都可能创造出不同的名字,但决策的方法和约束条件总是重复出现的。

作者说本书的深度:

  • 《Design Patterns》《设计模式:可复用面向对象软件的基础》 每天做几次的决策,协调对象之间交互的决策
  • 《Implementation Patterns》《实现模式》 每过几秒钟就可能用上一个模式。
  • 《Java 语言手册》 能做什么,有什么功能等。

Thinking: 这其实也是学习任何一种语言的三个层次,第二和第三个层次分别是实现模式和设计模式,而这两个模式其实是各种语言能用的,换一种语言,只需要切换一下第一个层次,即底层的那些语法和第三方库等。

我对待并发问题的策略一向很简单:尽可能地把涉及并发的部分从我的程序中隔离出去。

关于并发处理的实践指导,我推荐 《Java Concurrency in Practice》《Java 并发编程实践》之类的书籍。

本书完全没有涉及的另一个主题是软件过程。

第 2 章 模式

绝大多数程序都遵循一组简单的法则:

  • 更多的时候,程序是在被阅读,而不是被编写。
  • 没有“完工”一说。修改程序的投入会远大于最初编写程序的投入。
  • 程序都由一组基本的语句和控制流概念组合而成。
  • 程序的阅读者需要理解程序。
    • 既从细节上,也从概念上。
    • 有时他们从细节开始,逐渐理解概念;
    • 有时他们从概念开始,逐渐理解细节。

在思考具体代码的写法时,大部分领域问题都暂时放在一边,专注在纯技术问题上:

这个实现应该:

  • 容易读懂
  • 容易编写
  • 容易验证
  • 容易修改
  • 高效

这一系列约束(压力 force)就是《实现模式》的起源,这其实是关于压力的模式。

每个模式都代表着一种对压力进行相对优先级排序的观点。

每个模式都带着一个解决方案的种子。模式在抽象的原则和具体的实践之间架起了一座桥梁。

模式彼此协作。 多个模式可以组合使用。

当模式成为习惯之后,便成为自然,节省脑力。

没有任何一组模式能够适用于所有情况。盲目效仿别人的风格,永远都不如思考和实践自己的风格并在团队中讨论交流来得有效。

模式最大的作用就是帮助人们做决定。有些实现模式会融入编程语言,不过大部分时候,模式需要加以调整才能投入使用。

使用模式可以帮助程序员用更合理的方式来解决常见问题,从而把更多的时间、精力和创造力留下来解决真正独一无二的问题。

第 3 章 一种编程理论

实际编程中存在一些更加深广的影响力,远不是孤立的模式所能概括的。

这些贯穿于编程中的横切概念,分为两类:价值观与原则

价值观

  • 沟通:珍视与其他人沟通的重要性

Knuth 所提出的文学编程理论促使我把注意力放到沟通上来:程序应该读起来像一本书一样。它需要有情节和韵律,句子间应该有优雅的小小跌宕起伏。

软件的绝大部分成本都是在第一次部署以后才产生的。花在阅读已有代码上的时间比写新代码的时间长得多。

作为社会性的产物,明确地考虑社会因素要比在假设它们不存在的情况下工作更为现实。

  • 简单:把代码中多余的复杂性去掉

去掉多余的复杂性可以让那些阅读、使用和修改代码的人更容易理解。

简单存在于旁观者的眼中。

沟通和简单通常都是不可分割的。多余的复杂性越少,系统就越容易理解;在沟通方面投入越多,就越容易发现应该被抛弃的复杂性。

  • 灵活:保持开放的心态

灵活是衡量那些低效编码与设计实践的一把标尺。

想象中可能会用得上的灵活性,可能与真正修改代码时所需要的灵活性不是一回事。这就是简单性和大规模测试所带来的灵活性比专门设计出来的灵活性更为有效的原因。

要选择那些提倡灵活性并能够带来及时收益的模式。

灵活性的提高可能以复杂性的提高为代价。

反过来简单也可以促进灵活。增进软件的沟通效果同样会提高灵活性。

原则

原则在价值观和模式之间搭建了桥梁。

原则可以帮助解决没有现成的模式或多个矛盾模式的问题,可以“无中生有”创造出新的实践,同时也和其他的实践保持一致。

价值观 —— 原则 —— 模式 为什么做 —— 怎么做 —— 做什么

把编程方式用价值观、原则和模式的形式展现出来,其优点之一就是可以更加有效地展现编程方法的差异。

Thinking: 从不同的做事方式识别出在哪个层次上存在分歧,模式? 原则? 价值观?

清晰的原则可以引出新的模式。原则可以解释模式背后的动机,它是有普遍意义的。

在矛盾的模式间选择时,最好的方式就是用原则来说话,而不是让模式争来争去。

如果遇到从未碰到过的情况,对原则的理解可以充当我们的向导。

原则1 局部化影响

组织代码结构时,要保证变化只会产生局部化影响。

实现模式的动机之一:减少变化所引起的代价。

Thinking:如果一部分代码被大量重用,应该是好事,但也带来了风险,变化这段代码就会影响非常多的地方,如何平衡这样的矛盾?

原则2 最小化重复

最小化重复有助于保证局部化影响。

并行的类层次结构也是重复的一种。

重复不是罪过,它只是增加了变化的开销。

把程序拆分成更小的部分可以消除重复,大段逻辑很容易与其他大段逻辑出现重复。

原则3 将逻辑与数据捆绑

局部化影响的必然结果就是将逻辑与数据捆绑。

在发生变化时,逻辑和数据很可能会同时被改动。放在一起,影响会控制在局部。

原则4 对称性

程序中处处充满了对称性。 add() vs remove()

识别出对称性,把它清晰地表述出来,代码将更容易阅读。一旦阅读者理解了对称性所涵盖的某一半,他们就会很快地理解另外一半。

void process() {
  input();
  count++;
  output();
}

第二句跟前后两句不同,更加具体,不能和前后两句形成对称。

void process() {
  input();
  incrementCount();
  output();
}

还是不太好,input()和output()都是方法意图命名,而incrementCount()是实现方式命名,那么为什么要增加这个数值?

void process() {
  input();
  tally(); // 计算、记录、记账
  output();
}

在准备消灭重复之前,常常需要寻找并表示出代码中的对称性。如果在很多代码中都存在类似的想法,那么可以先把它们用对称的方式表示出来,让接下来的重构有一个良好开端。

原则5 声明式表达

用简单的声明方式写代码,读起来更容易。

Thinking 给你的代码打上一些标记 ,因此 Annotation 注解是较好的实践。

声明式表达放弃了原始方法所具备的能力和通用性,但是它的风格使代码更易阅读。

原则6 变化率

把具有相同变化率的逻辑、数据放在一起,把具有不同变化率的逻辑、数据分离。

变化率具有时间上的对称性。

setAmount(int value, String currency) {
  this.value = value;
  this.currency = currency;
}
setAmount(int value, String currency) {
  this.value = new Money(value, currency);
}
setAmount(Money value) {
  this.value = value;
}

变化率原则也是对称性的一个应用,不过是时间上的对称。

第 4 章 动机

30年前,Yourdon 和 Constantine 在 Structured Design 一书中将经济学作为了软件设计的底层驱动力。软件设计应该致力于减少整体成本。

cost_total = cost_develop + cost_maintain

维护的代价很大,这是因为理解现有代码需要花费时间,而且容易出错。知道了需要修改什么以后,做出改动就变得轻而易举了。

掌握现在的代码做了哪些事情是最需要花费人力物力的部分。改动之后,还要进行测试和部署。

cost_maintain = cost_understand + cost_change + cost_test + cost_deploy

重要的经济学原则:金钱的时间价值和未来的不确定性。

实现策略应该是尽量将支出推后,更倾向于即时收益而非长远收益。

并非目光短浅,而是一方面着眼于即时收益,另一方面追求干净的代码,方便未来的开发工作。

Thinking: 三个星期,Kent Beck就完成了所有模式的梳理和写作!只是开始时,为了促使自己把注意力放在模式上,在记录下所遵守的模式之前一个字符的代码也不肯输入。

开发框架时的价值取向不同于一般开发,所以其实现模式也不同于一般的实现模式。

代码来自于人,服务于人。

第 5 章 类

实现模式最大的跨度只到类一级;与之相比,设计模式则主要是在讨论类与类之间的关系。

数据的变化比逻辑要频繁得多,正是这种现象让类有了存在的意义。

学会如何用类来包装逻辑和如何表达逻辑的变化,这是有效使用对象编程的重要部分。

在由对象搭建而成的程序中,类是相对昂贵的设计元素。一个类应该做一些有直接而明显的意义的事情。减少类的数量是对系统的改进,只要剩下的类不因此而变得臃肿就好。

找到一个贴切的名字是编程中最令人开心的时刻之一。贴切的名字能引发连锁反应,带来更深入的简化与改进。

在所有的命名当中,类的命名是最重要的。类是维系其他概念的核心。

类名应该简明扼要,但有时候一个类名又要用到好几个单词才足够精确。摆脱这种两难境地的办法就是给计算逻辑找到强有力的隐喻。脑子里有了隐喻,一个个单词就不只是单词,而是一张张关系、连接和暗示的大网。

Thinking: 所以学好英语对编程是有很大促进作用的,国内很多开发人员忽视或躲避英语的提升,长期下来就会吃很大的亏,对于深入理解较复杂的系统代码,英语的基础感觉会有很大的帮助。毕竟99%的程序代码是用英语完成的,代码最终看起来是不是专业,命名就是一个非常关键的重点,因此需要更多的英语世界的词汇量和语感做背书。

对于重要的类,尽量用一个单词来为它命名。


待续……