AOP:通過面向方面編程提高代碼的封裝和複用性

AOP:通過面向方面編程提高代碼的封裝和複用性

發佈日期: 4/8/2004 | 更新日期: 5/28/2004

Dharma Shukla,Simon Fell,和 Chris Sells

Level of Difficulty 1 2 3

下載本文代碼:(538KB)

摘要面向方面編程 (AOP) 是施樂公司帕洛阿爾託研究中心 (Xerox PARC) 在上世紀 90 年代發明的一種編程範式,它使開發人員可以更好地將本不該彼此糾纏在一起的任務(例如數學運算和異常處理)分離開來。 AOP 方法有很多優點。首先,由於操作更爲簡潔,所以改進了性能。其次,它使程序員可以花費更少的時間重寫相同的代碼。總之,AOP 能夠爲不同過程提供更好的封裝性,提高未來的互操作性。

是什麼使軟件工程師都希望自己能成爲硬件工程師呢?自從函數發明以來,程序員花費了大量時間(及其老闆的大多數資金)試圖設計這樣的系統:它們不過是一些組合模型,由其他人創建的部件構成,佈置成獨特的形狀,再覆蓋上一些悅目的顏色。函數、模板、類、組件等等一切,都是軟件工程師自己創建“軟件集成電路”(模擬硬件設計師的電子器件)的種種嘗試。

我把這些都歸咎於 Lego(樂高玩具)。把兩個玩具塊(即組件)拼起時發出的悅耳的咔噠聲很讓人上癮,會促使許多程序員發明一種又一種新的封裝和重用的新機制。這方面最新的進展就稱爲面向方面編程 (AOP) 。 AOP 的核心是安排(一個摞在另一個之上)組件的一種方式,可以獲得其他種類基於組件的開發方法無法得到的重用級別。這種安排是在客戶端和對象之間的調用堆棧中進行的,其結果是爲對象創建了一種特定的環境。這種環境正是 AOP 程序員主要追求的東西,繼續閱讀本文,您將瞭解這一點。

隨本文一起提供的代碼示例分爲兩部分:COM 部分和 Microsoft .NET 部分。 COM 部分創建了一種基礎結構,可以在 COM 對象中添加方面,提供用戶界面來配置類的方面,還給出了在我們提供的基礎結構上創建的一個示例方面實現。 .NET 部分說明了如何使用內置於 .NET 基礎結構來完成 COM 版本同樣的任務,但是所用代碼更少,選擇也更多。也提供了適合此基礎結構的示例方面。本文後面將講述所有這些代碼。

*
本頁內容
何謂面向方面編程? 何謂面向方面編程?
常規用途 AOP 常規用途 AOP
設計方面框架 設計方面框架
作爲 COM 對象的方面 作爲 COM 對象的方面
對象激活 對象激活
Aspect Builder Aspect Builder
.NET 中的方面 .NET 中的方面
上下文綁定對象 上下文綁定對象
.NET 方面 .NET 方面
方面和上下文 方面和上下文
結論 結論
相關文章請參閱: 相關文章請參閱:
有關背景信息,請參閱: 有關背景信息,請參閱:

何謂面向方面編程?

一般情況下,對象是由一行行的代碼“粘合”在一起的。創建這個對象。創建那個對象。爲那個對象(其值爲這個對象)設置屬性。其間還點綴着一些用戶數據。將一切攪拌在一起。在運行時達到 450 度時就可以執行了。將多個組件以這種方式連接起來會出現這樣的問題:要實現不同的方法時,需要花費大量時間編寫同樣的代碼。這些代碼行中往往會有以下操作:將這個方法的活動記錄日誌到一個文件中以備調試,運行安全性檢查,啓動一個事務,打開一個數據庫連接,記住捕捉 C++ 異常或者 Win32 結構化異常以轉換爲 COM 異常,還要驗證參數。而且,還要切記在方法執行完之後銷燬方法開始時的設置。

這種重複代碼經常出現的原因在於,開發人員被訓練爲根據軟件新聞稿中的名詞來設計系統。如果設計的是銀行系統,Account 類和 Customer 類必不可少,它們都將自己獨特的詳細信息收集到一處,但是它們的每個方法也都需要進行日誌、安全檢查、事務管理等操作。區別在於,日誌等操作是一些與特定應用無關的系統方面。人人都需要它們。人人都編寫這些代碼。人人都討厭這樣。

噢,並不是人人……人人都需要使用這些服務,人人都討厭編寫重複的代碼,但並不是人人都需要編寫這些代碼。例如,COM+ 和 .NET 程序員可以進行所謂的屬性化編程,也稱聲明性編程。這種技術允許程序員用屬性來修飾類型或者方法,聲明需要運行時提供某種服務。例如 COM+ 提供的一些服務,包括基於角色的安全性、實時激活、分佈式事務管理和封送處理。在調用方法時,運行時會放置一組在客戶端和服務器之間獲得的對象(對於 COM+ 程序員而言稱爲“偵聽器”,對於 .NET 程序員而言稱爲“消息接收”),爲每個方法提供服務,無須組件開發人員編寫任何代碼。這是面向方面編程(20 世紀 90 年代施樂公司帕洛阿爾託研究中心 Gregor Kiczales 發明的一種編程範式,參見http://www.parc.xerox.com/csl/groups/sda/publications/papers/Kiczales-ECOOP97/for-web.pdf 最簡單的形式。

在 AOP 的領域中,COM+ 偵聽器就是通過元數據與組件相關聯的一些方面。運行時使用元數據來構造這組方面,通常是在創建對象時進行的。當客戶端調用方法時,那些特殊的方面依次獲得了處理調用、執行其服務的機會,最後再調用對象的方法。返程時,每個方面又有機會進行展開。這樣,就可以將每個組件的每個方法中都要編寫的相同樣代碼行提取出來並放在各個方面中,讓運行時來放置它們。這組方面共同爲組件方法的執行提供了上下文。上下文在環境中提供了方法的實現,其中的操作具有特殊的意義。

圖 1 安全地存在於上下文中的對象

圖 1 安全地存在於上下文中的對象

例如,圖 1顯示了一個安全地存在於上下文中的對象,該上下文提供了錯誤傳播、事務管理和同步。與 Win32 控制檯應用程序中的程序能夠假定在上下文中存在控制檯,而且調用 printf 的結果將顯示在控制檯上一樣,AOP 對象也可以假設事務已經建立,且調用數據庫將是該事務的一部分。如果設置這些服務出現了問題(例如沒有足夠資源建立事務),對象將不會被調用,因此也就無須擔心了。

常規用途 AOP

雖然 COM+ 提供了 AOP 所需的大多數服務,但是若要用來作爲常規用途 AOP 環境,它還缺乏一個必需的重要細節:定義自定義方面的能力。例如,如果基於角色的安全性不適合的話,就不能實現基於角色扮演的安全性(如同讓最危險的人保護自己的對象)。如果程序員有這種能力,許多 COM 慣用法都可以用 AOP 框架實現。圖 2 提供了示例的簡短列表。

設計方面框架

當然,有了這樣的框架構思之後,我們還必須把它構建出來。我們希望這個框架具備以下功能:

將客戶端和對象之間的方面串聯起來的運行時。

用戶定義的方面,以 COM 組件實現。

有關哪個方面與每個 COM 組件相關聯的元數據描述,如 COM+ 目錄一樣。

在方面就緒時客戶端可以用來激活組件的方法。

有關 AOP 框架的概念很簡單。關鍵就是偵聽和委託。偵聽的技巧在於,讓調用者相信它持有的接口指針指向它所請求的對象,而實際上這是一個指向偵聽器的指針,可以通過本文後面講述的激活技術之一獲取該偵聽器。偵聽器需要實現與目標組件相同的接口,但是需要通過與組件相關聯的方面堆棧來委託所有調用。在調用方法時,偵聽器將爲每個方面提供預處理和後處理調用的機會,以及傳播或者取消當前調用的能力。

AOP 框架執行兩個不同的步驟,組件激活和方法調用。在組件激活中,AOP 框架構建方面實現對象的堆棧,並返回一個偵聽器的引用,而不是實際對象的引用。在方法調用中,當調用者對偵聽器進行方法調用時,偵聽器將調用委託給所有已經註冊的方面,從而對調用堆棧中的 [in] 和 [in,out] 參數進行預處理,並將實際調用提供給對象。然後通過傳遞組件返回的調用返回值,以及調用堆棧上的 [in,out] 和 [out] 參數,將調用提供給方面進行後處理。

作爲 COM 對象的方面

在我們的 AOP 框架中,方面是實現了 IAspect 接口的 COM 類,如 圖 3 所示。框架在將方法調用傳遞給實際的底層組件實例(以下稱之爲被委託者)之前,會調用所有指定方面的 IAspect::PreProcess 方法。它把被委託者的身份、接口的 IID、方法名、方法發生的 vtbl 槽以及對 [in] 和 [in,out] 參數的枚舉器傳遞給相應的方面。如果方面從 PreProcess 返回故障 HRESULT,框架就不把調用提供給被委託者,實際上是取消了調用。

方面預處理成功返回後,框架將實際調用提供給被委託者。無論是否從被委託者返回 HRESULT,框架都會調用 IAspect::PostProcess 方法,傳遞被委託者返回的 HRESULT 和 PostProcess 方法需要的所有參數,只不過這一次枚舉器是建立在 [out], [in,out] 和 [out,retval] 參數之上的。

圖 4 顯示瞭如何編寫調用跟蹤方面,它能夠跟蹤傳遞給被委託者方法的所有調用者提供的參數。

既然已經有了一個用來調用方面的框架和一個可以使用的方面,我們需要一種機制將它們串聯起來。這種操作在對象激活時進行。

對象激活

儘管我們要將客戶端和對象之間任意數量的方面堆放起來,客戶端應該可以創建對象並調用其方法,正如沒有偵聽時的方式一樣。糟糕的是,COM 如果不採取一些奇特的技術手段(這正是 Microsoft 事務服務在集成到 COM 基礎結構中並改名爲 COM+ 之前必須實現的),就無法支持在其主激活 API CoCreateInstance 中注入的任意擴展性代碼。但是,COM 確實提供了完全擴展的激活 API :Visual Basic 中的 GetObject(對於 C++ 程序員是 CoGetObject )。我們使用自定義名字對象基於該 API 構建 AOP 激活代碼。

COM 名字對象是將任意字符串(稱爲顯示名)轉換爲 COM 對象的一段代碼,這意味着可以創建一個新的,或者從文件中找到一個,甚至從月球上下載。我們的 AOP 名字對象獲得元數據(描述與此處談論的類相關聯的方面),創建該類的實例,構造方面堆棧,通過 AOP 偵聽器將它們串聯在一起,然後將偵聽器返回給客戶端。下面是一個示例:

Private Sub Form_Load() 
    Set myfoo = GetObject("AOActivator:c:/AopFoo.xml") 
    myfoo.DoSomethingFooish 
End Sub 

請注意,除了獲取 Foo 的實例,客戶端使用組件不需要任何特殊操作。儘管 AopFoo.xml 文件會將任意數量的方面與 Foo 的該特定實例關聯起來,它還是實現相同的接口,更重要的是,它具有相同的語義。

實現自定義 COM 名字對象在某種意義上是一種神奇的技術,主要涉及以前 OLE 細節的內幕知識。幸運的是,實現的大部分內容都是陳詞濫調,而 COM 社區很久以前就把名字對象的基本實現都寫進了一個稱爲 CComMoniker 的 ATL 類中。(可以訪問 http://www.sellsbrothers.com/tools 獲取該 COM 名字對象框架。)使用這個框架,我們真正需要關心的就是實現 ParseDisplayName(這是一個分析自定義顯示名語法的枯燥方法)和 BindToObject(屬於名字對象的一部分,該名字對象用於激活客戶端提供的顯示名稱所指示的 COM 對象)(參見圖 5 )。

請注意,圖 5 中的代碼沒有顯示最困難的部分 — 偵聽器的創建和初始化。之所以困難,不在於偵聽器本身,而是偵聽器必須要做的工作。請記住,通用 AOP 框架要想通用地發揮功能,必須能夠用與所包裝的任何組件完全相同的一組接口來響應 QueryInterface 方法。而返回的接口必須能夠獲得每個方法的客戶端所提供的調用堆棧,將其傳遞給所有方面,並一直傳遞到組件本身,保持參數完整 — 無論有多少,以及是何種類型。這是一項非常困難的任務,涉及到大量的 __declspec(naked) 和 ASM thunk。

幸運的是,因爲 COM 社區已經非常成熟,我們得以站在巨人的肩膀上使用通用委託器 (UD),一個由 Keith Brown 創建的執行這一任務的 COM 組件。 Keith 曾在 MSJ 中分兩部分撰文描述了其 UD,文章名爲“ Building a Lightweight COM Interception Framework, Part I:The Universal Delegator ”,和 Part II:“ The Guts of the UD ”。我們用 Keith 的 UD 實現 AOP 框架,這減少了 BindToObject 實現中“神奇” 的部分,如 圖 6 所示。

爲了包裝目標組件供客戶端使用,執行以下四個步驟:

1. 使用組件的 CLSID 創建實際組件,該 CLSID 傳遞給原來在元數據 XML 文件中的名字對象。

2. 創建了一個 DelegatorHook 對象,偵聽對對象的 QueryInterface 調用。掛鉤負責將方法調用路由到每個方面。

3. 接下來,創建 UD 對象,檢索 IDelegatorFactory 接口。

4. 使用 IDelegatorFactory 調用 CreateDelegator,傳遞實際對象的接口、委託器掛鉤、源調用者請求的接口的 IID (riidResult),以及指向接口指針的指針 (ppvResult)。委託器返回指向偵聽器的指針,可以調用每個調用的委託器掛鉤。


圖 7 COM AOP 結構

結果如圖 7 所示。由此開始,客戶端可以將偵聽器用作目標組件的實際接口指針。調用後,它們沿着路徑上的方面路由到目標組件。

Aspect Builder

爲了激活組件,並使所有方面都正確的串聯起來,我們的 AOP 名字對象依賴一個 XML 文件來描述組件和相關聯的方面。其格式非常簡單,只包含組件的 CLSID 和方面組件的 CLSID 。圖 8 的示例包裝了帶有兩個方面的 Microsoft FlexGrid Control。爲了簡化 AOP 元數據實例的創建任務,我們創建了 Aspect Builder (如 圖 9 所示)。


圖 9 Aspect Builder

Aspect Builder 會枚舉機器上註冊的所有方面,並在左邊的列表視圖中用雲狀圖將它們都顯示出來。Aspect Builder 的客戶端區域包含組件的圖形表示。可以雙擊它(或者使用相應的菜單項)並指定組件的 ProgID。選擇了一個組件之後,可以將方面拖放到客戶端區域中,將方面添加到組件的 AOP 元數據中。

要生成提供給 AOP 名字對象所必需的 XML 格式,可以在 “Tools” 菜單中選擇 “Compile” 菜單項,元數據就會顯示在底部窗格中。可以在 Verify Aspects 窗格中實際編寫腳本,驗證元數據確實正確。可以將已經編譯的 XML 實例保存在磁盤上,也可以用 Aspect Builder 重載它們。

.NET 中的方面

儘管 Aspect Builder 使工作大大簡化,但是由於方面元數據與組件分開存儲,這使得在 COM 中進行 AOP 編程並不方便。糟糕的是,COM 的元數據在擴展性方面缺乏很多必要的功能,這正是我們感到首先需要將元數據和類分開存儲的原因。但是,作爲 COM 顯然的繼承者,.NET 就沒有這種問題。 .NET 的元數據是完全可擴展的,因此具備所有必要的基礎,可以將方面通過屬性直接與類本身相關聯。例如,給定一個自定義的 .NET 屬性,我們就可以輕鬆地將調用跟蹤屬性與 .NET 方法相關聯:

public class Bar { 
    [CallTracingAttribute("In Bar ctor")] 
    public Bar() {} 
    [CallTracingAttribute("In Bar.Calculate method")] 
    public int Calculate(int x, int y){ return x + y; } 
  } 

請注意,方括號中包含 CallTracingAttribute 和訪問方法時輸出的字符串。這是將自定義元數據與 Bar 的兩個方法相關聯的屬性語法。與 COM 中的 AOP 框架一樣,.NET 中的屬性根據 .NET 中的組件分類。.NET 中的自定義屬性是用派生自 Attribute 的類實現的,如下所示:

using System; 
  using System.Reflection; 
  [AttributeUsage( AttributeTargets.ClassMembers,  
   AllowMultiple = false )] 
  public class CallTracingAttribute : Attribute {     
      public CallTracingAttribute(string s) { 
        Console.WriteLine(s);     
      } 
  } 

我們的屬性類本身也有屬性,這些屬性會修改其行爲。在這種情況下,我們要求該屬性只與方法,而不是程序集、類或者字段關聯,並且每個方法只能有一個跟蹤屬性與之關聯。

將屬性與方法關聯以後,我們就成功了一半。爲了提供 AOP 功能,還需要在每個方法建立執行組件所必需的環境的前後訪問調用堆棧。這就需要一個偵聽器,以及組件賴以生存的上下文。在 COM 中,我們要求客戶端使用 AOP 名字對象激活組件,從而實現該任務。幸運的是,.NET 已經內置了掛鉤,因此客戶端無須做任何特殊的工作。

上下文綁定對象

.NET 中偵聽的關鍵(與 COM 中一樣)在於,要爲 COM 組件提供上下文。在 COM+ 和自定義的 AOP 框架中,通過在客戶端和對象之間堆放方面,在方法執行之前爲組件建立上下文的屬性來提供上下文。而在 .NET 中,將爲任何從 System.ContextBoundObject 派生的類提供上下文:

public class LikeToLiveAlone : ContextBoundObject {...} 

當 LikeToLiveAlone 類的實例被激活時,.NET 運行時將自動創建一個單獨的上下文供其生存,並建立一個偵聽器,可以從中掛起我們自己的方面。.NET 偵聽器是兩個對象的組合 — 透明代理和真實代理。透明代理的行爲與目標對象相同,與 COM AOP 偵聽器也一樣,它將調用堆棧序列化爲一個稱爲消息的對象,然後再將消息傳遞給真實代理。真實代理接收消息,並將其發送給第一個消息接收進行處理。第一個消息接收對消息進行預處理,將其繼續發送給位於客戶端和對象之間的消息接收堆棧中的下一個消息接收,然後對消息進行後處理。下一個消息接收也如此照辦,以此類推,直到到達堆棧構建器接收,它將消息反序列化回調用堆棧,調用對象,序列化出站參數和返回值,並返回到前面的消息接收。這個調用鏈如圖 10 所示。


圖 10 偵聽

爲了參與到這個消息接收鏈中,我們首先需要從 ContextAttribute (而不只是 Attribute)派生屬性,並提供所謂上下文屬性,將屬性更新爲參與上下文綁定對象:

{[AttributeUsage(AttributeTargets.Class)] 
public class CallTracingAttribute : ContextAttribute { 
    public CallTracingAttribute() :  
    base("CallTrace") {} 
    public override void    
    GetPropertiesForNewContext 
(IConstructionCallMessage ccm) { 
ccm.ContextProperties.Add(new  
CallTracingProperty()); 
    } 
    ??? 
} 

當激活這個對象時,爲每個上下文屬性調用 GetPropertiesForNewContext 方法。這樣我們就能將自己的上下文屬性添加到與爲對象創建的新上下文關聯的屬性列表中。上下文屬性允許我們將消息接收與消息接收鏈中的對象關聯起來。屬性類通過實現 IContextObject 和 IContextObjectSink 作爲方面消息接收的工廠:

public class CallTracingProperty : IContextProperty,  
    IContributeObjectSink { 
    public IMessageSink GetObjectSink(MarshalByRefObject o,  
        IMessageSink next) { 
        return new CallTracingAspect(next); 
    } 
    ??? 
} 

代理創建屬性的過程如圖 11 所示,其中先創建上下文屬性,然後創建消息接收。


圖 11 .NET MessageSink 的創建

.NET 方面

當一切都正確附加後,每個調用都會進入方面的 IMessageSink 實現。SyncProcessMessage 允許我們預處理和後處理消息,如圖 12 所示。最後,希望將自己與調用跟蹤方面相關聯的上下文綁定類使用 CallTracingAttribute 聲明其首選項:

最後,希望將自己與調用跟蹤方面相關聯的上下文綁定類使用 CallTracingAttribute 聲明其首選項:

{[AOP.Experiments.CallTracingAttribute()] 
public class TraceMe : ContextBoundObject { 
    public int ReturnFive(String s) { 
return 5; 
    } 
} 

請注意,我們把上下文屬性與類而非每個方法相關聯。.NET 上下文結構將自動通知我們每個方法,因此我們的調用跟蹤屬性擁有所有需要的信息,這爲我們避免了以前處理普通屬性時,需要手工將屬性和每個方法相關聯的麻煩。當客戶端類實例化類並調用一個方法時,方面就被激活了:

public class client { 
    public static void Main() { 
        TraceMe traceMe = new TraceMe(); 
        traceMe.ReturnFive("stringArg"); 
    } 
} 

運行時,客戶端和麪向方面的對象輸出如下內容:

PreProcessing: TraceMe.ReturnFive(s= stringArg) 
PostProcessing: TraceMe.ReturnFive( returned [5]) 

方面和上下文

迄今爲止,我們這個簡單的方面還沒能真正實現預期的 AOP 理想。儘管方面確實可以用來對方法調用進行單獨的預處理和後處理,但真正有趣的還是方面對方法執行本身的影響。例如,COM+ 事務性方面會使對象方法中使用的所有資源提供程序參與同一個事務,這樣方法就可以僅通過中止 COM+ 事務性方面提供的事務來中止所有活動。爲此,COM+ 方面增加了 COM 調用上下文,後者爲有興趣訪問當前事務的所有組件提供了聚集點。同樣,.NET 也提供了可以用來允許方法參與的可擴展的調用上下文。例如,可以通過將自身置於 .NET 上下文中,使對象(它封裝在調用跟蹤方面中)能夠在跟蹤消息流中添加信息,如下所示:

internal class CallTracingAspect : IMessageSink { 
    public static string ContextName { 
        get { return "CallTrace" ; } 
    } 
    private void Preprocess(IMessage msg) { 
        ??? 
        // set us up in the call context 
        call.LogicalCallContext.SetData(ContextName, this); 
    } 
    ??? 
} 

一旦將方面添加到調用上下文後,方法可以再次將其抽出,並參與到跟蹤中:

[CallTracingAttribute()] 
public class TraceMe : ContextBoundObject { 
    public int ReturnFive(String s) { 
        Object obj =  
            CallContext.GetData(CallTracingAspect.ContextName) ; 
        CallTracingAspect aspect = (CallTracingAspect)obj ; 
        aspect.Trace("Inside MethodCall"); 
        return 5; 
    } 

通過提供一種方式來增加調用上下文,.NET 允許方面爲對象設置真實的環境。在我們的例子中,允許對象向流添加跟蹤語句,而無需知道流的目的地、如何建立流以及何時銷燬流,這很像 COM+ 的事務性方面,如下所示:

PreProcessing: TraceMe.ReturnFive(s= stringArg) 
During: TraceMe.ReturnFive: Inside MethodCall 
PostProcessing: TraceMe.ReturnFive( returned [5]) 

結論

藉助面向方面編程,開發人員可以用與封裝組件本身相同的方式跨組件封裝公共服務的使用。通過使用元數據和偵聽器,可以在客戶端和對象之間放置任意服務,這種操作在 COM 中是半無縫的,在 .NET 中是無縫的。本文介紹的方面可以在進出方法調用的過程中訪問調用堆棧,它們提供增加了的上下文,供對象在其中生存。雖然與結構化編程或者面向對象編程相比 AOP 尚不成熟,但是 .NET 中對 AOP 的本地支持爲我們追求像樂高玩具那樣的軟件夢想提供了寶貴的工具。

相關文章請參閱:

Keith Brown 關於通用委託器的系列文章:

Building a Lightweight COM Interception Framework, Part 1:The Universal Delegator

Building a Lightweight COM Interception Framework, Part II:The Guts of the UD

有關背景信息,請參閱:

http://portal.acm.org/portal.cfm

http://www.aosd.net

Krzysztof Czarnecki 和 Ulrich Eisenecker 撰寫的 Generative Programming (Addison-Wesley, 2000 年)

AspectJ—a Java implementation of AOP

Dharma Shukla 是 Microsoft 公司 BizTalk Server 小組的開發組長。Dharma 目前正在從事下一代企業工具的開發工作。可以發郵件到 [email protected] 與他聯繫。

Simon Fell 是 Provada 公司的傑出工程師,使用 XML、.NET 和 COM 開發分佈式系統。Simon 是 pocketSOAP 開放源代碼 SOAP 工具包的作者,並與 Chris Sells 創建了 COM TraceHook,目前從事 Avian Carrier 的 SOAP 綁定的開發。要聯繫 Simon,可以發送電子郵件至 http://www.pocketsoap.com

Chris Sells 是一位獨立諮詢師,專長於 .NET 和 COM 中的分佈式應用程序,並在 DevelopMentors 公司任教。他創作了幾本著作,包括 ATL Internals ,這本書今年應該更新了。他正在撰寫一本 Windows 窗體方面的書,將於 2002 年出版。可以發送電子郵件至 http://www.sellsbrothers.comhttp://www.sellsbrothers.com 聯繫 Chris。

摘自 2002 年 3 月MSDN Magazine

此雜誌可通過各地的報攤購買,也可以 在此訂閱

轉到原英文頁面


 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章