Extension Object同Visitor、Decorator等模式差不多,都是在不改變類層次機構的前提下向系統添加功能,被稱為擴展對象模式。
在了解擴展對象模式之前需要了解一下相關的一條OOD原則,就是單一職責原則(Single Responsibility Principle,SRP)。
就一個類而言,應該僅有一個引起他變化的原因。
大致的意思就是要把職責分配到不同的對象當中去。如果我們把每一個職責的變化看成是變化的一個軸線,當需求變化時,該變化會反映為類的職責的變化。 如果一個類承擔了多余一個的職責,那么引起他變化的原因就多余一個軸線,也就是把這些職責耦合到了一起。在某種程度上,這種耦合會導致Fragile Design,當變化發生時,設計會遭受到意向不到的破壞。
在具體討論之前,先來看一個設計問題。
我們要開始設計一個RSS Reader,一般使用一棵樹來呈現對于Feed的管理和組織結構,類似于Bloglines、GreatNews那樣的。構建這棵樹的過程中,我們可以使用Composite模式來設計相應的類層次結構,例如,基本接口我們可以采用
public interface IChannelNode {
public String getTitle();
public String getId();
... ...
}
然后構建Composite的子類(用來容納目錄等組織結構)和具體類,例如
public class CompositeChannelNode implements IChannelNode {
public void addChild(IChannelNode node);
... ...
}
public class FeedChannelNode implements IChannelNode {
... ...
}
上述這些類結構完全可以用來描述這棵樹的層次結構了,而且我們可以使用Eclipse中的TreeViewer來顯示。Eclipse中的 TreeViewer是基于Structured Model的,所以,幾乎使用很少的代碼,就可以完成顯示這個樹的功能,這也是Eclipse對于這種基于Structured Model封裝的比較好的結果。不過簡單的使用UI會比較單調,所以,就需要對于UI部分進行潤色,那么在這個類層次中不可避免的要加入UI部分的代碼, 這就不符合SRP的要求了。另外,假如我們還要完成對于這棵樹的persistent,那么序列化的代碼又會污染這個單純的模型了。這里我們就可以使用擴 展對象模式了,把這些變化另外wrap起來。
對Extension的抽象封裝
這個是Uncle Bob提過的一種優雅的實現機制,是基于對于Extension的抽象封裝,然后在本身模型(上面的Channel組織模型)注冊這些 Extension,在運行時可以方便的得到合適的擴展來執行相應功能。例如我們來定義一個Extension的注冊和使用機制,然后在 IChannelNode中將請求delegate給這套機制,當然也可以把這個直接集成到IChannelNode的代碼中,下面用Part和 PartExtension來完成這部分功能(代碼不是完整的)。
public abstract class Part {
public void addExtension(String extensionType, PartExtension extension) { ... ... }
public PartExtension getExtension(String extensionType) { ... ... }
}
public interface PartExtension {
}
可以簡單的使用一個HashMap來完成extension的注冊和獲取。其實這部分的代碼就把擴展這部分不變的內容封裝起來,而對于可能存在的變 化完全可以利用增加新的類來完成,也就滿足了開放封閉原則(OCP)。比如我們說到的persistent部分,建立相應的persistent擴展就可 以了。
public class PersistentPart extends Part {
public PersistentPart() {
addExtension("CSVFormat", new CSVPersistentPartExtension());
// 注冊其他format的存儲方式
}
}
public interface PersistentExtension extends PartExtension {
public void doPersistent(IChannelNode node);
}
public class CSVPersistentPartExtension implements PersistentExtension {
public void doPersistent(IChannelNode node) {
// save node.getTitle();
// save node.getId();
......
}
}
變化的部分就是用什么方式來存儲信息了,比如我們不使用CVS來存儲而改用XML存儲,那么只需要生成相應的XMLPersistentPartExtension,并把他注冊到PersistentPart就可以了。
看了這個,當時我覺得搞得真是很麻煩,很復雜,而且可讀性很差啊,呵呵。不過本身Extension Object的實現就是特別復雜的,慢慢實踐,我想會越來越掌握這種方法的。
我投入精力更大的是下面要說的一種同Adapter結合起來的擴展方法,并使用這種方法實現了UI部分的分離,感覺確實方便很多,不過開始理解起來也是比較頭疼。
使用Adapter模式來擴展
這里要說的是Eclipse平臺底層最為核心的IAdaptable,Erich
Gamma一手打造,我先后看了好久才體會到這種設計的魅力。這次擴展我們拿UI來說事。Eclipse中顯示一個Tree很容易,有了
Structured
Model之后,打造好相應的TreeContentProvider和LabelContentProvider就基本完成了。但是對于一個效果復雜的
顯示在Provider中的代碼就會顯的很臃腫,主要也是在LabelContentProvider中,而原有模型的邏輯也會增加很多基于UI部分的代
碼,比如Channel不同狀態下要顯示什么樣的圖片,使用什么樣的字體,什么樣的前景背景色等等。所以,比較好的辦法就是分離出一套并行的類層次,
IChannelNodeUI,而且如果在擴展過程中我們能夠把IChannelNode適配到IChannelNodeUI,并交給
LabelContentProvider就好辦了,所有的UI代碼就搞到IChannelNodeUI這套東西里面去了。
現在介紹的擴展方式就是依據Adapter為基礎的,所以,現在先把Adapter這部分搞定
public interface IChannelNodeUI {
public Image getImage();
public Font getFont();
... ...
}
public class ChannelNodeUIAdapter implements IChannelNodeUI {
private IChannelNode node_;
public ChannelNodeUIAdapter(IChannelNode node) { ... ... }
public Image getImage() {
if (node_.isLoading())
// return loading image;
... ...
}
......
}
接下來也就是對這些擴展的管理,比如我們還有對于IPersistent的適配,那么如何更好的來管理這些擴展呢。比較容易想到的還是注冊然后獲取這樣的方法,可是這些適配器和適配關系如何管理呢?
看一下,Erich大牛的IAdaptable
org.eclipse.core.runtime/IAdaptable
public interface IAdaptable {
public Object getAdapter(Class adapter);
}
如果IChannelNode從IAdaptable繼承過來,那么我們就可以在LabelContentProvider中這樣寫到(都沒有進行check啊):
public Image getImage(Object element) {
IChannelNode node = (IChannelNode)element;
IChannelNodeUI uiNode = (IChannelNodeUI)node.getAdapter(IChannelNodeUI.class);
return uiNode.getImage();
}
那么就需要我們對于IChannelNode中的getAdapter來實現,比如我們在ChannelNode中:
public Object getAdapter(Class adapter) {
if(adapter == IChannelNodeUI.class)
return new ChannelNodeUIAdapter(this);
return null;
}
這樣的問題就是不好維護,所以,Erich大牛還提供了對應的IAdapterFactory和IAdapterManager,當然AdapterManager是Platform中提供的對于框架部分的封裝,不需要我們干什么了。
我們也就需要生成一個AdapterFactory,然后把他注冊給Platform的AdapterManager就可以了:
IAdapterManager manager = Platform.getAdapterManager();
IAdapterFactory uiFactory = new ChannelNodeUIFactory();
manager.registerAdapters(uiFactory, IChannelNode.class);
那么如何使用呢,Eclipse的Platform里提供了一個PlatformObject的類,繼承他直接就具有了Adaptable的能力, 并自動把getAdapter的請求轉交給Platform了,缺陷就是會影響你的繼承體系,畢竟Java無法多元繼承,所以,如果你不是繼承自 PlatformObject的話,自己轉交一下,
Platform.getAdapterManager().getAdapter(this, IChannelNodeUI.class);
這種擴展對象模式就是基于這樣的Adapter模式,由IAdaptable、IAdapterFactory和IAdapterManager完成了對框架的封裝,從而給Eclipse帶來了巨大的靈活性。
參考數目:《敏捷軟件開發》、《Contributing to Eclipse》