读《程序员修炼之道》篇二:务实的方法

阅读完篇一之后, 我们可以从务实的程序员身上学到很多优秀的特质, 比如务实程序员在面对突发情况,是怎么处理这些问题的, 比如务实程序员是怎么看待他们自己的工作的等等. 篇二: 务实的方法,更多的告诉我们务实程序员一些好的习惯, 学习了这些习惯之后, 你也可以是一名务实的程序员

篇二给我印象最深刻的内容

“优秀的设计比糟糕的设计更容易变更 , 也称为ETC(Easy To Change)原则”

关于Easy To Change原则

无论是什么设计原则, 都是ETC的一个特例.

  1. 为什么解耦(decouple)很好? 因为通过隔离关注点,可以让每个部分都更加容易变更, 这就是ETC.
  2. 为什么会有控制反转? 因为相比于依赖具体的class, 依赖抽象的interface 可以让代码的修改变得更加简单, 因为interface 是不经常变化的, 而具体的class 是会频繁变化的
  3. 为什么单一职责(Single Responsibility Principle)很有用? 因为一个需求的变化仅体现为某个单一模块上的修改, 这就是ETC.
  4. 为什么命名很重要, 因为好的命名可以使代码更容易阅读, 而你需要通过阅读代码来进行变更, 这就是ETC.

一个简单的方法可以判断你的代码是否遵循ETC原则, 尝试提出一个小需求, 如果你只需要修改模块中很少的地方就可以满足这个需求, 那说明你的代码是遵守ETC原则, 反之则否.

 就我个人工作一年的体验来说, 对ETC原则的感受还是很深刻的, 我曾经写出像一坨翔一样的代码杂糅在一起, 可以毫不夸张地说,这坨代码就连我自己修改都很费劲. 虽然这坨代码阅读起来感觉还好,但是因为各个模块都耦合的很厉害,往往修改了一个小地方,就会引起组件内其他模块的错误, 导致需求变更的时候 往往要修改很多处的代码 并且多次调试之后才能满足要求. 这个代码就是违反ETC原则的典型例子. (由于这个组件涉及的范围很小,并且对外部变化是不敏感的,所以在Code Review的时候很幸运的逃过一劫)

Why we need ETC

程序员一致处于维护模式下,从未间断. 我们的理解每天都在变化.

  在看到ETC的时候, 我会下意识问自己为什么需要ETC? ETC的目的是什么? 现在我想我应该能给出一个相对稳妥的答案. 作为一个程序员, 我们的理解会时刻随着需求的变化而变化, 比如政策变动导致某些业务逻辑需求重构, 再比如产品经理说需求文档的这里他并不是这个意思, 又或者是你想重构某一个大的模块, 务实程序员都会有一个共识那就是“变化是常态的,是无时无刻都在发生的”. 这些时候如果你的代码遵循ETC原则, 那么这将给你带来极大的便利性.

 那么关键的问题在于如何使你写的代码满足ETC呢? 书中给的建议是一开始需要有一点有意识的强化, 你可能需要花一周的时间来有意识的问自己:“我刚刚做的事情使得整个系统更容易改变还是更难改变? 当你保存文件的时候问一遍, 当你写测试的时候问一遍, 当你修复Bug的时候也问一遍” , 具体到我而言, 我是习惯在准备一次commit的时候, 我会review我自己的代码, 并且问自己是否使整个系统更容易改变了?

DRY(Don’t Repeat Yourself)原则

DRY原则: 在一个系统中, 每处知识都必须单一、明确、权威地表达.

  相信我们都有这样的情况, 当一个函数在多处被调用的时候, 如果没想到很好的方法暴露这个函数, 就很自然而然的想到把那个函数复制到需要使用的地方, 这样就能解决我们的问题. 虽然这是一个解决方案, 但是它带来极大的负担, 因为你在维护两处一样的知识

DRY 不限于编码

  很多人包括我在一开始理解DRY的时候, 都片面的认为DRY只适用于编程中, 把它的意思限定为“不要复制粘贴源码”, 这确实是DRY的一部分. 但更多DRY想要表达的点在于“你对知识和意图的复制, 它强调的是,在两个地方表达的东西其实是相同的, 只是表达方式有可能完全不同”.

举个例子
当代码的某个模块发生变化的时候, 你是否发现自己在多个地方以多种不同的形式进行了变更? 修改你代码的同时需不需要修改你的技术方案? 需不需要同时变更数据库里面的schema? 如果这类情况发生, 说明你的代码并不满足DRY. 就我个人而言,我的技术方案不会像写代码那样详细, 一般就是伪代码和对应的流程图, 要从数据的角度把整个流程给串起来. 这样的话,只要主流程不会改变, 那么代码的修改并不影响我技术方案的正确性, 除非主流程发生变化(这其实对应着的是知识的变化), 那么我也是对应着先修改我的技术方案, 然后再着手修改代码. 完全追求DRY是不可取的, 因为技术方案必然会和实现代码有部分耦合的地方, 我们只能做到尽量减少这些耦合的地方.

注意 这里要区分 到底是不是知识的重复 ,还是只是恰巧使用了相同的规则, 这是个巧合,而不是重复, 比如下例的两个函数, 他们校验了两个不相干的东西,只是恰巧是用了同样的规则.

1
2
3
4
5
6
7
def validate_age(value):
  validate_type(value, :integer)
  validate_min_integer(value, 0)

def validate_quantity(value):
  validate_type(value, :integer)
  validate_min_integer(value, 0)

数据中的DRY违规

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

class Line {
  private double length;
  private Point start;
  private Point end;

  public Line(Point start, Point end) {
    this.start = start;
    this.end = end;
    calculateLength()
  }

  //public

  void setStart(Point p) {this.start = p; calculateLength();}
  void setEnd(Point p) {this.end = p; calculateLength();}

  Point getStart(void) {return start;}
  Point getEnd(void) {return end;}

  double getLength() {return length;}

  private void calculateLength() {
    this.length = start.distanceTo(end);
  }
}

其实在一开始,我是很不理解为什么要有访问器这种东西的,因为感觉用起来并没有很方便, 直接通过点操作符去修改成员变量不是更好吗? 但是你会发现基本上所有面向对象的语言都会强调访问器, 其一我觉得有部分原因在于对象不应该暴露其内部的实现给外界, 这个说法我觉得是合乎道理的, 外界不需要这个对象的存储形式, 只需要关心这个对象提供的接口就可以了; 其二我觉得就是通过约定访问器,我们可以知道对象是getter 和setter都提供,还是只提供了getter/setter, 这样明确的划分了数据的权限; 其三,就是容易变更, 我们需要对内部实现进行变更的时候,对外的接口不需要发生变化. 比如上述例子中,length这个成员变量, 如果我们是需要昂贵的计算才能得到的话,我们可以缓存这个值. 如果计算很廉价的话/或者存储很昂贵的前提下,我们能基于计算去提供给外界.

一个模块提供的所有服务都应该通过统一的约定来提供, 该约定不应暴露出其内部实现是基于存储的还是基于运算的.

开发人员之间的重复

最难检测到且最难处理的重复类型, 可能发生在同一项目中的不同开发人员之间. 整体功能可能会在不经意间重复, 而这种重复或许好几年都不会有人发现, 最终导致严重的维护问题.

开发人员之间的重复这个问题, 目前我觉得最好的解决方法就是鼓励开发人员之间的频繁交流, 包括但不限于深度参与小组内其他成员的技术方案的评审/对其他成员提交的代码进行Code Review/ 平常遇到开发相关的问题多和其他成员讨论. 我在字节的时候, 遇到的这个问题还不太严重, 一个原因是因为我呆的时间还不够长, 另一个原因在于我们小组只要有需求都会在小组内进行一次技术方案的评审, 所以的小组成员在需求不是特别紧张的情况下都需要参与. 而且我们小组内部的Code Review制度还是很完善的, 负责评审的同学也是对整个模块特别了解的人, 所以很少会发生开发人员之间的重复的问题, 一般我们都能在开发之前发现其他人应该也有这个问题, 然后我们就会商量谁来解决这个问题, 另外一个人去前置依赖解决问题的人的方案. 我在字节的时候做的还不够好的点在于我没有积极的review 别人代码, 第一个点我觉得这会很浪费我的时间, 因为我们组的需求一般都是很大的, 一个需求可能就是1个月-2个月的工期, 整个需求的commit修改可能涉及好几千行, 看起来很耗费力气. 第二个点是觉得目前我这个阶段还是不太需要进行review 别人的代码, 虽然我经常性review我之前写的代码, 会发现很多不合理的地方, 这其实说实话对我个人的成长是很有帮助的. 不过把时间线拉到现在来看, review他人的代码还是很有必要的, 因为这会对自己编码水平会有很大的帮助! 说实话, 当时我觉得我们组内的大多数同学的水平都是比我高的hhh, review 他们的代码无疑是一种向他们学习最好的方式之一. 这个点希望对其他同学也是一个很大的帮助, 应该牢记在心, 但这个前提是你对项目已经有一定水平的了解, 我觉得对于应届生的标准应该在入职半年后就可以这么去做了.

正交性原则

正交性是我们在写不同模块的代码所追求的最高境界, 我们希望多个模块的代码相互独立, 彼此之间并不依赖. 正交性在计算机科学中象征着独立性或解偶性, 比如

一个模块内的修改不应该影响其他模块的正常行为, 也是我们经常所提到的高内聚.

举个例子

举个直升机的例子, 假设你在乘坐直升机游览城市景观的时候, 飞行员犯了一个严重的错误,他午餐吃鱼吃坏了肚子,陷入了昏迷当中. 幸运的是, 事发时飞行员让直升机悬停住了, 你现在身处于离地100英尺的天空中. 恰巧, 你头天晚上在维基百科上读到有关于直升机的介绍, 知道直升机有四种基本操作. 回旋杆指的是你右手握着的操作杆, 推动它直升机就会向相应的方向移动; 你的左手握着的是总距离操纵杆, 向上拉能增加所有桨叶的螺距,产生升力; 总距操纵杆的顶端是油门; 最后还有两个。脚蹬, 用来改变尾桨的推力, 从而帮直升机转向.

“简单!“,你心里想这不是有手就行. 只要缓缓地放低总距操纵杆, 就能优雅地降落到地面, 然而你真的去做的时候, 才发现现实远没有这么简单. 当直升机的机头开始下沉的时候, 机身开始向坐下回旋. 你猛然发现 ,飞行系统对每个操作输入都有一个次生效应(副作用), 压低左手操纵杆的同时, 你需要在右手操作杆上补偿一点向后的运动, 并踩一下右脚蹬.这一系列操作, 每个都会再次对其他操作造成影响. 突然间,你要应付一个难以置信的复杂系统, 系统中的每个变化都影响着其他输入. 你的工作负担大的惊人, 手脚不断移动, 试图平衡所有的交互力

正如直升机的例子所揭示的, 非正交系统天生就很复杂, 难以变更和控制. 当系统的组件相互之间高度依赖的时, 就没有局部修理这种说法.

其实并不是说直升机系统写的不够好, 而是大型复杂系统很难保证正交性, 就像Linux 操作系统也是同样的道理, 你说Linux系统能保证完全的正交吗? 显然是不能的, 虽然Linux的模块化做的很好, 但是这么复杂的系统上,模块之间的相互交互很难保证完全的正交, 只能是开发人员尽量只暴露模块有限的接口给外界, 将变更限定于局部等等, 来维护正交性. 只要不去改变组件对外的接口, 就可以放心, 不会发生波及整个系统的问题.


need Update 正交性

可逆性原则

举个例子
错误往往在于认为任何决定都是板上钉钉的, 没有为可能发生出现的意外做好准备. 与其认为决定是被刻在石头上的, 还不如把他们想象成写在海滩上的沙子上. 一个大浪随时都可能袭来,卷走一切. 可逆性原则也同时遵循ETC原则, 因为变化随时都可能发生.
  • 数据库的例子

曳光弹

曳光弹在我个人理解来说就是 项目的skeleton, 你得先写skeleton 然后专注于某个功能的实现.

原型

原型在我看来就是Demo工程, 在你不确定一个第三方库能不能满足你的需求的时候, 你往往需要一个Demo工程来测试这个库, Demo工程测试完就可以丢到, 然后把它集成到我们项目中,

为什么需要原型, 首先,我们什么都不做 只是觉得符合要求就直接集成到项目中, 第一我们需要做出大量的更改来测试这个库是否能满足我们的需求, 一旦发现这个库不能满足我们的需求,这些大量的更改所花费时间就会浪费掉. 如果只是Demo工程的话,我们只需要很少的修改就可以测试这个库的功能, 这样来说成本会更低.

估算

0%