💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
### Introduce Local Extension(引入本地扩展) 你所使用的server class需要一些额外函数,但你无法修改这个class。 建立一个新class,使它包含这些额外函数。让这个扩展品成为source class的subclass (子类〕或wrapper(外覆类)。 ![](https://box.kancloud.cn/2016-08-15_57b1b56cab71a.gif) **动机(Motivation)** 很遗憾,classes的作者无法预知未来,他们常常没能为你预先准备一些有用的函数。如果你可以修改源码,最好的办法就是直接加入自己需要的函数。但你经常无法修改源码。如果只需要一两个函数,你可以使用Introduce Foreign Method。 但如果你需要的额外函数超过两个,外加函数(foreign methods)就很难控制住它 们了。所以,你需要将这些函数组织在一起,放到一个恰当地方去。要达到这一目 的,标准对象技术subclassing和wrapping是显而易见的办法。这种情况下我把 subclass 或wrapper称为local extention(本地扩展〕。 所谓local extention是一个独立的class,但也是其extended class的subtype(译注: 这里的subtype不同于subclass;它和extended class并不一定存在严格的继承关系,只要能够提供extended class的所有特性即可)。这意味它提供original class的一切特性,同时并额外添加新特性。在任何使用original class的地方,你都可以使用local extention取而代之。 使用local extention(本地扩展)使你得以坚持「函数和数据应该被包装在形式良好 的单元内」这一原则。如果你一直把本该放在extended class 中的代码零散放置于其他classes中,最终只会让其他这些classes变得过分复杂,并使得其中函数难以被复用。 在subclass和wrapper之间做选择时,我通常首选subclass,因为这样的工作量比较少。制作subclass的最大障碍在于,它必须在对象创建期(object-createion time)实施。如果我可以接管对象创建过程,那当然没问题;但如果你想在对象创建之后再使用local extention ;就有问题了。此外,"subclassing"还迫使我必须产生一个subclass对象,这种情况下如果有其他对象引用了旧对象,我们就同时有两个对象保存了原数据!如果原数据是不可修改的(immutable),那也没问题,我可以放心进行拷贝;但如果原数据允许被修改,问题就来了,因为这时候闹了双包,一个修改动作无法同时改变两份拷贝。这时候我就必须改用wrapper。但使用wrapper时, 对local extention的修改会波及原物(original),反之亦然。 **作法(Mechanics)** - 建立一个extension class,将它作为原物(原类〉的subclass或wrapper。 - 在extension class 中加入转型构造函数(converting constructors )。 - 所谓「转型构造函数」是指接受原物(original)作为参数。如果你釆用subclassing方案,那么转型构造函数应该调用适当的subclass构造函数;如果你采用wrapper方案,那么转型构造函数应该将它所获得之引数(argument)赋值给「用以保存委托关系(delegate)」的那个值域。 - 在extension class中加入新特性。 - 根据需要,将原物(original)替换为扩展物(extension)。 - 将「针对原始类(original class)而定义的所有外加函数(foreign methods)」 搬移到扩展类extension中。 **范例(Examples)** 我将以Java 1.0.1的Date class为例。Java 1.1已经提供了我想要的功能,但是在它到来之前的那段日子,很多时候我需要扩展Java 1.0.1的Date class。 第一件待决事项就是使用subclass或wrapper。subclassing是比较显而易见的办法: ~~~ Class mfDate extends Date { public nextDay()... public dayOfYear()... ~~~ wrapper则需要用上委托(delegation): ~~~ class mfDate { private Date _original; ~~~ **范例:是用Subclass(子类)** 首先,我要建立一个新的MfDateSub class来表示「日期」(译注:"Mf"是作者Martin Fowler的姓名缩写),并使其成为Date的subclass: ~~~ class MfDateSub extends Date ~~~ 然后,我需要处理Date 和我的extension class之间的不同处。MfDateSub 构造函数需要委托(delegating)给Date构造函数: ~~~ public MfDateSub (String dateString) { super (dateString); }; ~~~ 现在,我需要加入一个转型构造函数,其参数是一个隶属原类的对象: ~~~ public MfDateSub (Date arg) { super (arg.getTime()); } ~~~ 现在,我可以在extension class中添加新特性,并使用Move Method 将所有外加函数(foreign methods)搬移到extension class。于是,下面的代码: ~~~ 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 MfDate... Date nextDay() { return new Date (getYear(),getMonth(), getDate() + 1); } ~~~ **范例:是用wrapper(外覆类)** 首先声明一个wrapping class: ~~~ class mfDate { private Date _original; } ~~~ 使用wrapping方案时,我对构造函数的设定与先前有所不同。现在的构造函数将只是执行一个单纯的委托动作(delegation): ~~~ public MfDateWrap (String dateString) { _original = new Date(dateString); }; ~~~ 而转型构造函数则只是对其instance变量赋值而己: ~~~ public MfDateWrap (Date arg) { _original = arg; } ~~~ 接下来是一项枯燥乏味的工作:为原始类的所有函数提供委托函数。我只展示两个函数,其他函数的处理依此类推。 ~~~ public int getYear() { return _original.getYear(); } public boolean equals (MfDateWrap arg) { return (toDate().equals(arg.toDate())); } ~~~ 完成这项工作之后,我就可以后使用Move Method 将日期相关行为搬移到新class中。于是以下代码: ~~~ 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 MfDate... Date nextDay() { return new Date (getYear(),getMonth(), getDate() + 1); } ~~~ 使用wrappers有一个特殊问题:如何处理「接受原始类之实体为参数」的函数?例如: public boolean after (Date arg) 由于无法改变原始类〔original),所以我只能以一种方式使用上述的after() : ~~~ aWrapper.after(aDate) // can be made to work aWrapper.after(anotherWrapper) // can be made to work aDate.after(aWrapper) // will not work ~~~ 这样覆写(overridden)的目的是为了向用户隐藏wrapper 的存在。这是一个好策略,因为wrapper 的用户的确不应该关心wrapper 的存在,的确应该可以同样地对待wrapper(外覆类)和orignal((原始类)。但是我无法完全隐藏此一信息,因为某些系统所提供的函数(例如equals() 会出问题。你可能会认为:你可以在MfDateWrap class 中覆写equals(),像这样: ~~~ public boolean equals (Date arg) // causes problems ~~~ 但这样做是危险的,因为尽管我达到了自己的目的,Java 系统的其他部分都认为equals() 符合交换律:如果a.equals(b)为真,那么b.equals(a)也必为真。违反这一规则将使我遭遇一大堆莫名其妙的错误。要避免这样的尴尬境地,惟一办法就是修改Date class。但如果我能够修改Date ,我又何必进行此项重构?所以,在这种情况下,我只能(必须〕向用户暴露「我进行了包装」这一事实。我将以一个新函数来进行日期之间的相等性检查(equality tests): ~~~ public boolean equalsDate (Date arg) ~~~ 我可以重载equalsDate() ,让一个重载版本接受Date 对象,另一个重载版本接受MfDateWrap 对象。这样我就不必检查未知对象的型别了: ~~~ public boolean equalsDate (MfDateWrap arg) ~~~ subclassing方案中就没有这样的问题,只要我不覆写原函数就行了。但如果我覆写了original class 中的函数,那么寻找函数时,我会被搞得晕头转向。一般来说,我不会在extension class 中覆写0original class 的函数,我只会添加新函数。 译注:equality(相等性)是一个很基础的大题目。《Effective Java》 by Joshua Bloch 第3章,以及《Practical Java》by Peter Haggar 第2章,对此均有很深入的讨论。这两本书对于其他的基础大题目如Serizable,Comparable,Cloneable,hashCode() 也都有深刻讨论。