COM範例(二)
——範例結構設計
本文為《COM範例》系列文章的第二篇,說明範例的結構,解釋COM的編程思想,並協助理解範例代碼。
範例目標
欲給一個公司做一個資訊管理系統,也就是公司中所有部門的資訊可以被輸入電腦,並可進行分散式查詢,即總經理可隨時查詢最新的訂單情況和出貨情況。由於使用COM作為此資訊管理系統的基架,可以很容易的解決分布式問題,並且由於COM對安全的封裝,使得提供存取控制也變得容易。下面先說明COM提供的編程思想,再以此編程思想設計各介面。
COM編程模型
見過不少這種說法:“COM是更加地物件導向,封裝地更徹底”。這裡要糾正這種錯誤的思想,雖然可以說對,但是是錯誤的應用。這就好像牛刀可以殺雞,但並不應該被說對。
物件導向編程思想是一種思想,指導如何設計程式架構的。其主打思想就是將被操作資料看成一個個對象。而所謂的對象就是具有狀態,並對外提供了介面以暴露其可以提供的服務的邏輯概念。其是狀態和功能通過語義的混合體。
其和日常生活很像,比如電視機就既提供了服務——搜台,又提供了狀態——哪個頻道是哪個台。因此在使用物件導向編程思想時會從對象的概念出發來定義資料結構,這和COM完全不一樣。
COM叫做元件物件模型,從名字看其異常明顯地表示了最開始引號中的話的正確性,這是個誤解。COM最突出的貢獻不是組件這個概念,而是介面。
介面表示功能的集合,其不是狀態。與物件導向正好相反,其完全不看重對象的實現,甚至淡化對象這個概念,極力強調介面的概念,這在各本COM教科書中表現地很明顯——裡面第一個講的就是IUnknown介面,極力強調沒有對象指標,只有介面指標。
這看起來有點混亂,如果認為物件導向強調的是狀態和功能的混合,COM強調的就是功能集的集合。而類就是只實現了一個介面的COM組件(不包括IUnknown),這從根本上說是COM的退化。因此當設計中的每個COM組件都只實現一個介面時,此時根本不是設計一個COM應用,只是在二進位代碼級上應用C++提供的編程思想而已。
由於COM做地並不是那麼好,以至於會產生前面所說的誤解。其強調功能的概念沒有體現出來,而更表現為組件,以至於很容易認為組件是積木,而整個程式就是用不同的組件搭建的房子。這是對象級上的模組化編程。COM不會設計到最後反而跑回老路上去。
搭積木的重點是積木,是以積木來搭建房屋。而COM提供的並不是積木,是積木間銜接的形狀,它主張在搭積木前先搭一個架子,不同的積木能放到架子上形成的不同的格子裡,架子搭好後再根據架子上形成的格子的形狀做積木,最後將積木放到架子上。而不是先做積木,然後根據積木搭房屋(這個比喻並不是非常準確)。
思考這個問題:欲實現任務和工作管理員的功能,設計兩個介面ITask和ITaskManager,考慮ITask的功能定義。其代表的是能夠作用於任務上的功能,不是任務本身,因此其有如下兩個方法:TerminateTask和GetProcessRateOfTask以分別終止任務和得到任務進度。但是很明顯,任務是需要啟動的。如果按照物件導向的思想,在不考慮設計模式的情況下,很容易想到將任務的發起這個動作作為ITask中的一個方法:StartTask,這樣ITask的實現者就是一個完全的任務,如果使用線程進行任務操作,其就連那個線程的操作也一起封裝起來,形成一個任務。這不是一個好的設計,ITask是個介面,代表的是功能,不是對象。介面以為實現它的對象就可以照其定義進行操作,因此ITask的實現者是可以被相當於任務一樣的操作,而不是任務這個東西。前者具有更好的可擴充性,如可以通過按遙控器來操作東西,但那個東西不一定必須是電視機,而後者就一定要求其是電視機。
因此COM裡重點的不是組件,而是介面,這是一種可擴充性相當好的設計思想,可以稱做面向介面編程思想。它本身是沒有什麼缺點的,但其實現方式由於使用對象的概念,則一定和狀態關聯,這在資料量很大時是不好的。如訂單會很容易地就被設計成一個類,然後提供諸如訂單結帳、提貨等多種服務(即成員函數)。這裡的問題就是訂單如此之多,如果使用一個數組作為其容器顯然性地問題嚴重,而鏈表更是應該判死刑。因此這裡將訂單設計成一個類是很不明智的選擇。對於此,應該專門仔細研究如何處理大資料量的技術,並將功能與狀態拆開,然後資料變成原材料,而功能變成機器,通過流水線生產以提高效率。即物件導向是個人主義,當資料量大時,就需要分工合作來提高效率了。對於此,Microsoft早已提供了MTS來協助開發,其中提供的編程思想就是專門針對這種大資料量而設計的,提倡無狀態組件,即狀態和功能的分離,其對於開發大資料量的應用提供了非常好的支援(關於MTS,可參考我寫的另一篇文章《MTS基礎教程》)。
範例設計
前面已經說明了COM提供的編程模型,下面就本範例說明如何設計介面。
程式員考慮最多的事應該是如何偷懶,並且美其名曰“代碼重用”,但現在又被更好聽的名詞所替代——“具有可擴充性”。範例是一個公司的資訊管理系統,裡面人事部門的資訊處理和營銷部門的將會千差萬別,資訊有完全不同的流程。因此是肯定需要一個一個編的。但它們能夠被稱做部門,就一定有共通的地方,這正是程式員最厲害的地方——歸納能力,然後推演出其他東西以達到偷懶的目的。
照前面的說法,由於資料量巨大,因此決定選擇資料庫而不是建立對象。由於各個部門沒有什麼同一性(其實還是有的,後敘),最後認為唯一相同的是同屬一個公司旗下,故決定提供一個基本架構介面,其提供最基本的如錯誤處理、日誌記錄等功能,欲通過在同一個基架下以表現得各部門在同一公司旗下。
基本架構需要提供錯誤記錄檔的記錄以方便系統的維護和查錯;需要提供介面架構以容納不同的部門組件的操作介面,即需要提供功能表命令的提供,也出於Windows介面的想法而提供工具條和快速鍵;需要提供工作管理員,因為在海量的資料中尋找那麼一兩條資訊不是瞬間的事,因此可能總經理髮起了一個人事尋找後,又發起一個訂單尋找或客戶尋找,但卻由於等得不耐煩而終止了前面的人事尋找;需要提供資料庫系統的相關資訊,以使得部門組件可以將資料存放區到統一的地方,方便備份等管理。
部門組件需要提供介面以進行資訊操作(如錄入、尋找等),作為Windows介面,常規性地需要提供菜單、工具條的維護性操作(如命令的說明字串的提供);需要提供任務執行進度,以提高操作者的忍耐限度。
經過上面的決定,基本架構應有4個介面,而部門組件應有2個介面。但請注意,一個介面表示一個功能集合,如果一個組件實現了一個介面就表示其所有功能都實現了,但COM非常可惜地提供了E_NOTIMPL這個錯誤碼,因此導致了錯誤的介面設計——裡面的方法可以有未實現的。這個錯誤碼準確的說應該是為將來擴充而預留的,即方法中的某個參數代表功能的種類,如是畫圓形還是畫矩形,但其可以指定為畫橢圓形,這種形狀暫時不支援,但相信以後版本將會支援,這才是E_NOTIMPL的真正含義,卻被錯誤的應用了,比如:
上面提到的部門組件應該支援一個介面,其提供包含部門組件介面、菜單、工具條和快速鍵的提供。完全有可能一個部門組件不使用工具條進行操作,完全使用一個對話視搞定一切,那麼當可憐的基本架構錯誤地以為其需要工具條的空閑處理而調用了相應方法時卻得到一個E_NOTIMPL時,應該怎樣?因此應該將一定會同時存在的功能歸為一個介面,因此出於上面的考慮,應該再提供三個介面:快速鍵處理、菜單處理、工具條處理。由於快速鍵沒有處理,只是獲得即可,不像菜單還需提供菜單狀態的處理等操作,所以無須快速鍵處理的介面,因此部門組件應該具有4個介面,其中三個是可選的。
上面的介面分工顯得有些牽強,不過這隻是粒度粗細的問題。如果願意粗粒度,也可以說成基本架構只需一個介面,如果要細粒度,也可再定個工具條處理介面和菜單處理介面,這裡就是見仁見智的地方了。但還是建議至少要保證介面中的方法如果實現一個,則其他的邏輯上也都需實現。
最後其IDL定義檔案如下:
import "oaidl.idl";
import "ocidl.idl";
// 基本架構實現IModuleSite,其提供基本的操作
[
object,
uuid(1A201ABA-A669-4ac7-9DF8-2DA772E927FC),
pointer_default(unique)
]
interface IModuleSite : IUnknown
{
// 供部門組件改變當前顯示模組,如點擊了營銷模組中的訂單尋找結果中的
// 辦理人欄位後自動跳轉到人事模組中顯示辦理人的相關資訊
HRESULT ChangeModule( [in] REFCLSID clsid, // 模組的CLSID
// 模組名字,僅用於提示
[in, string] WCHAR *pModuleName,
// 模組命令,指明欲讓模組執行的命令,由模組解釋
[in] ULONG command,
[in] ULONG param ); // 模組命令的相關參數
HRESULT GetFrameWindow( [out] HWND *pHwnd ); // 返回主架構視窗
};
// 基本架構實現IErrorReport,其提供報告錯誤的功能
[
object,
uuid(1A201ABA-A669-4ac7-9DF9-2DA772E927FC),
pointer_default(unique)
]
interface IErrorReport: IUnknown
{
// 報告溫和型錯誤,相當於警告
// fileName代表原始碼檔案的名字,row代表錯誤所在行
HRESULT ReportSoftError( [in, string] WCHAR *fileName,
[in] ULONG row,
[in, string] WCHAR *errorString );
// 報告暴力型錯誤,相當於錯誤
HRESULT ReportHardError( [in, string] WCHAR *fileName,
[in] ULONG row,
[in, string] WCHAR *errorString );
}
// 基本架構實現ICompanyInfo,其提供資料庫伺服器資訊
[
object,
uuid(1A201ABA-A669-4ac7-9DFA-2DA772E927FC),
pointer_default(unique)
]
interface ICompanyInfo: IUnknown
{
// 返回資料庫伺服器的相關資訊,主機IP、伺服器名字及密碼
HRESULT GetDataServerInfo( [in, string] WCHAR *loaction,
[in, string] WCHAR *server,
[in, string] WCHAR *password );
}
// 基本架構實現ITaskManager,其提供任務的操作
interface ITask;
[
object,
uuid(1A201ABA-A669-4ac7-9DFB-2DA772E927FC),
pointer_default(unique)
]
interface ITaskManager: IUnknown
{
// 新增工作
HRESULT AddTask( [in, string] WCHAR *taskString, // 任務說明字串
[in] ITask *pTask, // 任務的指標
// 返回標識一個任務的cookie
[out] DWORD* pCookie );
};
// 基本架構實現ITaskNotify,其提供任務的通知
[
object,
uuid(1A201ABA-A669-4ac7-9DFC-2DA772E927FC),
pointer_default(unique)
]
interface ITaskNotify: IUnknown
{
// 通知指定任務的進度已經變化
HRESULT ProcessRateChange( [in] DWORD cookie );
// 通知任務已經結束
HRESULT TaskOver( [in] DWORD cookie );
};
// 部門組件必須實現IModule,其提供模組的操作
[
object,
uuid(1A201ABA-A669-4ac7-9DFD-2DA772E927FC),
pointer_default(unique)
]
interface IModule: IUnknown
{
// 初始化模組,nID為模組視窗的子視窗ID
HRESULT InitialModule( [in] IModuleSite *pSite, [in] UINT nID );
// 返回模組的表徵圖
HRESULT GetIcon( [out] HICON *pHicon );
// 返回模組的名字
HRESULT GetName( [out, string] WCHAR **pName );
};
// 部門組件不一定實現IModuleCommand,其提供執行模組所特有的命令
[
object,
uuid(1A201ABA-A669-4ac7-9DFE-2DA772E927FC),
pointer_default(unique)
]
interface IModuleCommand: IUnknown
{
HRESULT DoCommand( [in] ULONG command, [in] DWORD param );
};
// 部門組件不一定實現IModuleNotify,其對模組提供一個通知途徑
[
object,
uuid(1A201ABA-A669-4ac7-9DFF-2DA772E927FC),
pointer_default(unique)
]
interface IModuleNotify: IUnknown
{
HRESULT OnActivate(); // 模組切換時被啟用
HRESULT OnDeActivate(); // 模組切換時取消啟用
};
// 部門組件必須實現IModuleUI,其提供模組介面的相關操作
[
object,
uuid(1A201ABA-A669-4ac7-9E00-2DA772E927FC),
pointer_default(unique)
]
interface IModuleUI: IUnknown
{
// 返回模組的主要視窗
HRESULT GetMainWindow( [out] HWND *pHwnd );
// 翻譯快速鍵
HRESULT TranslateAccelerator( [in] MSG *pMsg );
};
// 部門組件不一定實現IMenuUdpate,其提供模組介面中菜單的相關操作
[
object,
uuid(1A201ABA-A669-4ac7-9E01-2DA772E927FC),
pointer_default(unique)
]
interface IMenuUpdate: IUnknown
{
HRESULT GetMenu( [out] HMENU *pHmenu );
HRESULT GetMenuItemString( [in] ULONG nID,
[out, string] WCHAR **pString );
};
// 當部門組件建立了一個任務時,任務對象必須實現ITask以進行相應的任務管理
[
object,
uuid(1A201ABA-A669-4ac7-9E02-2DA772E927FC),
pointer_default(unique)
]
interface ITask: IUnknown
{
// 返回任務的進度
HRESULT GetProcessRateOfTask( [out] float *pRate );
HRESULT TerminateTask(); // 終止任務
// 將任務和工作管理員綁定起來
HRESULT SetTaskSite( [in] ITaskManager *pManager, [in] DWORD cookie );
};
[
uuid(1A201ABA-A669-4ac7-9D00-2DA772E927FC),
version(1.0),
helpstring("ExampleBase 1.0 TypeLib")
]
library ExampleBaseLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
interface IModuleSite;
interface IErrorReport;
interface ICompanyInfo;
interface ITaskManager;
interface IModule;
interface IModuleCommand;
interface IModuleNotify;
interface IModuleUI;
interface IMenuUpdate;
interface ITask;
interface ITaskNotify;
};
上面的設計有個很明顯的問題就是並沒有體現組件的特性,只是很簡單的部門組件和基本架構的組合,部門組件不能再有什麼其他作為,是一種變相的DLL技術。這是範例的目標及特點(各部門完全不一樣的資訊處理方式)決定的,就是一個外掛程式介面。基本架構相當於一個播放器,而部門組件相當於一種音效處理外掛程式。由於這隻是個簡單的例子,無法表現出COM組件特性的優點,但就此範例給出執行緒模式的例子已經是足夠了。
如果每個部門組件都只是資訊錄入、資訊查詢和資訊管理(忽略其商務程序,如訂單需要和出貨聯絡起來),則可以使用另一種功能分割方式,即資訊表現的介面、錄入資訊的介面、查詢資訊的介面和管理資訊的介面(甚至還可以抽象出業務進而形成業務介面),這種方案將體現出組件的概念,但複雜程度亦增加了不少,因為其靈活性大大高於前一種方案。
由於添加工具條的支援需要更多的代碼,並且對本範例沒有什麼意義,故本範例中沒有提供對工具條的支援。
作為一種習慣,將工程中所有的介面定義在一個.idl檔案中,然後再專門定一個項目產生其代理/佔位組件,並匯出IID等這類全域變數以供以後的使用,並且可以將類型資訊一起加入其中,以減少最終完成中的檔案數量。對於此,我已經在範例的解決方案中如此做了,原始碼請在《COM範例(四)》中下載。