《Code Complete 2》读后总结

最近读完《Code Complete 2》,分享记录书中的一些关于写程序的观点与建议。

102页,语义上的耦合(需要避免)

一个模块不仅使用了另一个模块的语法元素,而且还使用了有关那个模块内部工作细节的语义知识。这里有一些例子:

  • Module1向Module2传递了一个控制标示,用它告诉Module2该做什么。这种 方法要求Module1对Module2的内部工作细节有所了解,也就是说需要了解Module2对控制标志的使用。(如果Module2把这个控制标志定义成一个特定的数据类型(枚举类型或者对象),那还能接受)
  • Module2在Module1修改了某个全局数据之后,使用该全局数据。这种方式要求Module2假设Module1对该数据所做的修改符合Module2的需要,并且Module1已经在恰当的时间被调用过。
  • Module1的接口要求它的Module1.Initialize()子程序必须在它的Module1.Routine()之前得到调用。Module2知道Module1.Routine()无论如何都会调用Module1.Initialize(),所以它在实例化Module1之后只是调用了Module1.Routine(),而没有先去调用Module1.Initialize()。
  • Module1把Object传给Module2。由于Module1知道Module2只用了Object的7个方法中的3个,因此它只部分地初始化Object,只包含那3个方法所需的数据。
  • Module1把BaseObject传给Module2。由于Module2知道Module1实际上传给它的是DerivedObject,所以它把BaseObject转换成了DerivedObject,并且调用了DerivedObject特有的方法。

语义上的耦合是非常危险的,因为更改被调用的模块中的代码可能会破坏调用它的模块,破坏的方式是编译器完全无法检查的。
松散耦合的关键之处在于,一个有效的模块提供出了一层附加的抽象,你就可以想当然地去用它。这样就降低了整体系统的复杂度,使得你可以在同一时间只关注一件事。如果对一个模块的使用要求你同时关注好几件事(其内部工作细节、对全局数据的修改、不确定的功能点等),那么就失去了抽象的能力,模块所具有的管理复杂度的能力也削弱或完全丧失了。

139页,良好的封装

这里节选一些:
每当你发现自己是通过查看类的内部实现来得知该如何使用这个类的时候,你就不是在针对接口编程了,而是在透过接口针对内部实现编程了。
如果仅仅根据类的接口文档还是无法得知如何使用一个类的话,正确的做法不是拉出这个类的源代码,从中查看其内部实现。这是个好的初衷,但却是个错误的决断。正确的做法应该是去联系类的作者,让他修改类的接口文档,之后你再查看文档。
(在实际工作中,书中所说的只能是一种理想。大多数情况是连文档都没有,都是靠自己查看类的内部实现来搞懂如何使用。但是你可以往这种理想情况去努力。你可以尽量要求自己所写的方法,别人能够通过方法名、参数与返回值就知道如何使用。你自己看过的其它类的内部实现,可以通过添加注释的办法,让后来的同事可以尽快了解该如何使用。)

144页,继承(“是一个。。。。。。”关系)

如果派生类不准备完全遵守由基类定义的同一个接口契约,继承就不是正确的实现技术了。请考虑换用包含的方式,或者对继承体系的上层做修改。
Liskov(LSP)替换原则的总结:“派生类必须能通过基类的接口而被使用,且使用者无须了解两者之间的差异”
这里有个例子:
如果你有一个Account基类以及CheckingAccount、SaveingsAccount、AutoLoanAccount三个派生类,那么程序员应该能调用这三个Account派类中从Account继承而来的任何一个子程序,而无须关心到底用的是Account的哪一个派生类的对象。
如果程序遵循Liskov替换原则,继承就能成为降低复杂度的一个强大工具,因为它能让程序员关注于对象的一般特性而不必担心细节。如果程序员必须要不断地思考不同派生类的实现在语义上的差异,继承就只会增加复杂度了。假如说程序员必须记得:“如果我调用的是CheckingAccount或SavingsAccount中的InterestRate()方法的话,它返回的是银行应付给消费者的利息;但如果我调用 的是AutoLoanAccount中的InterestRate()方法就必须记得变号,因为它返回的是消费者要向银行支付的利息。”根据LSP,在这个例子中AutoLoanAccount就不应该从Account继承而来,因为它的InterestRate()方法的语义同基类中的InterestRate()方法的语义是不同的。

150页,对其他类的子程序的间接调用要尽可能少

例如:account.ContactPersion().DaytimeContactInfo().PhoneNumber()。
A对象可以任意调用它自己的所有子程序。如果A对象创建了一个B对象,它也可以调用B对象的任何(公用)子程序,但是它应该避免再调用由B对象所提供的对象中的子程序。

164页,创建子程序的正当理由

简化复杂的布尔判断。把判断放到函数中,可以提高代码的可读性,因为:(1)这样就把判断的细节放到一边了;(2)一个具有描述性的函数名字可以概括出该判断的目的。

小的子程序有许多优点。其一便是它们能够提高其可读性。

伪代码示例
points = deviceUnits * (POINTS_PER_INCH / DeviceUnitsPerInch())

多数人最终都能看懂,它进行的是从设备单位(device unit)到磅数(point)的转换计算。人们也会看出这十几处代码都在做着同样的事情。但是,它们原本可以更清楚些,所以创建一个子程序,并起个好的名字。

伪代码示例
Function DeviceUnitsToPoints(deviceUnits Integer) : Integer
  DeviceUnitsToPoints = deviceUntis * (POINTS_PER_INCH / DeviceUnitsPerInch())
End Function

调用时
points = DeviceUnitsToPoints(deviceUnits)

修改后的代码更具可读性–甚至已经达到自我注解的地步。

168页,在子程序层上设计

功能的内聚性是最强也是最好的一种内聚性,也就是说让一个子程序仅执行一项操作。
除此之外,还有其他一些种类的内聚性人们却通常认为是不够理想的。
顺序上的内聚性,通信上的内聚性,临时的内聚性。(书中都举了例子,利于理解)

179页,为子程序传递用以维持其接口抽象的变量或对象

关于如何把对象的成员传给子程序这一问题,存在着两种互不相让的观点。比如说你有一个对象,它通过10个访问器子程序暴露其中的数据,被调用的子程序只需要其中的3项数据就能进行操作。
第一种观点是,只应传递子程序所需的3项特定数据。
第二种观点是,应该传递整个对象。
这两种规则都过于简单,没有击中问题的要害:子程序的接口要表达何种抽象?如果要表达的抽象是子程序期望3项特定的数据,但这3项数据只是碰巧由同一个对象所提供的,那就应该单独传这3项数据。然而,如果子程序接口要表达的抽象是想一直拥有某个特定对象,且该子程序要对这一对象执行各种操作,那就传整个对象。
(在工作中也遇到过这样的问题,不管是第一种观点还是第二种观点,都不能说服我自己。现在书中给了比较明确的判断思路。)

198页,异常

只有真正例外的情况下才抛出异常。
避免在构造函数和析构函数中抛出异常,除非你在同一个地方把它们捕获。
在恰当的抽象层次抛出异常。

Java反例
class Employee{
  ...
  public TaxId getTaxId() throws EOFException {
  }
}

getTaxId()把更低层的EOFException(文件结束,end of file)异常返回给了它的调用方。它本身并不拥有这一异常,但却通过把更低层的异常传递给了其调用方,暴露了自身的一些实现细节。这就使得子程序的调用方代码不是与Employee类的代码耦合,而是与比Employee类层次更低的抛出EOFException异常的代码耦合起来了。

246页,尽可能缩短变量的“存活”时间

变量定义和使用的地方尽量靠近,除了集中阅读外,在拆分长子程序时,也比较容易。

252页,绑定时间

绑定时间:把变量和它的值绑定在一起的时间。采用越晚的绑定会越有利。

268页,为布尔变量命名

对于boolean类型,不要用status这种变量名。因为status=true也不知道是代表什么意思。或者用statusOK。
另外,有时喜欢用isDone、isError这种变量,这种优点是它不能用于那些模糊不清的名字:isStatus?这毫无意义。它的缺点是降低了简单逻辑的表达式的可读性:if(isFound)的可读性要略差于if(found)。
使用肯定的布尔变量名!否定的名字如notFound、notDone等较难阅读,特别是如果它们被求反:if(!notFound)。

293页,数值概论

使类型转换变得明显。确认当不同数据类型之间的转换发生时,阅读你代码的人会注意到这点。

y = x + (float) i

这种实践还能帮助确认有关转换正是你期望发生的。
(吐槽:常常会遇到对于类型隐式转换的一些面试题,如果是出于知识点考核那还勉强说得过去。在工作中,个人倾向于明确地指出类型转换,即使你非常清楚自己的“隐式”写法没问题。在代码中出现隐式转换的情况,要么就是没注意到类型不同,要么就是为了装X。对于整个团队都没什么好处。其它成员看到这样的代码,还要想想这个转换规则是怎样啊?原代码作者是没注意到类型转换还是本来的意图就是这样呢?)

避免混合类型的比较。如果x是浮点数,i是整数,那么下面的测试

if (i == x) then ...

不能保证可行。请自己动手进行类型转换。

347页,必须有明确顺序的语句

在必须有明确顺序的语句情况下,应该尽量使得这些依赖关系变明显。
例如:
computeMarketingExpense()
computeSalesExpense()
computeTravelExpense()
displayExpenseSummary()
在computeMarketingExpense()中,除了进行计算marketing数据外,还负责初始化数据,以便后面其他方法都能把它们的数据放进去。但是仅通过代码无法看出这点。
下面有一些简单原则组织语句,避免上面的情况。

  1. 设法组织代码,使依赖关系变得明显。例如:把初始化的代码移出来,新增一个initExpenseData()的方法。
  2. 使子程序名能突显依赖关系。computeMarketingExpense()的命名是错误的,因为它里面除了计算,还做了初始化数据的工作。但是名字加上initExpenseData后,你就会发现方法名太长。这时就应该审视,这个方法是否合理了。(所以这时你就会发现,这个方法没做到功能单一了~)
  3. 利用子程序参数明确显示依赖关系。具体查看349页。
  4. 用注释对不清晰的依赖关系进行说明。
355页,if-then语句

1.首先写正常代码路径;再处理不常见情况
2.把正常情况的处理放在if后面而不是放在else后面
3.让if子句后面跟随一个有意义的语句

if (someTest)
  ;
else {
  // do something
  ...
}

这样的代码看上去就很迷惑。
4.考虑else子句。如果你认为自己只需要一个简单的if语句,请考虑你是否真的不需要一个if-else语句。

382页,循环变量

在嵌套循环中使用有意义的变量名来提高其可读性。

for (int i=0; i < numPayCodes; i++) {
  for (int j=0; j < 12; j++){
    for (int k=0; k < numDivisions; k++){
      sum = sum + transaction[j][i][k];
    }
  }
}

例子使用了没有意义的i、j和k。透过i、j和k并不能看出transaction的内容。

for (int payCodeIdx=0; payCodeIdx < numPayCodes; payCodeIdx++) {
  for (int month=0; month < 12; month++){
    for (int divisionIdx=0; divisionIdx < numDivisions; divisionIdx++){
      sum = sum + transaction[month][payCodeIdx][divisionIdx];
    }
  }
}

修改变量名后,你从payCodeIdx、month和divisionIdx能获取的信息更多。(如果只是简单的一层循环,直接用i其实也没什么大问题。还是看具体情况,看代码本身是否能表达出自己的意图。)

411页,表驱动法

从表里面查找信息而不使用逻辑语句(if和case)。
(这个表不是指数据库表,可以简单理解成数组)

435页,编写肯定形式的布尔表达式
437页,用括号使布尔表达式更清晰
if (a < b == c == d) ...

看代码的人可能会疑惑,写代码的人想表达的是(a < b) == (c == d)还是(( a < d) == c) == d。
运算符是有优先级的,但是个人更倾向加括号来提高代码的可读性和正确性,即使你自己非常清楚优先级,并且没写错。主要是方便其它成员阅读代码。(吐槽:同样的,也遇到很多考核运算符优先级的面试题~)

440页,按照数轴的顺序编写数值表达式

这里的关键点在于要从左到右、从小到大地排列元素。
在这里插入图片描述

写在最后

《Code Complete 2》其实前几年也读过,但是没读完。最近重新读,发现有些地方在日常工作中会有忽视。在写代码时,也会因为各种各样的理由,对代码质量进行了妥协。所以,在这里记录下个人认为比较容易达成的“准则”。这些“准则”都是非常容易做到的,基本不涉及需求和设计。
书中也有关于编程思想和设计的一些篇章,这些可以直接看书。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值