次の方法で共有


ダイナミック HTML と Microsoft Visual C++ による Web ベース クライアントの構築

Christian Gross
euSOFT

1998年5月

要約: レガシー アプリケーションをWebに移行するときのデザイン パターンについて解説しま す(24ページ)。以下のトピックを扱っています。

  • デカップリングされたクライアント
  • フォーマットをロジックから分離するデザイン パターン
  • DHTMLとATLによる、パターンをベースにしたコンポーネントの構築

問題

Webが普及する前の時代には、個々の企業が独自のソリューションを使ってレガシー アプリケ ーションを開発していました。今日では、これらの多様な開発ソリューションを使用しているアプリ ケーションを、Webベースのフレームワークに移行するという問題が浮上しています。レガシー アプリケーションのWebへの移行は簡単な作業ではありません。アプリケーションを部分的に、 または完全に書き換える必要があり、多層アプリケーションを効率的に開発できる手法が強く求 められています。

デザイン パターン

デザイン パターンはなぜ必要なのでしょうか? デザイン パターンは、細かい部分に立ち入らず にロジックを定義する効率的な手段となります。例として、2人の機械工学者が面と向かい合って、 熱タービンについて話をしている場面を考えてみましょう。2人の会話は、高水準の概念とアイデ アを論じるための、共通の特殊化された言語によって円滑に進みます。デザイン パターンは、 複雑なアーキテクチャを定義する効率的な手段を提供することで、これと同じ目的を果たします。 レガシー アプリケーションのWebへの移行という問題を検討するなかで、いくつかのデザイン パターンが評価されました。そのうちの1つであるModel-View-Controller(MVC)アプローチ (http://atddoc.cern.ch/Atlas/Notes/004/Note004-7.htmlを参照)は、最初のうちは優れた選択 肢であるように思われました。MVCは、UIを、その作成に使われるロジックから分離(デカップ ル)するので、本記事で扱っている問題をうまく解決できるように見えます。しかし、より細かく検 討してみると、MVCパターンは中央のコントローラとモデルを使って、さまざまなビューを操作し ています。われわれのデザイン パターンには、複数のデータ オブジェクトと単一のビュー(Web ページ)があります。つまり、MVCパターンは複数ドキュメント アプリケーションには理想的です が、Webベースのフレームワークにはうまく適合しません。
Strategy Pattern(Design patterns, Gamma, et al, pg. 315)も、やはりUIをロジックから分離し ます。しかし、UIとのリッチなインタラクションを可能にするほどではありません。Strategy Patternのもう1つの問題は、コンテキスト オブジェクトが、グローバル操作に使われる外部イン ターフェイスを定義していないということです。外部インターフェイスについては、特定のインプリメ ンテーションに動的に接続する特定のインターフェイス クラスを定義するBridge Patternが理想 的です。本記事で解説するデザイン パターンは、この2つのパターンの優れた特徴、すなわち Strategy PatternのデカップリングされたUIと、Bridge Patternの動的なインターフェイス クラス をベースにしています。

デザインのデカップリング
これまで、デカップリングという言葉には望ましい特徴という意味合いを持たせてきました。しかし、 「デカップリング」とはそもそもどういう意味なのでしょうか? デカップリングとは、インターフェイス を定義した上で、特定の言語を使ってそのインターフェイスをインプリメントするというプロセスの ことです。次のインターフェイス定義の例を考えてみましょう。

  
class baseOperations {
public:
   virtual long operation1( long param1, long param2) = 0;
   virtual long operation2( long param1) = 0;
};

この例は、2つの数値演算のためのインターフェイス定義を使用しています。これらがどのように 機能し、何を行うかは、インターフェイスとは独立した問題です。これがデカップリングの本質です。 つまり、インターフェイスではなく、インプリメンテーションが機能を定義するということです。次に 示すのは、インプリメンテーションの例です。

  
class Implemented : public baseOperations
{
public:
   virtual long operation1( long param1, long param2) {
      return param1 * param2;
   }
   virtual long operation2( long param1) {
      return param1 + param2;
   }
};

このインプリメンテーションは以下のように使用されます。

  
void Method1( baseOperations *op, int param1, int param2) {
   printf( "Operation1 is %ld, %ld = %ld\n", param1,
           param2, op->operation1( param1, param2));
   printf( "Operation2 is %ld = %ld\n", param1, op->operation2( param1));
}

int main() {
   Implemented imp;

   Method1( &imp, 1, 2);
   return 0;
}

インプリメントされたオブジェクトは、baseOperationsインターフェイスを期待しているメソッド (Method1)に渡されます。これは、Method1関数は任意のインプリメンテーションを使えるという ことを意味しています。このデカップリングの例では、Method1はインプリメンテーションが何で あるのかを知らず、単に使用するだけです。
この例では継承を使うこともできます。実際、継承はあらゆる問題の解決に使用できます。しかし、 継承を使うことの問題は、メイン関数が特定のクラス インプリメンテーションへの参照を必要とす るという点にあります。別のインプリメンテーションを使用するときには、別のクラスを参照しなくて はなりません。このような形で参照を行うと、依存関係が生じ、インターフェイスを独立に拡張、変 更、または再利用するのが難しくなります。

デザイン パターン

デカップリングされたインターフェイス デザインと動的なインターフェイス クラスの特徴を組み合 わせた新しいデザイン パターンを提案します。以下に、このデザイン パターンの概要を説明し ます。

名前
その特徴から、このデザイン パターンは"Separating Format from Logic"と呼ぶことにします。

問題
Separating Format from Logicデザイン パターンは、レガシー アプリケーションのWebベース フレームワークへの移行という問題にどのように対処するのでしょうか? まず、時間追跡アプリ ケーションであるレガシー アプリケーション、TimeClockを例に取ります。オリジナルのコードは Zafir Anjumによって書かれ、後にMy Blenkersによって変更されました。オリジナルの TimeClockのインターフェイスを図1に示します。

図1

図1. オリジナルのTimeClockのインターフェイス

このアプリケーションの目的は、特定のプロジェクトにどれだけの時間を費やしたかを監視するこ とです。最初の形では、アプリケーションは休暇や病欠といったものを定義していました。しかし、 唯一可能な操作は、特定のタイムカードに開始と終了の時刻をパンチすることだけでした。この タイムカードは1つのプロジェクトとして扱うことができましたが、直接の関連はありませんでした。 第2の形では、プログラムはプロジェクトを定義していましたが、今回は病欠や休暇といった概念 は定義されていませんでした。
この2つの形は、どちらもUIをロジックにカップリングすることに内在する問題を表しています。 UIとロジックを分離して、粒度の細かいUIを作るのはきわめて困難です。カップリングされたUI では、単純なタスクを実行するのにも、予想以上に長い時間がかかります。その結果、インターフ ェイスはデータをクライアントからサーバーに送る以外の何もしないというシン クライアント コン ピューティングのルネッサンスが始まりました。従来のクライアントと比較すると、シン クライアン トは次の2つの特徴を備えています。

  1. ロジックをクライアントからサーバーに移動した。これにより、サーバーの開発に要する期間 が長くなった。
  2. リッチなクライアント サイド機能を持たない、より単純なインターフェイスを持っている。

ラピッド アプリケーション開発(RAD)ツールは、この傾向を産み出した責任の一端を担っていま す。というのも、RADツールでは、イベントの中にロジック コードを簡単に追加できるからです。 現在のRADツールは、キャンバスまたはフォームに要素を配置していく形になっています。この アプローチでは、UIをインクリメンタルに組み立てたり、分解したりするのは不可能です。
開発コストの増大に対抗するために、多くのベンダはコンポーネントをツールボックスからキャン バスにドラッグできるようなツールを提供してきました。これらのドラッグ アンド ドロップ ツール では、ユーザー インターフェイスを短期間に作成することができますが、結果として得られるロジ ックはUIに緊密に結び付けられ、それを拡張したり、別のキャンバスに移動したりするのが難し くなります。
UI開発を促進するための戦略の1つとして、目的のUIロジックの80%を実行する高機能なコン トロールを購入または構築するというものがあります。これらのコントロールはうまく動作しますが、 オーバーライドしたり変更したりするのは困難です。たとえば、グラフ作成コントロールをオーバ ーライドして、動的にVirtual Reality Modeling Language(VRML)互換にするというようなことは 不可能です。このためには新しいコントロールを開発または購入しなくてはなりません。いずれに しても、これは簡単な作業ではありません。コントロールを書き直さなくてはならないわけです。こ のように、シン クライアント、RADツール、ドラッグ アンド ドロップ ツール、および高機能のコン トロールは、ロジックからデカップリングされたユーザー インターフェイスを開発するという問題 の解決にはなりません。明らかに、何か別のものが必要なのです。

ソリューション
われわれが提案するソリューションは、特定のタスクを実行するオブジェクトに動的にカップリン グされるUIをベースにしています。このアーキテクチャでは、コントローラがビジネス ロジックの 一部を管理しますが、きわめて高い水準でこれを行います。コントローラのタスクは、集計、イベ ント キャスティング、および編成に限定されます。これらの高水準のタスクは、データの編成と、 一般的なビジネス プロセス(タイム クロックのパンチインとパンチアウトなど)に固有のタスクで す。コントローラはビジネス ロジックをインプリメントするわけではありません。単に、適切なイン プリメンテーションにアクションを指示するだけです。
一般的なビジネス ロジックを受け取るインプリメンテーションは、要求されたタスクを実行します が、操作の結果を格納することはしません。結果の格納はコントローラに任されており、コントロ ーラは情報の格納と取得に使われる総称インターフェイスを公開します。ロジックとコントローラ は、ロジック コンポーネントがコントローラに自分自身を登録するときにインターフェイスを交換し ます。このプロセスの中で、登録はロジック コンポーネントによって開始され、コントローラは受 動的な役割を果たします。ロジック コンポーネントが登録プロセスを通してコントローラに接続し た後は、コントローラが必要に応じて具体的な操作を実行します。この形でのデカップリングには、 ユーザー インターフェイスとロジックを動的に割り当てられるという利点があります。
ロジック コンポーネントは、グラフィカル ユーザー インターフェイス(GUI)的な要素は何も持って いません。コンポーネントがインスタンス作成されるときにUIと関連付けられるだけです。この関 連付けにより、見掛けは異なるが、同じロジックを実行する別のユーザー インターフェイスを作 成することが可能になります。UI機能は、ロジック コンポーネントやコントローラの動作には影響 を与えません。また、同じロジックに基づく別のソリューションのために、別のUI媒体を使うことも 可能です。この、インターフェイスのビジネス ロジックからの分離という特徴が、Separating Format from Logicという名前の由来となっています。
図2に示すように、Separating Format from Logicパターンは、1) サービスをインプリメントする コントローラ、2) ダイナミックHTMLの形でのインターフェイス、および3) ビジネス ロジックをイ ンプリメントするコンポーネントの3つの要素から構成されています。

図2

図2. Separating Format from Logicのアーキテクチャ レイアウト
**注:**本記事では、コンポーネント、ロジック、ビジネス ロジック、およびロジック コンポーネント という言葉を、いずれも同じ意味に使用しています。

結果
Separating Format from Logicデザイン パターンは、次のような結果をもたらします。

  • 言語に対する中立性: コントローラ、インターフェイス、およびコンポーネントは、分散オブジ ェクト テクノロジを使って連結されるので、新しいインプリメンテーションや新しいUIを追加す るのが簡単になります。後に変更を加えやすくするためには、個々のアーキテクチャ要素に 適した言語を使えるようにしておくことが重要です。たとえば、UIは動的でなくてはならない ので、ダイナミックHTMLが適しています。また、ロジック コンポーネントにはMicrosoft Visual C++開発システムのようなプログラミング言語が使えなくてはなりません。
  • コントローラへの依存性: 静的なビジネス プラクティスを反映しない不適切な設計のコントロ ーラは、コントローラのインターフェイスをインプリメントするコンポーネントをすぐに時代遅れ にしてしまいます。このようなことが起こると、ビジネス ロジックを書き直さなくてはなりませ ん。
  • インプリメンテーション上の細部の隠蔽: コントローラは、UIの細かい部分がどこから来てい るのかを知っている必要がありません。実際、コントローラは必要ならば、別のUI内で別の マシン上のコンポーネントと通信を行うこともできます。登録プロセスはロジック コンポーネ ントの側から開始されるので、高度な柔軟性が保証されています。
  • 単純性: このデザイン パターンは、UIのフォーマットを、そのUIが実行すべきロジックから 明確に分離しているので、単純性を備えています。
  • 拡張性: UIとビジネス ロジック コンポーネントの間の通信は、純粋に動的なプロセスで、 個々のコンポーネントが独立に行います。この動的な通信はComponent Object Model(COM)によって実現されます。

パターンのインプリメント

これまでに、われわれは新しいデザイン戦略が必要であることを確認し、Separating Format from Logicデザイン パターンのアーキテクチャ レイアウトと特徴を定義しました。次は、このパ ターンを使って既存のレガシー アプリケーションを変更する方法をデモンストレーションすること にします。本記事の残りの部分では、前に述べた例、TimeClockを使って、デザイン パターンの インプリメントの3ステップから成るプロセスを解説します。

  1. インターフェイスを定義する
  2. コンポーネントを構築する
  3. コントローラを構築する

使用するテクノロジ
デザイン パターンのインプリメンテーションについて論じる前に、インプリメンテーションに使用す るテクノロジを検討する必要があります。われわれが提案するデザイン パターンは、動的な関 連付けとデカップリングを使ったテクノロジカルなソリューションを必要とするので、テクノロジに依 存する部分が大きくなっています。2つのテクノロジカルな層が必要となります。

  • Component Object Model(COM): インターフェイスとインプリメンテーションのデカップリ ングを可能にするコンポーネント オブジェクト テクノロジ。
  • ダイナミックHTML: 関連付けを動的に行い、部品を組み立てるようにしてUIを構築する UI。

このデザイン パターンで使用される言語はJavaScriptとVisual C++です。このモデルではクラ イアント コードが使われるので、Javaの方が便利だと思う人もいるかもしれません。しかし、 C++にはリッチな機能があり、軽量のコードを短期間で作成できるという利点があります。個々の ツールの長所を考えると、ダイナミックHTMLは動的なスクリプティングと関連付けに適しており、 C++はロジックのインプリメントに適しています。

ステップ1: インターフェイスを定義する
ソリューションをインプリメントするための最初のステップは、ロジック コンポーネントとコントロー ラが使用するインターフェイスの定義です。最初のインターフェイスは、個々のコンポーネントが インプリメントしなければならない操作です。レガシー アプリケーションは2つの操作(パンチイン とパンチアウト)しかサポートしていないので、われわれのインターフェイスはとりあえずこれらの 操作をインプリメントする必要があります。さらに、ハウスキーピングの目的に2つの機能を追加 する必要があります。インターフェイス定義言語(IDL)を使用して作られたインターフェイスを次に 示します。

  
[
        object,
        uuid(EA55BFDF-BC3E-11d1-9484-00A0247D759A),
        pointer_default(unique),
        local,
        version(1.0)
]
interface ITimeCard : IUnknown
{
   HRESULT PunchIn( BSTR time, [out,retval]long *retVal);
   HRESULT PunchOut( BSTR time, [out,retval]long *retVal);
   HRESULT Descriptor([out, retval]BSTR *description);
   HRESULT SetService(IUnknown *serviceProvider);
}

操作がパンチインまたはパンチアウトであるときには、時刻が第1パラメータとなります。すべて のタイムカードを同期させる必要があるので、時刻のパラメータはコントローラが生成します。パ ラメータretvalは、操作によって実行された戻りコードです。

前述の2つのハウスキーピング メソッドは、descriptorSetServiceです。descriptorは、コ ントローラが、どのインプリメンテーションが自分自身を登録したかを表示するために使用するオ ブジェクトの単純な記述です。SetServiceは、サービスに似た総称操作を公開することで、コン トローラ インターフェイスを関連付けるために使われるメソッドです。
われわれのサンプル アプリケーションのコントローラは、データ レコードセットに似たサービスを 公開します。コンポーネントが操作を実行すると、結果として得られたデータはサービス インター フェイスを通してコントローラに格納されます。このため、第2のインターフェイスは次のように定 義されます。

  
[
        object,
        uuid(EA55BFDD-BC3E-11d1-9484-00A0247D759A),
        pointer_default(unique),
        local,
        version(1.0)
]
interface IControllerService : IUnknown
{
   HRESULT Reset();
   HRESULT Rewind();
   HRESULT MoveNext();
   HRESULT GetColumn( BSTR colName, [out,retval]BSTR *value);
   HRESULT SetColumn( BSTR colName, BSTR value);
   HRESULT RecordReference( [out,retval]long *retval);
   HRESULT Add();
   HRESULT Update();
}

レコードセットについての知識がある方は、上記のサービス インターフェイスに似た要素がいく つもあることがわかるでしょう。基本的な操作には以下のものがあります。

  • Reset: コントローラ内のレコードセットを空にします。
  • Rewind: ポインタをコントローラ内の最初のレコードセットに移動します。
  • MoveNext: コンポーネント レコードセット ポインタを次の位置に移動します。
  • GetColumn, SetColumn: コントローラ上のデータの取得と設定を行います。
  • RecordReference: 後に参照するためにブックマークを取得します。
  • Add: レコードセットにレコードを追加します。
  • Update: レコードセット内のデータを更新します。また、コントローラがUIまたはリモート デ ータ ソースにアクセスするために必要な任意の操作をサポートします。

上記の2つのインターフェイスをベースに、コントローラはコンポーネントとして、またコンポーネン トはコントローラとして機能することが可能になっています。また、コンポーネントとコントローラは 任意の言語で書くことができます。このように、Separating Format from Logicデザイン パター ンはオープンなアーキテクチャ ソリューションを提供します。
2つのインターフェイスは、プロジェクトのCommonInterfaceの一部です。これはTimeCard.idl ファイルだけを含んでいるプロジェクトです。IDLファイルは、オブジェクトのインターフェイスを定 義する一連の「プロトタイプ」を含んでいます。このファイルはMidl.exeによってコンパイルされ、 いくつかのタイプのファイルを生成します。次のコマンド ラインはIDLの典型的な使用方法を示 しています。
midl /h TimeCard.h /iid TimeCard_i.c TimeCard.idl
第1オプションの/hは、インターフェイスをC++で定義するために使用されるヘッダー ファイルを 作成します。ヘッダー ファイル(TimeCard_i.c)は、すべてのIID(uuid)をexternalとして宣言する ので、IIDが実際にインプリメントされるファイルが必要となります。第2オプションの/iidはIID定 義ファイルを作成します。最後に作成されるファイル(コマンド ラインでは指定されていません)は タイプ ライブラリ(.tlb)です。このファイルは、コンポーネントがC++以外の言語で書かれる場合 にのみ使用されます。タイプ ライブラリを使用する言語の例としては、Microsoft Visual Basic開発システムがあります。

ステップ2: ロジック コンポーネントを構築する
ビジネス ロジックを構築するときには、高機能のコントロールまたはコンポーネントを生成するべ きかという問題があります。これに対する答えは、どちらでもよいというものです。タイムカードの レガシー アプリケーションを例として使うとすると、唯一重要な側面は、コントロールまたはコン ポーネントがITimeCardインターフェイスをインプリメントしているかどうかということだけです。デ ザイン パターンの要件に従い、UIはありません。このため、ここではコンポーネントが使用され ます。コンポーネントの開発にはActive Template Library(ATL) シンプル オブジェクト ウィザー ドが使用され、スクリプティングによる関連付けをサポートするためにデュアル インターフェイス を持たせる必要があります。

ATLオブジェクトの定義
次の例は、タイムカード アプリケーションのATLオブジェクト定義に加えなくてはならない変更点 を示しています。

  
class ATL_NO_VTABLE CVacation :
   public CComObjectRootEx<CComSingleThreadModel>,
   public CComCoClass<CVacation, &CLSID_Vacation>,
   public IDispatchImpl<IVacation, &IID_IVacation,
&LIBID_TIMEVACATIONLib>,

   public ITimeCard

{
public:
   CVacation();

   virtual ~CVacation();

DECLARE_REGISTRY_RESOURCEID(IDR_VACATION)

BEGIN_COM_MAP(CVacation)
   COM_INTERFACE_ENTRY( IVacation)
   COM_INTERFACE_ENTRY( IDispatch)

   COM_INTERFACE_ENTRY( ITimeCard)
END_COM_MAP()

// IVacation
public:
   STDMETHOD(SetCommentElement)(IUnknown *component);


// ITimeCard

public:

    STDMETHOD( PunchIn)( BSTR time, long *retVal);

   STDMETHOD( PunchOut)( BSTR time, long *retVal);

   STDMETHOD( Descriptor)( BSTR *description);

   STDMETHOD( SetService)( IUnknown *serviceProvider);


private:
   bool m_punchedIn;
   IControllerService *m_service;
   MSHTML::IHTMLInputTextElement *m_comment;
};

強調表示されているのは、ITimeCardインターフェイスに固有の項目です。また、ここではオリジ ナル コードに3つの要素が追加されています。

  1. 継承チェーンにpublic ITimeCardを追加しています。ITimeCardは、IDLファイルで定義され たインターフェイスに似た一連の仮想関数を定義します。
  2. COMマップにCOM_INTERFACE_ENTRYマクロを追加しています。このマクロはインターフ ェイスをコンポーネントの一部として追加します。QueryInterface(QI)IID_ITimeCardインター フェイスが参照されると、QIのATLによるインプリメンテーションはCOMマップを調べて、インタ ーフェイスが実際に使用可能であるかどうかを確認します。
  3. ITimeCardが期待している必要なメソッドを追加します。ここではこのステップを無視していま すが、仮想関数をインプリメントする必要があるため、コンポーネントは正常にコンパイルできま せん。

サービス インターフェイスの設定
コンポーネントは、Webページ上でスクリプトをインスタンス作成するときに、コンポーネントをコ ントローラに登録します。これを受けて、コントローラはSetServiceメソッドを呼び出して、コンポ ーネントに自分のインターフェイスを登録します。ただし、このときに渡されるパラメータは IUnknownです。カスタム インターフェイスを直接に渡すという方法も考えられますが、 IUnknownを渡す方が簡単です。IUnknownを使うことには、デカップリングをさらに押し進め、イ ンターフェイス ポインタを任意のものにすることができるという利点があります。このようにデカッ プリングを行うことで、コンポーネントは最も適切なインターフェイスを選択できるようになります。 サービス インターフェイスは次のように設定されます。

  
HRESULT CVacation::SetService(IUnknown *serviceProvider)
{
   try
   {
      _com_util::CheckError( serviceProvider->QueryInterface
                 ( IID_IControllerService, (void **)&m_service));
   }
   catch ( _com_error err)
      {
      ::MessageBox( NULL, err.ErrorMessage(),
                    "registerInterface Error is", MB_OK);
      };
   return S_OK;
}

クラス宣言の中にはIControllerServiceへのポインタがあります。このポインタは、QIで IID_IControllerServiceを照会することによって取得されます。QIプロセス全体が例外ブロック で囲まれています。例外ブロックは、コントロールのサイズを20 KB増やすと言われています。し かし、例外をベースにしたコードは、サイズをいくぶん大きくするにしても、予期しない事態に対処 するための単純なアプローチを提供してくれます。serviceProvider->QueryInterfaceは関数 _com_util::CheckErrorの中に埋め込まれています。この関数はドキュメントには記載されて いませんが、HRESULTが有効であることを確認してくれる便利な関数です。HRESULTが有効 でなければ、COM例外が発生します。_com_errorオブジェクトは、何がおかしくなったのかを分 析するのに利用できる有益なエラー処理オブジェクトです。catchブロックの中のMessageBox はCOMエラー文字列を表示します。

操作の実行
サービス インターフェイスが取得されたら、操作を実行し、結果として得られたデータをコントロ ーラに格納することができます。われわれのタイムカード アプリケーションの例では、実行可能 な操作はパンチインとパンチアウトです。PunchInメソッドは次のようにインプリメントされます。

  
HRESULT CVacation::PunchIn( BSTR time, long *retVal) {
   *retVal = 0;
   try {
      if( m_punchedIn == true) {
         *retVal = 4;
         return S_OK;
      }
      if( m_comment == NULL) {
         *retVal = 3;
         return S_OK;
      }
      _bstr_t strComment = m_comment->Getvalue();

      if( strComment.length() == 0) {
         *retVal = 2;
         return S_OK;
      }

      m_service->Add();

      // I put this into brackets so it will create and
      // destroy the class.  Note this is does not create
      // extra memory because the VC++ catches this and
      // optimizes.  Neat trick, eh?
      {
      _bstr_t col( "TimeIn");
      m_service->SetColumn( col, time);
      }
      {
      _bstr_t col( "TimeOut");
      _bstr_t value("");
      m_service->SetColumn( col, value);
      }
      {
      _bstr_t col( "Empty column");
      _bstr_t value("");
      m_service->SetColumn( col, value);
      }
      {
      _bstr_t col( "Comment");
      m_service->SetColumn( col, strComment);
      }
      m_service->Update();
      m_punchedIn = true;
   } catch ( _com_error err) {
      ::MessageBox( NULL, err.ErrorMessage(), "registerInterface Error
is", MB_OK);
      *retVal = 1;
   };
   return S_OK;
}

このメソッドの機能を確認するときには、これがロジックをインプリメントする操作を実行している 点に注意してください。このメソッドは、最初のステップとして、その人がすでにパンチインを行っ たかどうかを確認します。パンチインを行っていた場合には、メソッドは何もせずに返ります。次 に、UIにUI要素が関連付けられているかどうかを確認します(m_comment)。これがNULLなら ば、関連付けは存在していないので、メソッドは先に進めません。この要素が有効ならば、現在 の値が取得されます(m_comment->Getvalue())。最後に、値をチェックして、それが空でないこ とを確認します。

すべての操作が完了したら、次のステップとして、結果として得られたデータをコントローラに保 存します。データのコントローラへの保存は、レコードを追加し、情報を特定の列に保存するとい う簡単な手順で行えます。すべてのデータが追加されたら、レコードが更新されます。これでコン ポーネントの機能は終わりです。

ダイナミックHTMLモデルの使用
前に述べたように、UIのコンポーネントへのバインドはスクリプトによって行われます。この機能 の例を、次のダイナミックHTMLコードに示します。

  
&LT;OBJECT ID="TimeVacation" WIDTH=1 HEIGHT=1

   CLASSID="CLSID:29F14E14-C10A-11D1-9486-00A0247D759A"&GT;

    &LT;PARAM NAME="_Version" VALUE="65536"&GT;

    &LT;PARAM NAME="_ExtentX" VALUE="2646"&GT;

    &LT;PARAM NAME="_ExtentY" VALUE="1323"&GT;

    &LT;PARAM NAME="_StockProps" VALUE="0"&GT;

&LT;/OBJECT&GT;

&LT;div class="components" id=secVacation style="visibility:hidden"&GT;
&LT;b&GT;Vaction Options&LT;/b&GT;
&LT;table&GT;
   &LT;tr&GT;
      &LT;td>Comment:&LT;/td&GT;


      &LT;td&GT;&LT;input id="txtVacComment" type="text" name="txtComment"
size="20"&GT;&LT;/td&GT;

	&LT;/tr&GT;
&LT;/table&GT;
…
&LT;/div&GT;

&LT;script language=javascript&GT;

function WindowOnLoad() {
   Controller.RegisterComponent( TimeWorking);
   Controller.RegisterComponent( TimeVacation)
   TimeWorking.SetCommentElement( txtWorkComment);
   TimeWorking.SetProjectElement( optProjects);

   TimeVacation.SetCommentElement( txtVacComment);

   Controller.activeInterface( TimeWorking);
}

この例の中の、強調表示されている3つのフィールドは、ダイナミックHTMLを示しています。第 1の強調表示されているフィールドは、これまで説明してきたCOMコンポーネントを定義していま す。第2のフィールドはダイナミックHTMLの入力要素を定義しています。このUI要素は、第3 のフィールドで定義されているスクリプティングを使って、コンポーネントに関連付けられます。コ ンポーネントが単純なオブジェクト参照によって渡されていることに注意してください。 次に、SetCommentElementのインプリメンテーションを示します。

  
STDMETHODIMP CVacation::SetCommentElement(IUnknown * inpElement)
{
   try {
      if( m_comment != NULL) {
         m_comment->Release();
      }
      _com_util::CheckError( inpElement->
              QueryInterface(__uuidof(MSHTML::IHTMLInputTextElement),
              (void **)&m_comment));
   } catch ( _com_error err) {
      ::MessageBox( NULL, err.ErrorMessage(), "registerInterface Error
is", MB_OK);
   };
   return S_OK;
}

SetComment要素はIUnknown(inpElement)として渡されます。Microsoft Internet Explorerで は、すべてのダイナミックHTML要素がWebページ上でCOMコンポーネントとして公開されま す。Internet Explorerコンポーネントが別のCOMコンポーネントに渡されると、それは参照し、 操作することのできるCOMコンポーネントとなります。この参照モデル全体が、Mshtml.dllファイ ルに格納されています。Platform SDKの最新エディションにはヘッダーが含まれています。これ よりも簡単に参照モデルにアクセスするには、COMコンパイラ サポートを次のように使用しま す。

#import "mshtml.dll"

メソッドのインプリメンテーションに戻ると、IUnknownをIHTMLInputTextElementに変換する必 要があります。これは、QIを実行し、__uuidof(MSHTML::IHTMLInputTextElement)を要求する ことによって行われています。インターフェイスを取得したら、要素のメソッドにアクセスすることが できます。ダイナミックHTMLのメソッド名はC++環境では異なる名前になっていますが、どちら も同じメソッドを操作しています。たとえば、スクリプティングでは、データの設定と取得にメソッド 名を使用します。一方、C++では、これらの名前がGetvaluePutvalueに変換されます。これ らの名前は、どちらの環境でも同じメソッドにアクセスします。

ステップ3: コントローラを構築する
デザイン ソリューションの最後のステップでは、コントローラを構築します。コントローラの構築は、 これまでの2つのステップよりも複雑なプロセスで、ある程度の総称的なコードを定義する必要 があります。われわれのサンプルのコントローラはスレッド セーフではなく、堅牢でもないという ことに注意してください。これは、コントローラの構築方法の例として示しているのに過ぎません。 このため、ここではデザイン パターンのインプリメントに直接関係する新しい概念のみについて 解説します。
コントローラはATLベースのActive Controlとしてプログラミングされます。これはコンポーネント であってもかまわないのですが、コントロールを使用することにはいくつかの利点があります。第 1の利点はユーザー フィードバックです。ユーザー フィードバックを何らかのUI要素にリンクす ることは可能ですが、そのためにはプログラミングに不要な負担がかかります。ユーザー フィー ドバックは、サーバーにリンクして、登録済みコンポーネントのカウントなどに利用することもでき ます。しかし、コントロールを使って高機能なUIを構築するときには、後の変更が不可能になる ので、ユーザー フィードバックをサーバーに接続するのは避けるべきです。
コントローラは、使用される操作の機能を公開します。コントローラの定義を次に示します。

  
   [
      object,
      uuid(29F14DFC-C10A-11D1-9486-00A0247D759A),
      dual,
      helpstring("IController2 Interface"),
      pointer_default(unique)
   ]
   interface IController2 : IDispatch
   {
   [id(1), helpstring("method ")] HRESULT RegisterComponent
                                      (IUnknown *component);
   [id(2), helpstring("method PunchIn")] HRESULT PunchIn();
   [id(3), helpstring("method PunchOut")] HRESULT PunchOut();
   [id(4), helpstring("method ActiveInterface")] HRESULT
                       ActiveInterface(IUnknown *currInterface);
   [id(5), helpstring("method ResetActiveInterface")] HRESULT
                                         ResetActiveInterface();
   };

これらのメソッドは、スクリプティングからコントローラを利用できなくてはならないので、デュアル インターフェイスに対応しています。パンチインとパンチアウトの2つのメソッドがあり、これに加え てハウスキーピング用のメソッドが存在します。これらの操作は、単に呼び出しをコンポーネント に中継するだけなので、詳しく説明する必要はないでしょう。ここで説明する必要があるのは、ハ ウスキーピング用のメソッドです。
前に示したダイナミックHTMLコードでは、スクリプトの最初の部分で、RegisterComponentを使 ってコンポーネントをコントローラに登録していました。

  
STDMETHODIMP CController2::RegisterComponent(IUnknown * component)
{
   IOleClientSite *site;

   try {
      // Set the client site
      // The site is set everytime an interface is registered
      // Sure its not the most efficient, but its simple and effective
      GetClientSite( &site);
      m_objectDHTML.setSite( site);
      m_dataManager->setDHTMLReference( &m_objectDHTML);
      m_dataManager->addComponent( component);
   } catch ( _com_error err) {
      ::MessageBox( NULL, err.ErrorMessage(), "registerInterface Error
is", MB_OK);
   };

   return S_OK;
}

ハウスキーピング操作には、ダイナミックHTMLポインタの取得と環境のセットアップの2つがあ ります。以下に、それぞれの操作について説明します。

ダイナミックHTMLポインタの使用
コントローラは、コンポーネントとは異なり、自分のUIの部品を探し出します。コントローラはUI を動的に変更するので、コントローラに関連付けられるUIの種類には、それほどの柔軟性はあ りません。UIの動的な変更は必須の条件ではありませんが、ダイナミックHTMLのいくつかの便 利な機能を示すために追加しています。
コントローラは、COMがコントロールをインスタンス作成する方法のおかげで、自分の環境を判 定することができます。コントロールが作成され、実行されるときには、コンテナの中で実行され ます。コンテナとコントロールは緊密な関係を持っており、互いに関する情報を交換します。 Internet Explorerはこれと同じように動作します。Internet Explorerがコントロールをインスタン ス作成するときのコンテナはWebページです。この関係を利用して、コントロールはWebページ 上の他のコンポーネントや要素にアクセスできるのです。このアクセスは次のコードによって実現 されます。

  
STDMETHODIMP CController2::RegisterComponent(IUnknown * component)
{
   IOleClientSite *site;

   try {
      // Set the client site
      // The site is set everytime an interface is registered
      // Sure its not the most efficient, but its simple and effective

      GetClientSite( &site);

      m_objectDHTML.setSite( site);

      m_dataManager->setDHTMLReference( &m_objectDHTML);
      m_dataManager->addComponent( component);
   } catch ( _com_error err) {
      ::MessageBox( NULL, err.ErrorMessage(), "registerInterface Error
is", MB_OK);
   };

   return S_OK;
}

bool CDHtmlObjectModel::setSite(
   LPOLECLIENTSITE clientsite) {

   // The next step is a bit touchy because it attempts to retrieve the
   // IWebBrowser2 interface from the container.  We want to do this
   // because once we have this interface we can do almost anything
   try
   {
      IServiceProviderPtr spSP((LPOLECLIENTSITE)clientsite);

      if( NULL == spSP) {
         return false;
      }

      spSP->QueryService(__uuidof(SHDocVw::IWebBrowserApp),
            __uuidof(SHDocVw::IWebBrowser2), (void**)&m_spWebBrowser);
      if( m_spWebBrowser == NULL) {
         return false;
      }

      m_spDocument2 = m_spWebBrowser->GetDocument();
   }
   catch(...)
   {
      return false;
   }
   return true;
}

コンポーネントがコントローラに登録されるときには、IOleClientSiteインターフェイスを取得する ための呼び出しが行われます。このサイトは、コントロールを保持しているコンテナです。 GetClientSiteはATL Controlクラスが提供しているメソッドです。
CDHtmlObjectModel::setSiteメソッドは、サイト インターフェイスを取り、WebBrowserインタ ーフェイスを要求します。テストがtry - catchブロックに入っているということが重要です。しかし、 このcatchブロックには省略記号が付いており、すべての例外をキャッチすることがわかります。 Visual C++の例外処理は、一般保護違反(GPF)、数値演算のオーバーフローなどを含めて、あ らゆるタイプの例外をキャッチすることができます。このブロックは、サイト インターフェイスが WebBrowserインターフェイスを照会して、そのインターフェイスが存在しなかった場合に発生す る例外であるGPFをキャッチするように設計されています。さらに、サイトはQueryServiceメソ ッドをサポートしていない可能性があり、この場合にもGPFが発生します。最後のステップでは、 m_spWebBrowser->GetDocument()呼び出しを使って、ルートのダイナミックHTMLポインタを 取得しています。これで、現在のWebページを表すダイナミックHTMLツリーをナビゲートできる ようになります。

環境のセットアップ
第2のハウスキーピング操作は、サービス インターフェイスのための環境のセットアップです。コ ントローラは、インスタンス作成を行うときに、CDataManagerインスタンスを作成します。このオ ブジェクトは、サービス インターフェイスと、結果として得られるデータ コンポーネントを管理する 責任を負っています。CControllerServiceImplクラスはCConnectorによって管理され、 IControllerServiceをインプリメントしています。CConnectorは、IControllerServiceImplインタ ーフェイスとITimeCardインターフェイスを保持することを唯一の目的とする単純なクラスです。こ れらのすべてのクラスに、何らかのストレージを必要とする一連の配列があります。これは Standard Template Library(STL)ベクトル クラスによって提供され、次のように宣言されていま す。

std::vector< CRecord *> m_records;

標準テンプレート ライブラリを使用していることにより、要素の配列を管理し、保持するのが簡単 になっています。STLをできる限り使うようにすることをお勧めします。ATLは、別の名前空間に 格納されるので、STLと何の問題もなく共存することができます。STLについての解説は、本記 事の範囲を超えていますが、msdn.microsoft.com/visualc/stl/がリソースとして優れています。

レコードとUIについて
コントローラの中の最後の興味深い要素は、UIの更新方法です。コンポーネントが IControllerService::Addメソッドを呼び出すと、m_recordsベクトルにCRecordオブジェクトが追 加されます。次にCRecordクラスの定義を示します。

  
struct _tagColumn {
   char name[ 255];
   char value[ 255];
};

class CRecord
{
public:
   void addColumn( char *name, char *value);
   void setColumn( char *name, char *value);
   CRecord();
   virtual ~CRecord();

   MSHTML::IHTMLTableRowPtr m_row;
   std::vector< struct _tagColumn *> m_columns;

private:

};

各レコード オブジェクトは、個々の列とその値を表すm_columnsのベクトルを含んでいます。こ れは最も効率的な方式とはいえませんが、カスタム レコードをサポートすることができます。もう 1つの変数、m_rowは、Webページ上の行への参照です。IControllerService::Addを使って行 が作成されると、ダイナミックHTMLの行が次のように作成されます。

  
STDMETHODIMP CControllerServiceImpl::Add() {

   // Add this row and then add an empty record set
   m_currRecord = new CRecord;

   m_currRecord->m_row = m_parent->m_objectDHTML->
                addTableRow( "tableTimeCard", "Work", "", "", "", "");

   m_currRecord->setColumn("Type", "Work");
   m_parent->m_records.push_back( m_currRecord);
   m_iterator = m_parent->m_records.end();
   return S_OK;
}

MSHTML::IHTMLTableRowPtr CDHtmlObjectModel::addTableRow(
   char *table,
   char *type,
   char *inTime,
   char *outTime,
   char *project,
   char *comment) {
   // Retrieve all of the page elements
   MSHTML::IHTMLTablePtr spTable;
   MSHTML::IHTMLElementCollectionPtr spAllElements = m_spDocument2-
>Getall();

   _variant_t vaTag( table);

   if((spTable = spAllElements->item( vaTag)) != NULL) {
      // We have found the table, so now add a row
      MSHTML::IHTMLTableRowPtr spRow( spTable->insertRow( 1));

      MSHTML::IHTMLTableCellPtr spType( spRow->insertCell( 0));
      MSHTML::IHTMLTableCellPtr spTimeIn( spRow->insertCell( 1));
      MSHTML::IHTMLTableCellPtr spTimeOut( spRow->insertCell( 2));
      MSHTML::IHTMLTableCellPtr spProject( spRow->insertCell( 3));
      MSHTML::IHTMLTableCellPtr spComment( spRow->insertCell( 4));

      // Here is the compiler trick again
      // If a series of variables are created
      // that are identical in size, the memory will be
      // reused and it will not cost an extra allocation
      // Neat trick, eh!
      {
      MSHTML::IHTMLElementPtr spAnElement = spType;
      _bstr_t bstrStr( type);
      spAnElement->PutinnerText( bstrStr);
      }

      {
      MSHTML::IHTMLElementPtr spAnElement = spTimeIn;
      _bstr_t bstrStr( inTime);
      spAnElement->PutinnerText( bstrStr);
      }

      {
      MSHTML::IHTMLElementPtr spAnElement = spTimeOut;
      _bstr_t bstrStr( outTime);
      spAnElement->PutinnerText( bstrStr);
      }

      {
      MSHTML::IHTMLElementPtr spAnElement = spProject;
      _bstr_t bstrStr( project);
      spAnElement->PutinnerText( bstrStr);
      }

      {
      MSHTML::IHTMLElementPtr spAnElement = spComment;
      _bstr_t bstrStr( comment);
      spAnElement->PutinnerText( bstrStr);
      }

      return spRow;
   } else {
      MSHTML::IHTMLTableRowPtr spRow;
      return spRow;
   }
}

addTableRowメソッドは、Webページ上にあるパラメータ(テーブル)をベースにしています。この テーブルは、ルートのダイナミックHTMLポインタを参照し、コレクションspAllElements->All()に 対して指定された要素を問い合わせることによって検索されます。テーブルが見つかったら、行 を挿入することができます(spTable->insertRow())。次に、表示する個々の要素について、セル が挿入されます(spRow->insertCell())。レコードがWebページ上の行への参照を保持している のは、これによって行を新しい情報で更新するときに、個々の行とセルを探す必要がなくなるか らです。
更新は、コンポーネントがIControllerService::Updateを呼び出すときに、次のように実行されま す。

  
STDMETHODIMP CControllerServiceImpl::Update() {
   // This is to update the current record on the DHTML page
   std::vector&LT; CRecord *&GT;::iterator i;

   for( i = m_parent->m_records.begin(); i != m_parent-&GT;m_records.end();
i ++) {
      m_parent-&GT;m_objectDHTML-&GT;updateTableRow( (*i)-&GT;m_row,
         ((*i)-&GT;m_columns)[ 0]-&GT;value, ((*i)->m_columns)[ 1]-&GT;value,
         ((*i)-&GT;m_columns)[ 2]-&GT;value, ((*i)->m_columns)[ 3]-&GT;value,
         ((*i)-&GT;m_columns)[ 4]-&GT;value);
   }
   return S_OK;
}

updateTableRowのインプリメンテーションは、最後の操作のセットの中のaddTableRowに似て います。ここでのポイントは、更新プロセスを単純化することにあります。

一歩前に戻って
3つのインプリメンテーション ステップ、すなわちインターフェイスの定義、ロジック コンポーネン トの構築、およびコントローラの構築によって、Separating Format from Logicデザイン パター ンは完成します。しかし、デザイン パターンの問題が解決されたことは、どうすればわかるので しょうか? われわれが行った作業は、動的な関連付けを持つデカップリングされたインターフェイ スというデザイン目標を達成したのでしょうか? 以下に、これらの目標について検討します。

動的な関連付け
インターフェイスとロジックの間の動的な関連付けはダイナミックHTMLスクリプティングによって 実現されています。ダイナミックHTMLにより、インターフェイスは個々の部品の組み合わせによ って構築されるため、UIを変更しながら、同じロジックを実行させることが可能になります。つまり、 ロジックに任意の言語をカップリングできるということです。

デカップリング
コントローラは特定のサービス インターフェイス、IControllerServiceを公開しなければならず、 コンポーネントはインターフェイスITimeCardを公開しなくてはなりません。元のインターフェイス がそのままの形で残っている限り、コンポーネントとコントローラは、別のコントローラまたはコン ポーネントと通信を行うように更新することができます。つまり、変更を加える必要が生じたときに は、部品ごとに変更していくことができます。

結論

Separating Format from Logicデザイン パターンの要件は満たされ、細かい粒度を持つ、より 単純なUIが実現されました。さらに、インターフェイスとロジック コンポーネントは互いに独立し ているので、今後もアプリケーションを簡単に強化していくことができます。 プロジェクトをビルドし、テストするには、次の操作を行います。

  1. プロジェクトtimetracker/timetracker.dswをロードします。
  2. 次のものをビルドします(順序は重要です):
  3. Webページtimetracker/testpage.htmをロードして、コンポーネントとコントローラをロードしま す。

関連情報

Microsoft Visual C++開発システムの最新情報については、World Wide Webサイト http://msdn.microsoft.com/visualc/(英語サイト)または http://www.microsoft.com/japan/developer/visualc/(日本語サイト)を参照してください。

Christian Grossは、企業顧客向けにインターネット関連のコンサルティングとトレーニング サー ビスを提供しているeuSOFT社に籍をおくパートナーで、アーキテクチャの問題を専門としていま す。Grossは、Visual C++ Developers Conference、DeveloperDays、TechEd、およびPDCな どのMicrosoftカンファレンスでよく講演を行っています。また、MINDやBasic Proなどのプログ ラマ向けの出版物の記事や、Microsoftのホワイト ペーパーを多数執筆しています。