Netbeans Platform的Lookup類給我們帶來了什么呢?注入依賴和解耦!Lookup的基本用法常常是:
Foo foo = someLookup.lookup(Foo.class)
使用Lookup來查找一個Foo類的實例。你可以將Lookup想成是一個Map,鍵是類,值是實例。
Netbeans中Lookup用途有:
1. 表現一個對象的能力。IS-A關系轉變為HAS-A關系。
想
想傳統的一種解耦方式:將其他模塊創建的對象cast到特定的類型。Netbeans,不這么做,Netbeans做法是讓其他模塊創建的對象實現
Lookup.Provider接口(這個接口只有一個方法:public Lookup
getLookup()),然后通過這個Lookup獲得你需要協同工作的對象。然后你的代碼詢問這個對象是否擁有特定接口的實例:
Your code <-- interact with --> Some object -- implements --> Lookup.Provider
|__ has a Lookup Object -- maintain
|__ Object 1
|__ Object 2
|__ Object 3 ...
這
樣,通過Lookup, Some object 將 IS-A 關系(Some
object實現Object1接口,Object2接口,Object3接口)轉化為HAS-A關系(Some
object的lookup對象擁有Object1, Object2, Object3).
這種轉化具有重要的意義!方便!快捷!動態
(運行時無法改變實現的接口和類型)!彈性(運行時一個對象的能力是可能變化的,但是類型是不能變化的)!例如如果你想保存一個對象,你不必問其是否實現
Saveable接口,你只需要問其是否擁有一個SaveCookie實例(SaveCookie有一個save方法)。
2. 依賴注射和解耦
一個模塊能夠定義多個接口讓其他模塊實現這些接口,這個模塊可以使用Lookup.getDefault()方法獲得全局Lookup,然后根據全局Lookup查找其接口的所有實現。
3. 動態服務發現
模塊能夠非常簡單的將代表全局服務或偽單例的對象通過默認的Lookup進行注射。
單
例模式的的目的是讓一個對象只擁有一個實例。實現這種模式又很多方法,例如最簡單的是創建一個工廠方法,然后將構造器定義為私有的,然后維護一個實例,對
所有的調用(工廠方法)都返回這個實例。單例模式在Netbeans中也有廣泛的應用。通常全局服務沒有必要有多個實例,例如在整個應用程序的主窗口中我
們只需要一個狀態顯式欄實例,來顯式狀態。例如Netbeans的Windows系統API,你不用直接使用它,這個模塊
org.netbeans.core.windows將StatusDisplayer實現注射進來,放在默認的Lookup中,你僅僅使用
StatusDisplayer.getDefault().setStatusText("something").就可以設置狀態信息了。
Lookup.Result 和 Lookup.Template
什么樣的對象需要擁有自己的Lookup呢?
在Netbeans中的一些基本API類中又很多都有getLookup()方法,他們都實現了Lookup.Provider接口。Netbeans中有三個著名的例子:
1.
Project. Project API .
Project實際就是一個將一個目錄和一個Lookup結合在一起,再加點東西的一個對象。Project API
定義了能在一個Project實例的Lookup中出現的(可選的)類。不同的模塊實現了Project,提供自己的實現。其他API定義一些其他的類。
例如Java Project
API定義了一個ClassPathProvider的接口。這個接口能在Java源代碼Project的Lookup中被找到,而其他普通的
Project沒必要有這個接口。整個Project類定義如下:
public interface Project extends Lookup.Provider {
// 維護一個文件對象,代表項目目錄
FileObject getProjectDirectory();
// 維護一個Lookup對象
Lookup getLookup();
}
可以從Project的Lookup中請求多個接口。例如ProjectInformation對象能夠提供Project名稱等基本信息。
2. TopComponent . 頂層組件是Netbeans的窗口系統管理的一個GUI面板。繼承于TopComponent的類可以通過TopComponent.getLookup()方法來操作面板中的選擇等事務。
3. Node. 節點就是通常說得樹-節點類型的對象,代表底層的數據模型。例如項目和文件窗口中的文件樹就是節點樹。org.openide.nodes.Node就有getLookup()方法。
說了Netbeans中著名的三種具有Lookup對象的類,到底什么樣的對象需要Lookup對象呢??
答案是,需要暴露某些能力的對象。這些對象可以通過Lookup對象向外界表明其具備某些能力,從而讓其他代碼能夠使用這些對象的能力。舉個例子,如果你想保存一個文件對象,那么這個文件對象肯定有保存的能力。傳統方式怎么做呢:
public void actionPerformed (ActionEvent e) {
Object o = something.getSelection();
// 你會檢查其是否實現Saveable接口
if (o instanceof Saveable && ((Saveable) o).canSave()) {
((Saveable) o).save();
}
}
可
惜的是這種方法太不強大了。Java對象無法輕易更改他們的類型。因為情況總是在變化的。Java文件包括源文件和編譯文件,有些事情你無法在只有源文件
沒有編譯文件的情況下做,例如執行。并不是所有Java源文件都有對應的編譯文件的。因此Lookup的出現能夠很好的解決這種問題。Java文件的
Lookup內容是可以隨時變化。當Java源文件被編譯后,可以向Java文件的Lookup內容添加一個編譯文件對象,如果Java文件被編譯了,那
么Lookup內容中就保存一個編譯文件對象,如果編譯文件被刪除了,這個編譯對象也從Lookup內容中刪除了。
public void actionPerformed (ActionEvent e) {
Lookup lkp = Utilities.actionsGlobalContext();
SaveCookie save = lkp.lookup (SaveCookie.class);
if (save != null) {
save.save();
}
}
事
實上,Netbeans中一個正在被編輯的文件的保存過程,就是代表這個文件的節點Node的Lookup中出現了一個SaveCookie對象。這個代
碼并不需要知道SaveCookie.save()方法的具體細節,只需要知道SaveCookie對象在Lookup中,就可以保存。
Lookup是一種通訊機制
上
面說得Project就是一個例子,Netbeans中有一個概念叫服務提供者接口(Service Provider Interface,
SPI ). 不同的模塊通過在默認的Lookup中安裝ProjectFactory實例,來插入不同的項目類型project
type.你可以在Netbeans的項目向導中發現你可以創建不同類型的項目。Netbeans包含多個項目接口的實現。
Lookup和代理
ProxyLookup允許將兩個Lookup融合。
Lookup和選擇
那我們如何在程序中使用Lookup呢?
基本上說,我們無需自己編寫Lookup(當然如果需要的話,你可以編寫)。
NetBeans 的 Lookup 可以說是無處不在,它也是 NetBeans 中最基本的模塊。在執行的時候,NetBeans 將Lookup分為系統服務池的Lookup和具有焦點窗口的對象池的Lookup。我們可以從以下地方獲得Lookup:TopComponent 、Node 、Utilities.actionsGlobalContext()
、和 Lookups四個地方。
我
們通過Utilities.actionsGlobalContext()來獲得對象池的Lookup,
這里Netbeans已經幫我們整合過Lookup對象了.Utilities.actionsGlobalContext()將返回一個Lookup對
象,這個對象是活動的(具有焦點)的頂層組件的Lookup的代理Lookup,如果頂層組件是Explorer視圖的話,將代理被選擇了的節點的
Lookup對象們。
我們通過Lookup.getDefault()方式獲得系統服務池的Lookup.
Netbeans平臺為我們準備好了幾種使用方式:
1.
通過Lookups. 如果你需要包含固定數目對象的Lookup,你可以使用Lookups.fixed(Object...
objectsToLookup) (注意:... 代表可變參數數量),也可以使用Lookups.singleton(Object
objectToLookup)返回一個只包含一個對象的Lookup.
2.
AbstractLookup和InstanceContent.
如果你希望你的Lookup可以動態的包含對象(數目可變),那么你可以使用AbstractLookup和InstanceContent相結合的方
式。注意AbstractLookup并不是抽象類。具體的用法這里有個很好的例子,Fox的Lookup講解:
在
這個例子中,有一個業務對象FoxObject,
有兩個頂層組件:Consumer頂層組件和Producer頂層組件。Producer頂層組件中包含兩個狀態按鈕
(ToggleButton):FoxButton和DonButton。Consumer頂層組件有一個List表單。
效果是:如果你
按下Producer頂層組件窗口中的其中一個Button,就會創建一個FoxObject對象,其名字根據按鈕的名字命名,例如按下
DonButton將創建一個名字為Don的FoxObject.
如果程序的焦點在Producer頂層組件上(藍色背景意味焦點在其上,灰色背景意味焦點不在其上)的話,Consumer頂層組件的List表單將出現
這個名字為Don的FoxObject,
當你再次按下DonButton時,按鈕的狀態改變,名字為Don的FoxObject被刪除,List表單中相應的對象也消失了。如果你將程序焦點換到
Consumer頂層組件的時候,List表單中的所有對象都消失了,重新將焦點換到Producer頂層組件上時,List表單中的對象重新出現了。
這是怎么實現的呢?我們看一下:
1. 我們在Producer頂層組件中維護一個InstanceContent對象,兩個FoxObject對象。在Producer頂層組件構造的時候,我們初始化Lookup:
private void initLookup(){
// 創建InstanceContent,然后構建一個AbstractLookup,然后將這個Lookup和Producer頂層組件關聯
m_InstancePool=new InstanceContent();
this.associateLookup(new AbstractLookup(m_InstancePool));
}
在ToggleButton狀態變化方法中,我們根據Button的狀態,決定InstanceContent的內容(名為Don的FoxObject對象的添加和刪除)
private void m_PutDonItemStateChanged(java.awt.event.ItemEvent evt) {
if(evt.getStateChange()==ItemEvent.SELECTED){
if(m_Don==null){
m_Don=FoxObject.createInstance("don", "Don, Chen");
}
m_InstancePool.add(m_Don);
}else if(evt.getStateChange()==ItemEvent.DESELECTED){
if(m_Don!=null){
m_InstancePool.remove(m_Don);
}
}
}
2. 我們讓Consumer頂層組件實現LookupLisenter, 因為他要根據FoxObject對象的狀態來顯式List表單。他維護一個JList表單以及一個Lookup.Result結果。
final class ConsumerTopComponent extends TopComponent implements LookupListener {...
當Consumer頂層組件被打開和關閉時:
public void componentOpened() {
clearList();
// 初始化LookupQuery
initLookupQuery();
FoxService oService=(FoxService) Lookup.getDefault().lookup(FoxService.class);
if(oService !=null ){
oService.saySomething();
}
}
public void componentClosed() {
// 關閉LookupQuery
uninitLookupQuery();
clearList();
}
我
們看一下,在Consumer頂層組件窗口被打開時,我們根據FoxObject類制作一個模版,然后通過
Utilities.actionsGlobalContext()查找到所有FoxObject的實例,以Lookup.Result方式展現。找到這
些FoxObject實例后,對代表他們的Lookup.Result添加監聽器,就是Consumer頂層組件自己。然后更新結果列表。
private void initLookupQuery(){
Lookup.Template oFoxTemplate=new Lookup.Template(FoxObject.class);
m_LookupResult=Utilities.actionsGlobalContext().lookup(oFoxTemplate);
m_LookupResult.addLookupListener(this);
refreshResultList();
}
private void uninitLookupQuery(){
if(m_LookupResult!=null){
m_LookupResult.removeLookupListener(this);
m_LookupResult=null;
}
}
實現Lookup監聽器的方法:
public void resultChanged(LookupEvent ev){
refreshResultList();
}
一旦Lookup變化了,resultChanged方法就被調用,執行refreshResultList方法。這個方法獲得Lookup.Result中的所有實例,如果有實例的話,重新根據這些實例繪制Consumer頂層組件的List表單。
private void refreshResultList(){
Collection<FoxObject> cList=m_LookupResult.allInstances();
if(!cList.isEmpty()){
this.m_FoxObjectList.setListData(new Vector<FoxObject>(cList));
this.m_FoxObjectList.revalidate();
this.m_FoxObjectList.repaint();
}else{
clearList();
}
}
整個演示就完成了。這里我們特別要注意幾點:
1. FoxObject是業務對象,它僅僅關注自己的業務和信息,例如名字,ID等,對其他的事情一概不知。
2. Producer頂層組件需要和一個Lookup關聯,這個Lookup是AbstractLookup和InstanceContent共同完成的,以便Lookup內的對象數目可以動態改變(通過ToggleButton的狀態):
ProducerTopComponent --> AbstractLookup --> InstanceContent --> FoxObject Class
|
|__ Add/Remove --> Fox FoxObject <--> Button1
|
|__ Add/Remove --> Don FoxObject <-->
Button2
|
| 關鍵:Netbeans通過Utilities.actionsGlobalContext將
| Producer頂層組件和Consumer頂層組件解耦,相互通過Lookup溝通
ConsumerTopComponent --> Utilities.actionsGlobalContext().lookup(FoxObject.class)
|______ 監聽 --> |__ Lookup.Result
|______ 更新 --> JList (根據Lookup.Result的變化)
3. 結構圖
<-------------- FoxObject --------------->
|
| |
| <----- Lookup (Utilitis, AbstractLookup, InstanceConent) ------> |
| |
| |
ConsumerTopComponent XXX ProducerTopComponent
轉自http://alarnan.spaces.live.com/blog/cns!819cbc613de169ef!141.entry