重构_改善既有代码第四天笔记

来源:互联网 发布:淘宝网金丝绒套装 编辑:程序博客网 时间:2024/06/03 17:42

12.程序有两面价值:
1)“今天可以为你做什么”
2)“明天可以为你做什么”

13.计算机科学是这样一门科学:它相信所有的问题都可以通过增加一个间接层来解决。

14.大多数重构都为程序引入更多间接层。重构往往把大型对象拆成多个小型对象,把大型函数拆成多个小型函数。

15.间接层的价值:
1)允许逻辑共享.
 比如说一个子函数在两个不同的地点被调用,或超类中的某个函数被所有子类共享。
2)分开解释意图和实现。
    可以选择每个类和函数的名字,类或函数内部则解释实现这个意图的做法。如果类和函数内部又以更小的意图来编写,所写的代码就可以描述其结构中的大部分重要信息。
3)隔离变化。
比如在两个不同地点使用同一个对象,其中一个地点需要改变对象行为,但如果修改了它,就要冒同时影响两处的风险。为此可以做出一个子类,并在需要修改处引用这个子类。这样就可以修改这个子类而不必承担无意中影响另一处的风险。
4)封装条件逻辑。
对象有一个奇妙的机制:多态消息,可以灵活而清晰地表达条件逻辑。将条件逻辑转化为消息形式,往往能降低代码的重复,增加清晰度并提高弹性。

16.修改过接口
关于对象,一件重要的事情是:它们允许你分开修改软件模块的实现和接口。你可以安全地修改对象内部实现而不影响他人,但对于接口要特别谨慎---如果接口被修改了,任何事情都有可能发生。

17.  该如何面对那些必须修改“已发布接口”的重构手法?
如果重构手法改变了已发布接口,必须同时维护新旧两个接口,直到所有用户都有时间对这个变化做出反应,留下旧函数,让旧接口调用新接口。当需要修改某个函数名称时,请留下旧函数,让它调用新函数。千万不要复制函数实现,那会陷入重复代码的泥潭中难以自拔。

18.何时不该重构
   有时候根本不应该重构,比如当你应该重现编写所有代码的时候,有时候既有代码实在太混乱,重构它还不如重新写一个来得简单。
  
19.重写而非重构的一个清楚讯号就是:现有代码根本不能正常运作。记住,重构之前,代码必须起码能够在大部分情况下正常运作。

20.除了对性能有严格要求的实时系统,其他任何情况下“编写快速软件”的秘密就是:首先写出可调的软件,然后调整它以求获得足够速度。

21.编写快速软件的三种方法:
1)其中最严格的是时间预算法,这通常只用于性能要求极高的实时系统。
2)第二种方法是持续关注法。这种方法要求任何程序员在任何时间做任何事时,都要设法保持系统的高性能。
3)第三种性能提升法。
关于性能,如果对大多数程序进行分析,就会发现它把大半时间都耗费在一小半代码身上。如果一视同仁地优化所有代码,90%的优化工作都是白费劲的,因为被优化的代码大多很少被执行。所花时间做的优化是为了让程序运行更快。
    利用上述的90%的统计数据,编写构造良好的程序,不对性能投以特别的关注,直至进入性能优化阶段(通常是在开发后期)。一旦进入该阶段,你再安装某个特定程序来调整程序性能。

22。在性能优化阶段,首先应该用一个度量工具来监控程序的运行,让它告诉你程序中那些地方大量消耗时间和空间。这样就可以找出性能热点所在的一小段代码。然后集中关注这些性能热点,并使用持续关注法中的优化手段来优化它们。
把注意力都集中在热点上,较少的工作量便可显现较好的成果。即便如此,还是必须保持谨慎,应该小幅度进行修改。每走一步都需要编译,测试,再次度量。如果没有提高性能,就应该撤销此次修改。继续这个“发现热点,去除热点”的过程,直到获得客户满意的性能为止。

23.一个构造良好的程序可从两方面帮助这一优化形式。
1)首先,让你有比较充裕的时间进行性能调整,因为有构造良好的代码在手,就能够更快速地添加功能,也就有更多时间用在性能问题上(准确的度量则保证你把这些时间投资在恰当地点)。
2)其次,面对构造良好的程序,在进行性能分析时,便有较细的粒度,于是度量工具把你带入范围较小的程序段落中,而性能的调整也比较容易些。由于代码更加清晰,因此能够更好地理解自己的选择,更清楚那种调整起关键作用。

24。Duplicated Code(重复代码)
坏味道行列首当其冲的就是Duplicated Code。如果在一个以上的地点看到相同的程序结构,那么可以肯定:设法将它们合二为一,程序会变得更好。
1)最单纯的Duplicated Code就是“同一个类的两个函数含有相同的表达式”。这时提炼出重复的代码,然后让这连个地点都调用被提炼出来的那一段代码。
2)另外一种常见情况就是“两个互为兄弟的子类内含有相同表达式”。
   a、这时只需把被提炼出来的代码,将它推入超类内。
   b、如果代码之间只是类似,并非完全相同,将相似部分和差异部分分割开来,构成单独一个函数。然后利用模板设计模式,来实现。
   c、如果两个毫不相关的类出现DulicatedCode,应该考虑将重复代码提炼到一个独立类中,然后在另一个类内使用这个新类。但是,重复代码所在的函数也可能的确只应该属于某个类,另一个类只能调用它,或这个函数可能属于第三个类,而另两个类应该引用这个第三个类。必须决定这个函数放在哪里最合适,并确保它被安置后就不会再其他任何地方出现。
 
25.Long Method(过长函数)
1)拥有短函数的对象会活得比较好,比较长。
2)积极地分解函数,遵循这样一条原则:每当感觉需要以注释来说明点什么的时候,我们就把需要说明的东西写进一个独立函数中,并以其用途(而非实现手法)命名。
我们可以对一组甚至短短一行代码做这件事,哪怕替换后的函数调用动作比函数自身还长,只要函数名称能够解释其用途,我们也应该毫不犹豫地那么做。关键不在于函数的长度,而在于函数“做什么”和“如何做”之间的语义距离。
3)如果函数内有大量的参数和临时变量,它们会对你的函数提炼形成障碍,可以把参数和临时变量当作参数,传递给被提炼出来的新函数,尽可能消除这些临时元素,则可以将过长的参数列表变得更加简洁一些。

26.如何确定该提炼那一段代码呢?
1)一个很好的技巧是:寻找注释。它们通常能指出代码用途和实现手法之间的语义距离。
2)如果代码前方有一行注释,就是在提醒你:可以将这段代码替换成一个函数,而且可以在注释的基础上给这个函数命名。
3)就算只有一行代码,如果它需要一注释来说明,那也值得将它提炼到独立函数去。
4)条件表达式和循环常常也是提炼的信号。可以使用不同的函数处理条件表达式,至于循环,应该将循环和其内的代码提炼到一个独立函数中。

27.Large Class(过大的类):
   如果想利用单个类做太多事情,其往往会出现太多实例变量。一旦如此,重复代码就接踵而至了。
  
28.Long Parameter List(过长参数列)
1)刚开始学习编程的时候,老师教我们:把函数所需的所有东西都以参数传递进去。这可以理解,因为除此之外就只能选择全局数据,而全局数据需要及时处理释放。
2)对象技术改变了这一情况:如果你手上没有所需的东西,总可以叫另一个对象给你。
3)因此,有了对象,你就不必把函数需要的所有东西都以参数传递给它了,只需传给它足够的,让函数能从中获得自己需要的东西就行了。函数需要的东西多半可以在函数的宿主类中找到,面向对象程序中的函数,其参数通常比传统程序中短得多。

29.Divergent Change(发散式变化)
1)我们希望软件能够更容易被修改--毕竟软件再怎么说本来就该是“软”的。一旦需要修改,我们希望能够跳到系统的某一点,只在该处做修改。如果不能做到这点,你就嗅出两种紧密相关的刺鼻味道中的一种了。
2)如果某个类经常因为不同的原因在不同的方向上发生变化,Divergent Change就出现了。Divergent Change是指一个类受多种变化的影响。

30. Shotgun Surgery(散弹式修改) :则是指“一种变化引发多个类相应修改”。

31. Feature Envy(依恋情结)
1)对象技术的全部要点在于:这是一种“将数据和对数据的操作行为包装在一起”的技术。
2)函数对某个类的兴趣搞过对自己所处类的兴趣。这种最通常的焦点就是数据。某个函数为了计算某个值,从另一个对象那里调用几乎半打的取值函数,这种就要把这个函数移动到取值函数里面去。

32.一个函数往往会用到几个类的功能,那么它究竟该被置于何处呢?
原则是:判断哪个类拥有最多被此函数使用的数据,然后就把这个函数和那些数据摆在一起。

33.Data Clumps(数据泥团)
1)如果在多个地方看到相同的三四项数据:两个类中相同的字段,许多函数签名中相同的参数。这些总是绑在一起出现的数据真应该拥有属于它们自己的对象。
2)首先找出这些数据以字段形式出现的地方,运用导出类将它们提炼到一个独立对象中。
3)然后将注意力转移到函数签名上。减少字段和参数的个数。

34.一个好的评判办法是:删掉众多数据中的一项。这么做,其他数据有没有因而失去意义?如果它们不再有意义,这就是一个明确信号:你应该为它们产生一个新对象。

35。Primitive Obsession(基本类型偏执)

36.大多数编程环境都有两种数据:
1)结构类型允许你将数据组织成有意义的形式:
2)基本类型则是构成结构类型的积木块。
结构总是会带来一定的额外开销,它们可能代表着数据库中的表,如果只为做一两件事而创建结构类型也可能显得太麻烦。

37.对象的一个极大的价值在于:它们模糊(甚至打破)了横亘于基本数据和体积较大的类之间的界限。
 
38. Switch Statements 

39.面向对象程序的一个最明显特征就是:少用switch(或case)语句。
从本质上说,switch语句的问题在于重复。面向对象中多态概念可为此带来解决办法。

40.Parallel Inheritance Hierarchies(平行继承体系)
arallel Inheritance Hierarchies其实也是Shotgun Surgery的特殊情况。在这种情况下,每当你为某个类添加一个子类,必须也为另一个类相应添加一个子类。消除这种重复性的一般策略是:
让一个继承体系的实例引用另一个继承体系的实例。

41.Lazy Class(冗赘类)
   如果一个类不再需要理解它,维护它,它就应该消失。
  
42.对象的基本特征之一就是封装--对外部世界隐藏其内部细节。封装往往伴随委托。
  比如说你问主管是否有时间参加一个会议,他就把这个消息“委托”给他的记事簿,然后才能回答你。你没必要知道这位主管到底使用传统记事簿或电子记事簿等或其他手段来记录自己的约会。

43.如果两个类过于亲密,花费太多时间去探究彼此的private成分。继承往往造成过度亲密,因为子类对超类的了解总是超过后者的主观愿望。如果过度亲密,就让子类立刻继承体系。

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

45.确保所有测试都完全自动化,让它们检查自己的测试结果。

46. 一套测试就是一个强大的bug侦测器,能够大大缩减查找bug所需要的时间。

47.测试夹具。TestCase类提供两个函数专门针对此一用途:setUp()用来产生相关对象,tearDown()负责删除它们。

48.频繁地运行测试,每次编译请把测试页考虑进去----每天至少执行每个测试一次。

49.每当你收到bug报告,请先写一个单元测试来暴露bug。

50.观察类该做的所有事情,然后针对任何一项功能的任何一种可能失败情况,进行测试。
测试应该是一种风险驱动的行为,测试的目的是希望找出现在或未来可能出现的错误。

51.测试的要诀是:测试你最担心出错的部分。

52.编写并不完善的测试并实际运行,好过对完美测试的无尽等待。

53.考虑可能出错的边界条件,把测试火力集中在那儿。

54.当事情被认为应该会出错时,别忘了检查是否抛出了预期的异常。

55.不要因为测试无法捕捉所有bug就不写测试,因为测试的确可以捕捉到大多数bug。

56.对象技术有个微妙处:继承和多态会让测试变得比较困难,因为将有许多种组合需要测试。如果你有3个彼此合作的抽象类,每个抽象类有3个子类,那么你总共拥有9个可供选择的类和27种组合。并不需要测试所有可能组合,但要尽量测试每一个类,这样可以大大减少各种组合所造成的风险。如果这些类之间彼此有合理的独立性,就不用尝试所有组合。

第5章 重构列表
57.重构的记录格式。每个重构手法都有如下五个部分:
1)首先是名称(name)。建造一个重构词汇表,名称是很重要的。
2)名称之后是一个简短概要(summary)。简单介绍此一重构手法的适用情景,以及它所做的事情。
3)动机(motivation)为你介绍“为什么需要这个重构”和“什么情况下不该使用这个重构”。
4)做法(mechanics)简明扼要地一步步介绍如何进行此一重构。
5)范例(examples)以一个十分简单的例子说明此重构手法如何运作。

58.概要包括三个部分:
1)一句话,介绍这个重构能够帮助解决的问题;
2)一段简短陈述,介绍你应该做的事;
3)一副速写图,简单展现重构前后实例。

59.寻找引用点:
很多重构都要求你找到对于某个函数,某个字段或某个类的所有引用点。

第6章  重新组织函数

60. Extract Method(提炼函数)
  你有一段代码可以被组织在一起,并独立出来。
  将这段代码放进一个独立函数中,并让函数名称解释该函数的用途。
 
61.提炼函数的做法
1)创造一个新函数,根据这个函数的意图来对它命名(以它“做什么”来命名,而不是以它“怎么做”命名)。
   即使你想要提炼的代码非常简单,例如只是一条消息或一个函数调用,只要新函数的名称能够以更好方式昭示代码意图,你也应该提炼它。但如果你想不出一个更有意义的名称,就别动。
2) 将提炼出的代码从源函数复制到新建的目标函数中。
3)仔细检查提炼出的代码,看看其中是否引用了“作用域限于源函数”的变量(包括局部变量和源函数参数)。
4)检查是否有“仅用于被提炼代码段”的临时变量。如果有,在目标函数中将它们声明为临时变量。
5)检查被提炼代码段,看看是否有任何局部变量的值被它改变。如果一个临时变量值被修改了,看看是否可以将被提炼代码段处理为一个查询,并将结果赋值给相关变量。如果很难这样做,或如果被修改的变量不止一个,你就不能仅仅将这段代码原封不动地提炼出来。就需要先使用消灭临时变量,然后再尝试提炼。
6)将被提炼代码段中需要读取的局部变量,当作参数传给目标函数。
7)处理完所有局部变量之后,进行编译。
8)在源函数中,将被提炼代码段替换为对目标函数的调用。
   如果你将任何临时变量移到目标函数中,请检查它们原本的声明式是否在被提炼代码段的外围。如果是,现在你可以删除这些声明式了。
9)编译,测试。

改造前
void printOwing(){
 Enumeration e = _orders.elements();
 double outstanding = 0.0;
 
 //print banner
 Syste.out.println("*******************************");
 Syste.out.println("******** Customer Owes ********");
 Syste.out.println("*******************************");
 
 //calculate outstanding
 while(e.hasMoreElements()){
  Order each = (Order) e.nextElement();
  outstanding += each.getAmount();
 }
 
 //print details
 Syste.out.println("name:" + _name);
 Syste.out.println("amount" + outstanding);
}

改造后的代码:
void printOwing(){
 Enumeration e = _orders.elements();
 double outstanding = 0.0;
 
 //1
 printBanner();
 
 //calculate outstanding
 while(e.hasMoreElements()){
  Order each = (Order) e.nextElement();
  outstanding += each.getAmount();
 }
 
 //2
 printDetails();
}

void printBanner(){
    //print banner
 Syste.out.println("*******************************");
 Syste.out.println("******** Customer Owes ********");
 Syste.out.println("*******************************");
}

void printDetails(double outstanding){
 //print details
 Syste.out.println("name:" + _name);
 Syste.out.println("amount" + outstanding);
}

0 0
原创粉丝点击