Dec 16, 2008

《重构》1-4章_基础

上个礼拜拿到了《重构》,读了前面的几章。还是蛮有意思的一本书,作者很幽默,译的也不错,只是价钱贵了点,有那么一点心疼。经典的书大多读起来不轻松,读快了啥也学不到,可有些经典的书读着有趣,随便翻翻有意无意间就会有比较大的收获,比如《代码大全》、《重构》等等。道理简单而实用,例子也恰到好处,加之文章穿插的点点幽默,实在是大快人心。

“记性好忘性大,故凡有所的必记诸文字”,译者熊节这话说得贴切。今天拿到的Maxtor的移动硬盘,广告居然叫“Save your life”。很好。不时时记录时时备份时时回顾,像我这等愚笨之人很快就会忘记生活了啥,这不能不说是一件可悲之事。

http://old.blog.edu.cn/user2/26669/archives/2007/1753406.shtml 上发现了《重构》的笔记,我就直接厚颜无耻得拿来,然后再稍稍加些东西吧。

译序 --熊节
快速迭代&RAD --> crack的问题解决方式,在混沌的循环往复中实现需求--解构的美。
软件开发的理想国--在设计前期使用模式往往导致过度工程
重构--空气和水

前言

设计不再是一切动作的前提,而是在整个开发过程中逐渐浮现出来。
重构:有纪律,经过训练的,有条不紊。
聚沙成塔,从根本上改变设计

第一章、重构:第一个案例


例子:影片出租店用的程序。操作者告诉程序:顾客租了哪些影片、租期,程序就根据租期和影片类型计算费用;还要为常客计算点数。点数的计算与是否为新片有关系。程序为每次租用打印报表。

措施:同一段代码不要出现两次,应该用函数来定义。否则将来还要修改多处,容易引进错误。

需求变化1:用html方式输出报表。
需求变化2:要改变电影的分类方式,当然会影响收费和点数的计算,但是具体方案还没确定。

经验:不论用户提出什么方案,你唯一能得到的保证就是用户会在6个月内修改。
(这条经验很好玩,相当于说,你根本无法保证用户的需求不变化,唯一能得到的保证就是用户需求一定会变化。)
如果你发现自己需要为程序添加一个特性,但代码结构使你无法方便地那么做,则先重构再添加特性

重构第一步:为即将重构的代码建立测试环境。这是永远的第一步。
修改必须依赖测试,否则很容易进入新的bug,而且非常难debug。
重构之前,首先检验自己是否有一套可靠的测试机制,这些测试机制必须有自我检验能力。

措施:修改长的离谱的函数,并把函数移动到合适的class中去。
措施:修改变量的名字,使程序更可读。

note:任何一个傻瓜都可以写出计算机可以理解的代码。只有写出人类容易理解的代码,才是优秀的程序员。

阅读代码的时候,我经常进行重构。这样我就不断地把我的理解嵌入到代码中,才不会遗忘我曾经理解的东西。

如果一个类成员函数没有用到所在类的数据成员,则要考虑这个函数是否放错了位置。

尽量消除临时变量。临时变量往往形成问题,可能导致大量参数传来传去。在长长的函数中不易读。当然会付出性能上的代价。
作者的意思是,如果临时变量需要好几行来计算得到,就把临时变量的计算抽取为函数,称为query method。

重构以微小的步伐修改程序,及时发现。

运用多态取代switch、if/else。

一部影片可以在生命周期内修改自己的分类,但是一个对象却不能在生命周期内修改自己的类型(class)。--解决办法:state pattern
建立price类以及三个子类,与价格相关的代码移动到price相关类中。movie中设置一个price类型的成员对象。
price类中设置一个虚函数getprice,再实现一个默认的getpoints,让newprice子类去覆盖它。
把类型相关的代码用state/strategy代替。

本章总结

extract method: 把长函数中的某部分抽取出来。把临时变量的计算抽取为函数(query method)。
move method: 看method使用哪个类的信息,就移动到哪个类中去。
replace conditional with polymorphism
self encapsulate filed ?这个是啥?
replace type code with state/strategy

重构的节奏:测试、小修改、测试、小修改。。。
重构后的程序风格将十分不同于过程化的风格。

第二章 重构原则

重构定义:对软件内部结构的一种调整,目的是在不改变软件的外部行为的前提下,提高可理解性,降低修改成本。
注意,重构的目的不是提高性能
高效且受控的代码整理模式。
两顶帽子:添加新功能和重构。
开发过程中,需要经常换帽子戴,无论何时,都该清楚你戴的是哪顶帽子,而且不能同时戴两顶帽子。

为什么要重构?
1、改进软件设计。重构就是要让所有代码回到应该在的位置。重构还可以消除重复代码。代码结构的流失是累积性的。
2、使软件更容易理解。修改代码,让代码反映我的意图。重构可以帮助你看到你以前看不到的设计层面的东西。重构带你到更高的理解层次上。
3、帮助你debug。弄清楚程序结构的同时,也很清楚地看到自己所做的一些假设。做个有着优秀习惯的好程序员。
4、助你提高编程速度。良好设计才是快速开发软件的根本。

重构应该随时随地进行。

三次法则:如果第三次做类似的事情,就应该重构。Three strikes and you refactor.
添加功能时重构
修改错误时重构
复审代码时重构。复审(code review)就是别人看你的代码。
不必想像代码应该是什么,可以看见它是什么样

为什么重构有用?kent beck

重构的方向:容易阅读;消除重复逻辑(代码);新的改动不会危及现有行为;尽可能简单表达条件逻辑。摆脱束缚的道路

间接层和重构 kent beck

间接层是双刃剑,需要更多的管理,难以阅读。但是,间接层的价值是:

1、允许逻辑共享;
2、对“意图”和“实现”分开解释。class和函数的名字可以看作你的意图,其内部代码是其实现。如果class和函数以“更小的意图”来编写,则“实现”更近距离地和“意图”接触。
3、将变化隔离。
4、把条件逻辑用多态来表示,往往能增加清晰度并提高弹性。

如果增加一个异常。。。为了控制这种情况,为一个pakage定义一个super异常,这个packge下的所有异常都继承自它。这样就可以新增异常了。

重构与设计互补。重构降低了预先设计的压力。可以带来更简单的设计。在设计时仍然考虑各种可能的变化和更为灵活的设计,但是要考虑:把一个简单的方案重构为灵活的方案需要多大代价?

教训:哪怕你完全了解系统,也请实际测量它的性能,不要臆测。(或许潜台词是,你也许了解程序的架构和各种细节,但是你不一定了解系统中代码的执行率。)

要想优化性能,必须认真测量。



重构与性能:重构的确会使软件的运行变慢,但它使优化阶段的性能调整更容易。
efficient:高效;effective:有效
首先编写出可调的软件,然后调整它以获得足够的速度。
时间预算法 >> 持续关注发 >> 90%"发现热点,去除热点"

第三章:代码的坏味道

知道How不代表知道When,决定何时重构、何时停止和知道重构机制同样重要。
没有任何知识规矩能比得上一个见识广博者的直觉。

1、重复代码

同一个类里的两个函数有相同的表达式。方法:extract method

两个兄弟子类里含有相同的表达式。方法:对两个类使用extract method,然后使用pull up method,放到父类中。


2、过长函数

现在的编译器优化已经基本消除了函数调用开销。

小函数容易理解。让函数容易理解的关键是,给函数起一个合适的名字。

每当感觉需要用注释说明什么的时候,就把那部分提取成函数,以其用途而不是实现方式命名。

其实关键不在于函数的长度,而在于“做什么”和“如何做”之间的语义距离。

99%的场合下,只需要使用extract method

对于大量的参数,可以引入参数对象;对于临时变量,可以使用query method。

杀手锏:replace method with method object.


3、过大的类

很容易产生代码重复。

产生太多的临时变量。


4、过多的参数

类可以减少参数,因为成员函数需要的很多参数可以在类中找到。


5、发散式变化(divergent change)

当需求变化一点时,一个类需要修改好几个函数,则这个类或许应该被拆成两个类。


6、散弹式修改(shotgun surgery)

当需求变化一点时,需要对多个类做出小修改。应该使用move method和move field把要修改的代码放到一个类中。

原则是:将一起变化的东西放在一块儿。


7、依恋情结(feature envy)


某个函数对某个宿主类以外的类特别依赖。症结往往是数据。extract method , move method


8、数据泥团(data clumps)


常常可以看到:两个类中有一个以上的相同数据成员,许多函数中有相同的参数。这些总在一起出现的数据应该提取为一个类。

更重要的是,可以帮助寻找feature envy。


9、基本类型偏执(primitive obsession)

很多程序员懒得定义一些小类。

你可以定义一些与基本类型差不多的小class,比如字符串、日期、含一个起始值和一个结束值的range class、电话号码等等特殊的字符串。


10、swith

switch的问题在于重复。?

如果要添加一个case子句,很容易出错。


11、平行继承体系

就是两套class有相似的继承体系。当你为一个类增加子类时,必须为另一个类增加一个平行的子类。

shotgun surgery的一种特殊情况。


12、冗余类

对于没用的类、体系结构坚决消除。


13、过多考虑未来不确定的需求


如,看不到用处的扩展性。


14、很少用的数据成员

有的field可能只在极少情况下被使用。可以为这个field及其相关代码extract class。


15、过度耦合的消息链


没懂。也许后面有例子会好一点。


16、中间人


太多的delegation,就产生了中间人。中间人几乎不干实事,应该消除。


17、过分亲密

两个类过于亲密,互相探究彼此的private部分。有很多方法解决这个问题。可以move method,move field,extract class等等。


18、异曲同工的类/函数


两个函数功能一样但是签名不一样。


19、不完美的程序库


如果程序库不满足需求,可以:introduce foreign method, introduce local extension


20、纯粹的数据类


就像java bean?也挺有用的啊

只有fieild和getter setter。应该把调用它的那些代码抽取到这个类中,以隐藏一部分getter/setter


21、拒绝继承

如果子类拒绝继承父类的某些field、某些函数的实现,问题不算很大,甚至可以不用管;但是如果子类拒绝继承父类的接口,这就出现问题了。


22、过多的注释


当你需要撰写注释,请先尝试重构,试着让所有的注释都变得多余。

注释常常把我们带到之前提到的各种smell。如果注释要解释一块代码做了什么,请extract method;如果已经提取出函数但是仍然需要注释,则请rename method;如果需要说明某些入口条件,请引入断言。

注释可以写todo,why。



第四章:构筑测试体系
seft-testing Code
: 确保所有的测试都完全自动化,让它们自己检验自己的测试结果。
极限编程者都是十分专注的测试者。
testing main: 每个Class都需要有一个用于测试的main()
频繁地运行测试,每次编译都把测试考虑进去。
编写测试代码时,先让它们失败--failures <--> errors
bug --> 编写单元测试
编写不完善的测试并实际运行,好过对完美测试的无尽等待。
必然有错 --> 抛出异常

0 comments: