3.5 Divergent Change ( 发散式变化)
针对某一外界变化的所有相应修改,都只应该发生在单一class中,而这个新class内的所有内容都应该反映外界变化。为此,你应该找出因为某特定原因而造成的所有变化,然后运用Extract Class将他们提炼到另一个class中。
3.6Shotgun Surgery(散弹式修改)
如果每遇到某种变化,你都必须在许多不同的classes内作出许多小修改以响应之,你所面临的坏味道就是散弹式修改。
这种情况下,你应该使用Move Method和Move Field把所有需要修改的代码放进同一个class。如果眼下没有合适的class可以安置这些代码,就创造一个。通常你可以运用Inline Class把一系列相关行为放进同一个class。这可能会造成少量发散式变化,但你可以轻易处理它。
InLine Class
场景:你的某个class没有做太多事情(没有承担足够责任)。
将class的所有特性搬移到另一个class中,然后移除原class。
作法
在移动的目的class身上声明所有被移动的class的公共属性和方法。(如果“以一个独立接口表示source class函数”更合适的话,就应该在inlining之前使用Extract Interface)
修改所有source class引用点,改而引用吸收被移动的class的目标class。(将source class声明为private,以斩断package之外的所有引用可能。同时并修改source class的名称,这便可以使编译器帮助你捕获到所有对于source class的dangling reference(虚悬引用点)
编译,测试
运用Move Method和Move Field,将source class的特性全部搬移到目标class。
3.7Feature Envy(依恋情结)
有一种经典的气味是:函数对某个class的兴趣高过对自己所处之host class的兴趣。这种倾慕之情最通常的焦点便是数据。无数次经验里,我们看到某个函数为了计算某值,从另一个对象那儿调用几乎半打的取值函数。疗法显而易见:把这个函数移至另一个地点。你应该使用Move Method,把它移到它该去的地方。有时候函数只有一部分受这种依恋之苦,这时候你应该使用Extract Method把这一部分提炼到独立的函数中,再使用Move Method带它去它的梦中家园。
往往一个函数会用上数个classes特性。那么它究竟该被置于何处呢?我们的原则是:判断哪个class拥有最多“被此函数使用”的数据,然后就把这个函数和那些数据摆在一起。如果先以Extract Method将这个函数分解为数个较小函数并分别放置于不同地点,上述步骤也就比较容易完成了。
3.8Data Clumps(数据泥团)
将“总是绑在一起出现的数据”放进属于它们自己的对象中。首先找出这些数据的值域形式出现点,运用Extract Class将它们提炼到一个独立的对象中。然后运用Introduce Parameter Object 或 Preserve Whole Object为参数过长的成员函数减肥。
一个好的评判数据泥团的办法:删除掉众多数据中的一笔。其他数据如果不再有意义,这就是个明确的信号:你应该为它们产生一个新对象。
3.9Primitive Obsession(基本型别偏执)
对象技术的新手通常不愿意在小任务上运用小对象--像是结合数值和币值的money class、含一个起始值和一个结束值得range class、电话号码或邮政编码(ZIP)等等的特殊strings。你可以运用Replace Data Value with Object将原本单独存在的数值替换为对象。如果欲替换之数值是type code(型别码),而它不影响行为,你可以运用Replace Type Code with Class将它换掉。如果你有相依于此type code的条件式,可运用Replace Type Code with SubClass或Replace Type Code with State/Strategy 加以处理。
如果你有一组应该总是放在一起的值域,可运用Extract Class。如果你在参数列中看到基本型数据,不妨试试Introduce Parameter Object。如果你发现自己正在从array中挑选数据,可运用Replace Array with Object。
Replace Type Code with Class
在以C为基础的编程语言中,type code(型别码)或枚举值很常见。如果带着一个有意义的符号名,type code的可读性还是不错的。问题在于,符号名终究只是个别名,编译器看见的、进行型别检验的,还是背后那个数值。任何接受type code作为引数(argument)的函数,所期望的实际上是一个数值,无法强制使用符号名。这会大大降低代码的可读性,从而成为臭虫之源。
如果把那样的数值转换为一个class,编译器就可以对这个class进行型别检验。只要为这个class提供factory method,你就可以始终保证只有合法的实体才会被创建出来,而且它们都回被传递给正确的宿主对象。
但是如果枚举被switch所使用,你就不能简单地将它替换为class。因为switch往往只会接受整型数据。所以应该首先对switch运用Replace Conditional with Polymorphism重构,而在这之前,还要进行Replace Type Code with Subclasses或Replace Type Code with State/Strategy把type code处理掉。
作法:
1、为 type code建立一个class。其中包含相应的用于记录type code的值域,并准备一组static变量保存允许被创建的实体,并以一个static函数根据原本的type code 返回合适的实体。
2、修改source class代码,让他使用上述新建的class。
3、编译、测试
4、对于source class中每一个使用type code 的函数,相应建立一个函数,让新函数使用新的class。你需要建立“以新class实体为自变量”的函数,用以替换原先“直接以type code为引数”的函数。你还需要建立一个“返回新class实体”的函数,用以替换原先一直返回type code的函数。建立新函数前,你可以使用Rename Method修改原函数名称,明确指出那些函数仍然使用旧式的type code。
5、逐一修改source class用户,让它们使用新接口。
6、每修改一个用户,编译并测试。
7、删除使用“type code”的接口,并删除保存旧type code的静态变量。
8、编译测试。
Replace Type Code with SubClass
我的理解:
![](http://www.cppblog.com/images/cppblog_com/qzb2006e/cls_room1.JPG)
在这里Room类包含一个属性IsMeetingRoom。如果为true,则这个房间就是一个会议室,如果为false,则这个房间就是一个普通的房间。这里true和false可以理解为一个简单的“型别编码”。它可以重构为下面的形式:
![](http://www.cppblog.com/images/cppblog_com/qzb2006e/cls_room2.JPG)
更复杂的情况:
![](http://www.cppblog.com/images/cppblog_com/qzb2006e/cls_room3.JPG)
这样就需要根据RoomType的实际数量分离出多个子类来。
作法:
使用Self Encapsulate Field方法把type code自我封装起来。如果type code被传递给构造函数,你就需要将构造函数换成factory method.
为type code的每一个数值建立一个相应的subclass。在每个subclass中覆写type code的取值函数(getter),使其返回相应的type code值。
每建立一个新的subclass,编译并测试。
从superclass中删除掉保存type code的值域。将type code访问函数声明为抽象函数。
编译,测试。
Replace Type Code with State/Strategy
你有一个type code,它会影响class的行为,但你无法使用subclassing。
以state object(专门用来描述状态的对象)取代type code。
之所以无法subclassing是因为Type Code会在对象生命周期过程中发生变化。
如果你打算再完成本项重构之后再以Replace Conditional with Polymorphism(用多态替换条件判断)简化一个算法,那么选择Strategy模式比较合适;如果你打算搬移与状态有关的数据,而且你把新建对象视为一种变迁状态,就应该使用State模式。
Replace Array with Object
你有一个数组(array),其中的元素各自代表不同的东西。
以对象替换数组,对于数组中的每个元素,以一个值域表示之。
String[] row =new String[3];
row[0]="Liverpool";
row[1]="15";
转换为
Performance row=new Performance();
row.setName("Liverpool");
row.setWins("15");
3.10Switch Statements
如果你只是在单一函数中有些选择事例,而你并不想改动它们,那么“多态”就有点杀鸡用牛刀了。这种情况下Replace Paremeter with Explicit Method是个不错的选择。如果你的选择条件之一是null,可以试试Introduce Null Object
Replace Paremeter with Explicit Method
此方法即是将包含多个switch分支的函数的各个分支独立成不同的函数。比如:
public static Object CreateObject(int typeOfObject)
{
switch(typeOfObject)
{
case 1:return new A();
case 2:return new B();
case 3:return new C();
}
}
转换为:
public static Object Create1()
{
return new A();
}
public static Object Create2()
{
return new B();
}
public static Object Create3()
{
return new C();
}
思考:这样会削弱此函数的运行期特性。
Introduce Null Object
使用这种重构方法的好处就是你不用在向一个对象发送消息的时候去检验这个对象是否存在。因为对象确实存在,只不过有可能对你发送的消息做出“我是空”的回应。
作法
为source class建立一个subclass,使其行为像source class的null版本。在source class和null class中都加上IsNull()函数,前者的IsNull()应该返回false,后者的应该返回true.可以建立一个nullable接口,将isnull函数放在其中,让source class实现这个接口。
另外可以创建一个testing接口,专门用来检验对象是否为null.
编译。
找出所有“索求source obejct却得到一个null”的地方。修改这些地方,使他们改而获得一个null object。
找出所有“讲source object与null做比较的地方”。修改这些地方,使它们调用IsNull()函数。
你可以在不该在出现null value的地方放上一些断言,确保null的确不再出现。这可能对你有所帮助。
这出这样的程序点:如果对象不是null,做A动作,否则做B动作。
对于每一个上述地点,在null class中覆写A动作,使其行为和B动作相同。
使用上述的被覆写动作(A),然后删除“对象是否等于null”的条件测试。编译并测试。
其他特殊情况
使用本项重构时,你可以有数种不同的null objects,例如你可以说“没有顾客”和“不知名顾客”这两种情况是不同的。果真如此,你可以针对不同的情况建立不同的null class。有时候null objects 也可以携带数据,例如不知名顾客的使用记录等等。
本质上讲,这是一个比Null Object模式更大的模式:Special Case模式。所谓Special case class是某个class的特殊情况,有着特殊的行为。因此表述不知名顾客的UnKnownCustomer 和表示没有顾客的NoCustomer都是Customer的特例。你经常可以在表示数量的class中看到这样的特例类。例如Java浮点数有“正无穷大”、“负无穷大”和“非数量”等特例。special case class的价值是:它们可以降低你的错误处理开销。例如浮点运算决不会抛出异常。如果你对NaN作浮点运算,结果也会是个NaN。这和“null object”的访问函数通常会返回另一个null object是一样的道理。
posted on 2007-07-17 21:26
littlegai 阅读(279)
评论(0) 编辑 收藏 引用 所属分类:
我的读书笔记