EternalWindows
COM基礎 / クラスとインターフェース

COM(Component Object Model)とは、バイナリモジュール同士(EXEやDLL)がインターフェースという手段で通信するための規格です。 通信といえば、ウインドウメッセージやWinsockなどの事を想像するかもしれませんが、 COMもそうした中の一種であると考えて問題ありません。 たとえば、Winsockはクライアントとサーバー間の通信をソケットで実現していますが、 これに対してCOMは通信をインターフェースで実現しているというだけのことです。 つまり、COMを理解することは建前の上では、通信手法の1つを理解するということに過ぎません。

さてそれでは、COMによる通信を行うつもりがない場合はCOMについて理解しなくても問題ないのでしょうか。 答えは残念ながらNOになります。 理由は、Windowsによって提供される機能が関数という形ではなく、 COMインターフェースという形で提供されることが相当数あるからです。 つまり、COMを理解しなければWindowsの一部の機能を利用できないため、 開発者は嫌でもCOMを理解しなければならないのが現状になっているのです。 しかし、COMというものをそこまで新しい何かと考える必要はありません。 なぜならCOMは、C++言語をベースとした技術になっているからです。

C++言語はクラスを定義し、そのクラスのインスタンスをオブジェクトとして定めていました。 この要領はCOMにおけるサーバーでも全く同じであり、 サーバーは自身の機能をメソッドとして含んだクラスを定義します。 たとえば、クライアントからのファイルオープンに応答したいサーバーは、 次のようなクラスを定義したいと思うはずです。

class CMyServer
{
public: // クライアントが呼び出せるようにpublicにする。
	BOOL LoadFile(LPTSTR lpszFileName);
	BOOL CloseFile();
	BOOL ReadFile(LPVOID lpBuffer, DWORD dwReadSize);

private: // これはクラス内部で使用するためprivateにする。
	BOOL ValidFileName(LPTSTR lpszFileName);

private:
	LPBYTE m_lpData;
};

サーバーがこのようなクラスをヘッダーファイルに定義し、さらにメソッドの処理をソースファイルに記述していれば、 クライアントはサーバーの機能を利用できるように思えます。 つまり、pMyServer->LoadFileのような呼び出しが成立するということであり、 このようなメソッド呼び出しをクライアントに提供することこそがサーバーの目的でもあるわけです。 しかし、残念ながら上記のようにクラスを定義した場合は、クライアントはサーバーの機能を利用できるようになりません。 理由は、先にも述べたようにCOMというものが通信であり、クライアントとサーバーが別々のファイルに存在しているからです。

クライアントとサーバーが別々のファイルに存在するということは、 サーバーはクライアントに対してクラスを予め公開しておかなければなりません。 しかし、このようにクラスを公開してしまうと、クライアントのコードがクラスの実装に強く依存するという問題が発生します。 たとえば、サーバーがクラスに新たなメンバを追加した場合、 クライアントにおけるsizeof(CMyServer)のサイズは変化しなければなりませんが、 これはクライアントのコードを再コンパイルすることによって初めて変化されるものです。 つまり、既に作成されたEXEは前のバージョンにおけるクラスを基にしており、 実行する環境によって正常に動作しないことが予測されます。 こうした問題を解決するべく、COMではサーバーのクラスをクライアントに公開するようにはなっていません。 クラスの中でクライアントが必要とするメソッド群をインターフェースという形で別個定義し、 それをクライアントに公開するようにしているのです。 これにより、クラスの実装がクライアントから隠蔽されることになり、 サーバーはクラスのメンバを自由に変更できるようになります。 このような方法は、インターフェースと実装の分離と呼ばれることがあります。

インターフェースの原則は、一切のメンバを持たないことと、メソッドを純粋仮想関数として定義することです。 メンバを公開してしまうと、クライアントがクラスの実装に依存してしまうため、これは避けなければなりません。 インターフェースは、クライアントがサーバーにアクセスする唯一の手段であるため、 サーバーの機能を利用できるようなメソッドを含んでおく必要があります。

struct IFileControl
{
public:
	virtual BOOL LoadFile(LPTSTR lpszFileName) = 0;
	virtual BOOL CloseFile() = 0;
	virtual BOOL ReadFile(LPVOID lpBuffer, DWORD dwReadSize) = 0;
}

インターフェースのメソッドを仮想関数ではなく、純粋仮想関数として定義していることには大きな意味があります。 まず第1に、純粋仮想関数であればクラスにメソッドのオーバーライドを強制することができますから、 インターフェースがメソッドの実装を持つことは完全になくなります。 これで何が起きるかというと、このインターフェースを他のクラスが再利用できるという利点が生じます。 つまり、独自のインターフェースを定義して継承する代わりに、既存のインターフェースを継承することができます。 そして第2に、純粋仮想関数として定義すると、メソッド呼び出しにおけるバイナリコードがコンパイラ依存でなくなるという利点が生じます。 純粋仮想関数の呼び出しは、vptr(vtblへのポインタ)からvtbl(関数テーブル)に格納されたアドレスを基に行うため、 単純なポインタアクセスということもあり、生成されるコードがコンパイラによって変化するようなことはありません。 つまり、クライアントとサーバーで使用するコンパイラが異なっていても、 両者は問題なく通信できるということになります。

クライアントがクラスのオブジェクトをインターフェースで識別するということは、 クラスはインターフェースを継承していなければなりません。 また、インターフェースは純粋仮想関数を含んでいるため、 クラスはメソッドをオーバーライドすることになります。 このようなことはインターフェースを実装すると呼ばれ、 クライアントが呼び出したメソッドが実は存在しないという自体を防ぎます。 次に、インターフェースを継承したクラスの定義を示します。

class CMyServer : public IFileControl
{
public:
	BOOL LoadFile(LPTSTR lpszFileName);
	BOOL CloseFile();
	BOOL ReadFile(LPVOID lpBuffer, DWORD dwReadSize);

private:
	BOOL ValidFileName(LPTSTR lpszFileName);

private:
	LPBYTE m_lpData;
};

CMyServerにはLoadFileからReadFileまでの定義が含まれていますが、 これはIFileControlを継承したことによるものです。 ValidFileNameはIFileControlに含まれていませんでしたが、 それはこのメソッドがCMyServerの中で必要になるものだからです。 つまり、クライアントが呼び出すべきメソッドではないため、 インターフェースに追加する必要もありませんし、 アクセス修飾子もprivateで構いません。

話を整理すると、サーバーは自身のデータを表すクラスを定義したいわけです。 しかし、クラスをクライアントに公開してしまっては、 クライアントがクラスの実装に依存するという問題が発生するため、 インターフェースと実装を分離することにしました。 つまり、クラスを公開するのではなくインターフェースを公開するようにし、 それを通じてクラスの機能を利用してもらうようにしたわけです。 そして、こうしてできあがったインターフェースベースの通信が、COMということになります。

COMの設計目標

今回はCOMをインターフェースという視点で説明しましたが、 これはあくまでCOMの一部分を取り上げているに過ぎません。 COMを使用することによってどのような利点が生じるのか、 あるいはCOMがどのような目的で設計されたのかを理解していなければ、 通信の手段としてCOMを選択する理由というものが見えてきません。 次に、COMの設計目標を示します。

1. COMは、オブジェクトの実装をクライアントから隠蔽する。 これにより、オブジェクトの実装が変化してもクライアントに再コンパイルを要求しない。

2. COMは、クライアントが使用するコンパイラやプログラミング言語を問わない。 つまり、どのプログラミング言語からでもオブジェクトを利用することができる。

3. COMは、オブジェクトがどこに存在するかをクライアントに意識させない。 クライアントは、オブジェクトがDLLに実装されていてもEXEに実装されていても、 同じようなコードでオブジェクトを利用することができる。

4. COMは、様々なオブジェクトを統一的な方法で扱える。 オブジェクトが既存のインターフェースを実装すれば、 クライアントとオブジェクト間で独自の規約を設ける必要がなくなるため、 どのクライアントでもオブジェクトの使い方が理解できる。

1番目と2番目の項については、今回述べた通りです。 3番目の項については、別の節で詳しく取り上げる予定です。 4番目の項については、独自のインターフェースではなく既存のインターフェースを使用せよということですが、 これだけでは少し分かりにくいので、ウインドウメッセージを例に考えてみましょう。 今、2つの自作プロセスがメッセージ通信を行っているものとして、 片方のプロセスが相手側プロセスを終了させたくなったとします。 そこで、次のようなコードを実行することになりました。

// プロセスの終了を促す独自のメッセージを定義する。
#define WM_EXITPROCESS WM_APP

// その独自のメッセージを相手プロセスのウインドウに送信する。
PostMessage(hwndTarget, WM_EXITPROCESS, 0, 0);

この実装は一見、何も問題のないように思えますが、実際には大きな無駄が含まれています。 それは、プロセスを終了させるためのメッセージが既に存在するのにも関わらず、 それを無視して独自のメッセージを定義しているという点です。 周知の通り、WM_CLOSEというメッセージを送信すれば、ウインドウは破棄されてプロセスは終了することになりますから、 独自のメッセージを定義してその意味を予め相手に伝えておくようなことはせず、 両者が既に理解しているWM_CLOSEを使用するのが最善であると言えます。 ここで得られるヒントは、既知の情報を使用すれば共通の規約を設ける必要がなくなるという点であり、 このような既知の情報というのは、COMではインターフェースという形で大量に定義されています。 つまり、サーバーが既存のインターフェースを使用するようになれば、 どのようなクライアントもサーバーの使い方を理解できるようになり、 汎用性が高くなります。



戻る