本文是在学习中的总结,欢迎转载但请注明出处:http://blog.csdn.net/pistolove/article/details/44958839
在上一篇文章中介绍了“引入外加函数”。本文将介绍“引入本地扩展”这种重构手法。
下面让我们来学习这种重构手法吧。
开门见山
发现:你需要为服务类提供一些额外函数,但你无法修改这个类。
解决:建立一个新类,使它包含这些额外函数。让这个扩展品成为源类的子类或者包装类。
动机
我们都无法预知一个类的未来,它们常常无法为你预先准备一些有用的函数。如果可以修改源码,那就太好了,那样就可以直接加入自己需要的函数。但是你经常无法修改源码。如果只是需要一两个函数,可以引入外加函数进行处理。但如果需要多个函数,外加函数就很难控制它们了。所以,需要将这这些函数组织起来,放到一个恰当的地方去。要达到这样的目的,需要用到子类化和包装这两种技术。这种情况下,把子类或包装类统称为本地扩展。
本地扩展是一个独立的类,但也是被扩展的子类型:它提供类的一切资源特性,同时额外添加新特性。在任何使用源类的地方,你都可以使用本地扩展取而代之。
使用本地扩展使得以坚持“函数和数据应该被统一封装”的原则。如果你一直把本该放在扩展类中的代码零散地放置于其它类中,最终只会让其它类变得复杂,并使得其中函数难以被复用。
在子类和包装类之间做选择,通常会选择子类,因为这样的工作量比较小。但是,制作子类的最大障碍在于,它必须在对象创建初期实施。如果可以接管对象的创建过程,那当然没问题;但如果你想在对象创建之后再使用本地扩展,就会有问题。此外,子类化方案还必须产生一个子类对象,这样如果有其它对象引用了旧对象,就同时有两个对象保存了原数据!如果原数据不可修改,那可以放心复制;但是如果允许修改,问题就随之而来,因为一个修改动作无法同时改变两份副本。这时就必须改用包装类。使用包装类时,对本地扩展的修改会波及原对象,反之也成立。
做法
(1)建立一个扩展类,将它作为原始类的子类或者包装类。
(2)在扩展类中加入转型构造函数。(所谓“转型构造函数”是指“接受原对象作为参数”的构造函数)
(3)在扩展类中加入新特性。
(4)根据需要,将原始对象替换为扩展对象。
(5)将针对原始类定义的所有外加函数版移到扩展类中。
示例
我们以JAVA中的Date类为例。Java已经提供了我们想要的功能,但是在到来之前,很多时候需要扩展Date类。
第一件需要做的事情就是:使用子类还是包装类。子类化是比较显而易见的方法:
class MyDateSub extends Date{ public MyDateSub nextDay()... public int dayOfYear()... }
包装类则需要用上委托:
class MyDateWrap{ private Date _original; }
范例:使用子类
首先,要建立一个MfDateSub类来表示“日期”,并使其成为Date的子类:
class MyDateSub extends Date
然后,需要处理Date和扩展类之间的不同处。MfDateSub构造函数需要委托给Date构造函数:
public MyDateSub(String dateStr){ super(dateStr); }
现在,需要加入一个转型构造函数,其参数是一个源类的对象:
public MyDateSub(Date arg){ super(arg.getTime()); }
现在,可以再扩展类中添加新特性,并使用搬移函数将所有的外加函数搬移到扩展类。于是:
client class... private static Date nextDay(Date arg){ // foreign method, should be on date return new Date(arg.getYear(),arg.getMonth(),arg.getDate()+1); }
经过搬移之后,就变成:
class MyDateSub... Date nextDay(){ return new Date(getYear(),getMonth(),getDate()+1); }
范例:使用包装类
首先声明一个包装类,使用包装类时,对构造函数的设定与先前有所不同。现在的构造函数将只执行一个单纯的委托动作:
class MyDateWrap{ private Date _original; }
public MyDateWrap(String dateStr){ _original = new Date(dateStr); }
而转型构造函数则只是对其实例变量赋值而已:
public MyDateWrap(Date arg){ __original = arg; }
接下来是一项枯燥乏味的工作:为原始类的所有函数提供委托函数。此处只展示两个函数:
public int getYear(){ return original.getYear(); } public boolean equals(Object arg){ if(this==arg){ return true; } if(!(arg instanceof MyDateWrap )){ return false; } MyDateWrap other = (MyDateWrap)arg; return (_original.equals(other._original)); }
完成这项工作之后,可以使用搬移函数将日期相关行为搬移到新类中。于是:
client class... private static Date nextDay(Date arg){ // foreign method, should be on date return new Date(arg.getYear(),arg.getMonth(),arg.getDate()+1); }
经过搬移之后,有:
class MyDateWrap... Date nextDay(){ return new Date(getYear(),getMonth(),getDate()+1); }
使用包装类有一个特殊问题:如何处理“接受原始类之实例为参数”的函数?
例如:
public boolean after(Date arg)
由于无法改变原始类,所以我只能做到在一个方向上的兼容——包装类的after()函数可以接受包装类或原始类的对象;但原始类的after()函数只能接受原始类对象,不接受包装类对象:
aWrapper.after(aDate); //can be made to work aWrapper.after(anotherWrapper); //can be made to work aDate.after(aWrapper); //not work
这样覆写的目的是为了向用户隐藏包装类的存在。这是一个比较好的策略,因为包装类的用户的确不应该关心
包装类的存在,的确应该可以同样地对待包装类和原始类。但是我无法隐藏包装类的存在,因为某些系统提供的函数
(例如equals())会出问题的。可能你会认为:在MyDateWrap类中覆写equals(),像这样:
public boolean equals(Date arg) //causes problems
但是这样做很危险,尽管达到了我的目的,但JAVA系统的其它部分认为equals()符合交换律:如果a.equals(b)为真,那么b.equals(a)也必为真。违反这一规则将使我遭遇一大堆莫名其妙的错误。要避免这种情况,唯一的办法就是修改Date类。但是如果我修改Date类,又何必进行此项重构呢?所以,这种情况下,只能向用户公开“我进行了包装”这一事实。将以一个新函数来进行日期之间的相等性检查:
public boolean equalsDate(Date arg)
可以重载equalsDate(),让一个重载版本接受Date对象,另一个重载版本接受MyDateWrap对象。这样就不必要检查未知对象类型了:
public boolean equalsDate(MyDateWrap arg)
子类化方案中就没有这样的问题,只要不覆写原函数就行。但如果覆写原始类中的函数,那么寻找函数时,会被搞的晕头转向的。一般不会再扩展类中覆写原始类的函数,只会添加新函数。
本文主要介绍了重构手法——引入本地扩展。该手法比较简单,很容易就能够理解,这里就不累赘了。
最后,希望本文对你有所帮助。有问题可以留言,谢谢。(PS:下一节将介绍重构笔记——重新组织函数)