March 2017

Volume 32 Number 3

パターン - Active Events: 複数ではなく 1 つの設計パターン

Thomas Hansen | March 2017

編集長より:

MSDN Magazine の上級寄稿編集者 James McCaffrey にこのコラムの草稿のレビューを依頼したところ、筆者が推進する意見や考え方の一部にかなりの不満を抱えたまま作業を終了しました。多くの場合、レビューでは草稿の校閲が中心になります。しかし、McCaffrey は、ソフトウェア エンジニアリングの新しいアイデアは、ただ新しいという理由だけで拒絶されることがあまりにも多すぎるとメモを残しています。McCaffrey はこのコラムの多くの記述にまだ不満を抱いていると話していますが、このコラムによってソフトウェアの設計と方法論のパラダイムについてさまざまな考え方が引き出される可能性があるという点で意見が一致しています。—Michael Desmond

ソフトウェアがあれば、その中には必ず失敗に終わるソフトウェアがあります。調査によると、全プロジェクトの 25 パーセントは完全な失敗で、50 パーセントは「問題あり」と示されています。 自動車メーカーが同じような統計をとり、製造した自動車のうち 25 パーセントが動かず、ライトを点けると炸裂し、50 パーセントは 1 時間に 25 マイルしか走行できず、1 マイルごとに 15 ガロンのガソリンを消費するところを想像してみてください。もっと重要なことに、自動車の代金を支払う前にどの車がうまく作動するか顧客にはわからないとしたらどうでしょう。今日のソフトウェア開発ではこのような事態が頻繁に起きています。

この事態を改善するためにさまざまなソリューションが提案されてきましたが、どの改善策も根本的な原因、つまりコードに踏み込んだものはありませんでした。 私見ですが、ソフトウェア プロジェクトが失敗する原因は、一般的に「スパゲティ コード」と呼ばれる 1 つの問題に集約されると考えます。また、コードがこれほどまでに問題を抱えた状態になっている原因は、実際に変更に対処する手段がないためです。エクストリーム プログラミングやアジャイル ソフトウェア開発などの方法論が推奨されてきたのはこのためです。ただし、これらのどの方法論も、適切なコードの作成を実際に支援できるわけではありません。アジャイルになれば、スパゲティ コードを魔法のように回避できるわけではありません。アジャイルになるためには、原子単位のビルディング ブロックに懸案事項を分離できる必要があります。

今回提案する内容は、アジャイルなどの方法論の代わりにはなりません。結局のところ、アジャイルは管理フレームワークであり、アーキテクチャの設計パターンではありません。代わりに、変更を前もって準備できるため、より簡単にアジャイルを実現できる設計の方法論を提案します。ここ数年、「Active Events」という設計パターンを開発してきました。 この設計パターンにより、拡張、変更、再利用、他のパーツとの交換を容易に行える、小規模で再利用可能なコンポーネントを作成できるようになります。この設計パターンではソフトウェアに新たな考え方を持ち込み、個々のパーツで LEGO を組み立てるかのように、コードを「編成」できるようにします。オブジェクト指向プログラミング (OOP) のパラダイムを使用したコードの作成とかなり似ていると感じるかもしれません。しかし、Active Events は、従来の OOP とは大きく異なります。

自身のコードのチェック

お願いがあります。 最近ビルドしたソリューションを Visual Studio で開いて、依存関係グラフをチェックしてみてください。そのソリューションには取り出せるプロジェクトとクラスがいくつありますか。 そのうち、将来のプロジェクトに再利用できるクラスはいくつですか。 ソフトウェアが機能を停止することなく他のクラスに置き換えられるクラスはいくつありますか。

おそらく、プロジェクトとクラスの依存関係は、他の多くのプロジェクトとたいして変わらないと思います。プロジェクトの依存関係グラフは、かなりスパゲティ状になっているのではないでしょうか。辛辣な言い方ですみませんが、筆者の経験からの推察です。けれど、安心してください。皆同じです。確実なデータはありませんが、全世界のコードの 98 パーセントが同じ問題に悩まされていると考えています。それに、解決法がないとしたら、この問題に向き合うことを強要するつもりはありません。

このような依存関係を何とか解決し、コードの個々のパーツが他のパーツを参照することがないように、再利用可能なコンポーネントを作成できれば、実質的に日々のプログラミングは改善されます。プロジェクトやクラスが現在のように絡み合っていなければ、より簡単に個々のパーツを交換できます。クラスどうしの絡み合いが解決されれば、将来のプロジェクトでそれらを再利用できます。プロジェクトが抱える依存関係を最小限に抑えれば、モノシリックな大規模設計ではなく、再利用しやすい、多種多様なコンポーネントが手に入ります。Active Events イベントは、クラス間やプロジェクト間の依存関係をなくす手助けをします。ソフトウェアの相互の絡み合いに関して言えば、容認できる数値は 0 しかありません。

Active Events のしくみ

設計パターン Active Events は、実は驚くほど簡単に理解できます。ロジックのパーツの実装と利用を分離するインターフェイスを作成するのではなく、メソッドを属性でマークするだけです。以下に例を示します。

[ActiveEvent (Name = “foo.bar”)]
protected static void foo_bar(ApplicationContext context, ActiveEventArgs e)
{
  var input = e.Args[“input”].Value as string;
  e.Args.Add(“output”, “Hello there ” + var);
}

この構文は、「Phosphorus Five」という筆者が作成したオープン ソースの Active Events の実装から引用しています。詳細については、本稿の「ほぼあり得ないソフトウェア」をご覧ください。

Active Events のパラダイムを使って定義しているメソッドを呼び出すのも、同様にシンプルです。メソッドを直接呼び出す代わりに、以下のようにアクティブ イベント (Active Event) の名前を使って間接的に呼び出します。

/* ... some method in another assembly ... */
var args = new Node ();
args.Add ("input", "John Doe");
/* context is your ApplicationContext instance, which keeps
   track of your Active Events */
var result = context.Raise ("foo.bar", args)["output"].Value as string;

Raise の呼び出しにより、アクティブ イベント foo.bar を呼び出しています。アクティブ イベントを呼び出している箇所とアクティブ イベントの実装の間に依存関係はありません。さらに、両者が共有する共通クラスもありません。データだけが、イベントに受け渡しされます。また、すべてのアクティブ イベントがまったく同じシグネチャになることもわかります。そのため、クラス、インターフェイス、メソッドが背後で機能していますが、ソース コード内ではあまり目立ちません。

アクティブ イベントという各パーツの読み込みは完全に動的に行われます。アプリケーションの構成ファイルで参照しているアセンブリのリストを指定するか、フォルダーからすべての DLL を自動的に読み込んで、アクティブ イベント ハンドラ―として登録します。内部では、アクティブ イベントの「カーネル」が、読み込み中に DLL 内の型をすべて詳しく調べ、文字列と MethodInfo のディクショナリに、アクティブ イベントのメソッドへの MethodInfo 参照を格納します。このディクショナリは、後からイベント呼び出し中に使用されます。アクティブ イベントにスコープを設定したり、アクティブ イベントに状態を関連付ける場合は、クラスのインスタンスをアクティブ イベント リスナーとして動的に追加し、静的メソッドの代わりにインスタンス メソッドをアクティブ イベント シンクとして使用できます。この例は、「Phosphorus Five」の plugins/p5.web プロジェクトで行っています。

これにより、さまざまなパーツが単純なビルディング ブロックになる環境が生み出されます。つまり、互いを間接的に呼び出し、任意の 1 つのアクティブ イベント アセンブリを他の実装と容易に交換できるようになります。出来上がるアプリケーションを完全に変えることは、構成の設定を変更したり、DLL を他の DLL と置き換える程度の簡単な処理になります。文字列「foo.bar」を「foo.bar-2」に変える程度の簡単な作業です。 同時に、ソリューション内の各アセンブリは、依然として他のアセンブリのメソッドを呼び出すことができます。その際、他のパーツと POD (Plain Old Data) 構造さえ共有する必要はありません。基本的には、不変の「さらにもう 1 つの抽象化層」を自由に使えます。言い換えれば、各パーツが原子化されます。

この状況を、従来の OOP で必要だったことと比べてみましょう。まず、インターフェイスによる設計手法を採用しているとすると、最低でもインターフェイスを 1 つとそのインターフェイスの実装を用意します。シナリオによっては、オブジェクトの作成に抽象ファクトリ方式を使用することも考えられます。その結果、2 つのクラスと 1 つのインターフェイスになります。これは合計 30 行を簡単に超えるコードになります。さらに、この実装を基に実際のプラグインを作成する場合、利用側パーツのプロジェクト (プラグインを利用するプロジェクト) と、実装側パーツのプロジェクト (インターフェイスを実装するクラスを含むプロジェクト) が 1 つずつ必要になります。また、完全なモジュール方式を採用する場合、インターフェイスと (場合によっては) 抽象ファクトリを格納する第 3 のプロジェクトを使用することもあります。結果として、3 つのプロジェクト間でコンポーネント相互参照が少なくとも 3 箇所になることがわかります。そのうえ、インターフェイスの入力パーツや出力パーツを追加または変更する場合、これら 3 つのプロジェクトをすべて変更する必要があります。これを先ほどアクティブ イベントの例で示した 1 行のソリューションと比べてみます。先程の例には、参照がまったくなく、入出力引数に指定する内容も気にする必要はありませんでした。従来の手法では、コード行数はすぐに 2 倍を超え、経験上、依存関係グラフは複雑になり、結果は柔軟性に欠ける複雑なコードになります。

OOP と OO の違い

OOP は大好きです。コード ロジック、メソッド、データを簡単にカプセル化できる優れたパラダイムです。ただし、OOP ではコーディング上の問題すべてを完全に解決することはできないという意見もあります。OOP を使用する際、場合によっては複数のクラスを定義する必要が生じます。それらのクラスは密接に結び付けられることが非常に多く、依存関係を生み出します。

OOP では、メソッドのシグネチャを把握する必要があります。インターフェイスにアクセスできるようにすることも必要です。さらに、このインターフェイスのインスタンスの作成方法も理解しなければなりません。また、インターフェイスの利用側と実装側の間で、型を共有する必要が生じることもよくあります。その結果、ビルディング ブロックの間に依存関係が生まれます。OOP を使用すると、シンプルな "Hello World" プラグインを作成するだけでも、複数のクラスを作成することになるのは珍しくありません。1 つの OOP インターフェイスを変更すると、極めて注意深くインターフェイスを設計していない限り、複数のアセンブリの再コンパイルが必要になるでしょう。コードの大部分が抽象インターフェイスと定型コードになる可能性があります。あるいは、インターフェイスでの変更によってドミノ倒しのように膨大な量のコード変更が追加で必要になる現実を受け入れなければなりません。過剰に複雑になり、退屈な反復作業が大量に必要になるたびに、開発者は悲鳴を上げることになります。

自問自答してください。 「OOP がオブジェクト指向 (OO) ソフトウェアを作成する機能を提供するとしたら、うまくいくのがそんなに難しいのはなぜでしょう」 私見ですが、OOP と OO は同じではありません。同じであるならば、OOP を最大限に活用するために、これほどたくさんの設計パターンを把握する必要はなくなります。

現在の実践方法では、OOP にはいくつか根本的制限事項があります。そのため、OOP を適切に使用するために実装しなければならない設計パターンはすべて、「OO ソフトウェアを提供できない」という、このアーキテクチャのもっと重大な根本的症状があると考えます。Active Events がこの問題を解決します。Active Events は、適切な抽象ファクトリや新しいクラス構造を実装するのではなく、メソッドや関数を呼び出す方法を変更することによって、こうした難しい問題を OOP から完全に取り除きます。OO ソフトウェアを作成するために必要なのは、抽象クラスやインターフェイスのインスタンスを作成する新しい方法ではありません。呼び出すすべてのインターフェイス メソッドに共通のシグネチャを 1 つ保持できるだけでなく、機能を呼び出す別のメカニズムです。

ほぼあり得ないソフトウェア

Active Events をどの程度まで活用できるかを示した例については「 Phosphorus Five 」 (github.com/polterguy/phosphorusfive) を確認してください。「Phosphorus Five」は大きなソフトウェア プロジェクトですが、Active Events の実装はわずか 1,000 行程度のコードです。core/p5.core で Active Events の実装が見つかります。

このプロジェクトでは、Active Events のフックを作成するために、新しいプログラミング言語 Hyperlambda を作成しました。これにより、必要であれば for-each ステートメントや while ステートメントの実装を変更できるようになっています。別のサーバーで実行するために、このプロジェクトの else ステートメントをスケールアウトすることも可能です。また、ドメインの問題で必要なことに対処するため、ドメイン固有のキーワードを作成して、プログラミング言語を簡単に拡張することもできます。

テキストのパーツとしてノード構造を動的に宣言できるファイル形式を作成して、ディスクやデータベースに保存することも可能です。サンプルでは、ツリーの任意のパーツ内のいずれかのノードを参照できるように、独自の式構文を作成しました。このため、非プログラミング言語と呼んでいますが、チューリング テストに 100 パーセント合格します。この非プログラミング言語は、動的に解釈されることも、静的にコンパイルされることもありません。また、他のプログラミング言語では数百行のコ―ドが必要になる処理でも、5 行のコードで実現できます。

Active Events を Hyperlambda とマネージ AJAX ライブラリ (AJAX ウィジェットを 1 つだけ含む) と組み合わせて、"Web OS" などの名前を持ち出さずには説明できないソフトウェアを何とか作成しています。 「Phosphorus Five」にはほぼ 30 のプロジェクトがあり、異なるプラグイン間での参照はありません。すべてのプロジェクトは p5.core (Active Events の実装) と p5.exp (式エンジン) を単純に参照します。これだけです。core フォルダー内のメイン Web サイト プロジェクト (p5.website) は、1 つのシンプルなコンテナー ウィジットのみを含み、ロジックはほぼ含みません。すべてのプラグインは、アプリケーションの起動時に、Global.asax に動的に読み込まれます。それでも、すべてのプロジェクトは、他のプロジェクト内部の機能を動的に呼び出します。参照も依存関係もありませんが、問題も生じません。

基本に立ち返る

問題があれば、必ず解決策があります。解決策として OOP が考案された共通の問題 (グローバル関数とデータ) は、逆接的に言えば、OOP で意図せず生まれた問題の解決策になります。Active Events 設計パターンを見て最初に気付くのは、ある程度基本に立ち返って、メソッドとクラスの代わりにグローバル関数を使用している点です。ただし、Active Events の利用側も実装側もシグネチャや型を認識することも共有することも必要ではないため、OOP よりもさらにブラック ボックス化された環境になります。その結果、たとえば、SaveToDatabase を InvokeWebService や SaveToFile と簡単に交換できるようになります。インターフェイスも、型も、POD 構造も、クラスもなく、あるのは共通の共有シグネチャが 1 つあるだけです。従来からのシンプルなデータだけです。必要なのはデータの受け渡しです。

ポリモーフィズムは、文字列を変更するくらい簡単です。以下は、Active Events を使用したポリモーフィズムの実装方法の例です。

string myEvent = "some-active-event";
if (usePolymorphism) {
  myEvent = "some-other-active-event";
}
context.Raise (myEvent);

このポリモーフィズム構成は、ベテランの設計者にとっては、滑稽なほど素朴で単純に見えるに違いありません。しかし、この単純さこそが、機能する理由です。Active Events により、データベースからでも、構成ファイルからでも、ユーザーがフォームのテキスト ボックス経由で名前を指定する方法でも、メソッドや関数の名前をフェッチできます。このことは、明示的なクラスを必要としないポリモーフィズムのバリエーションと考えることができます。これが、型を伴わないポリモーフィズムです。これが、実行中に動的に決定されるポリモーフィズムです。ポリモーフィズムに関する従来の考え方から難しい問題をすべて取り除き、ポリモーフィズムの基本をリファクタリングすることで、実際に有効に機能するポリモーフィズムになります。つまり、クラス、型、インターフェイス、設計パターンを必要としないカプセル化とポリモーフィズムになります。Active Events は原子を組み合わせて分子にするように、簡単に連鎖させることができます。これこそが、アジャイル ソフトウェアです。

最後に必要なグラフの種類 Node.cs

Active Events により、自身のアクティブ イベントアクティブ イベントからは自身のデータのみが送受信されます。これは、パーツどうしの疎結合を実現する方法です。そのためには、データ クラスが必要になります。このデータ クラスは、Active Events のパラダイムを採用する際に唯一必要になるグラフの種類です。このクラスは、可能性のあるすべてのクラスの可能性のあるすべてのフィールドとプロパティをラップできる必要があります。「Phosphorus Five」では、このクラスを Node.cs と呼んでいます。これは、キー/値/子の設計を含む単なるグラフ オブジェクトです。

Active Events の実装を成功させるには、Node クラスを、Active Events が入力を受け取って出力を返す唯一の引数にすることが鍵になります。偶然ですが、実際は、ほぼすべてのクラスを、キー/値/子のグラフ POD オブジェクトに集約できます。これにより、Active Events のアセンブリの動的な読み込みと組み合わせて、プロジェクト間の依存関係の数を大幅に削減できます。

Node.cs の実装では、キー (名前)、値 (任意のオブジェクト)、およびノードの "子" コレクションを保持できる必要があります。Node クラスがこれらの制約に従っていれば、可能性のあるほぼすべてのオブジェクトを 1 つの Node インスタンスに簡単に変換できます。JSON や XML に精通している方なら、この点での類似性がはっきりとわかるでしょう。以下は、Node クラスの構造を簡単に示す疑似コードです。

class Node
{
  public string Name;
  public object Value;
  public List<Node> Children;
}

内部処理としては、パーツ内で必要なだけ OOP を使用できます。ただし、あるパーツから別のパーツのロジックを呼び出す必要がある場合は、入力データをすべて Node インスタンスに変換しなければなりません。Active Events から情報を返す場合も、同様のことが必要です。しかし、パーツ内では、任意のクラス、インターフェイス、抽象ファクトリ、ファサード コンポーネント、シングルトン、設計パターンを必要に応じて自由に使用できます。一方、外部に対しては、Node が 1 つだけ存在し、Active Events がパーツ間のブリッジになります。リレーションの心理的なイメージ作りに役立つなら、Active Events をプロトコル、ノードをそのデータと考えてください。

まとめ

現在世界中に溢れる他の設計パターンをほぼすべて置き換えることを目指していますが、Active Events は特効薬ではありません。とりわけ、このテクノロジは幾分オーバーヘッドを伴います。たとえば、直接メソッドを呼び出す代わりに、ディクショナリ照合を行います。また、メソッドではなんらかのリフレクションも使用します。こうしたメソッドの間接呼び出しは、おそらく、従来の仮想メソッド呼び出しよりも、桁違いに高い負荷が掛かります。さらに、他のパーツとのインターフェイスをとる場合は、必ず、オブジェクトをノードやデータに変換することになります。

とは言え、Active Events によって、開発者の作業がすべて置き換えようとは考えていません。パーツ間により優れたインターフェイスを提供するという考え方なので、パフォーマンスのオーバーヘッドを主な考慮事項にはしていません。SaveToDatabase メソッドの呼び出しに 5 CPU サイクルかかるか、500 CPU サイクルかかるかは、実装に 5,000,000 CPU サイクルかかるとしたら問題にはなりません。 Donald Knuth はかつて、「未熟な最適化が諸悪の根源である」と発言しました。

インターフェイスの記述を検討する場合は、常に、代わりにアクティブ イベントを作成するほうが賢明ではないかと自問自答してみてください。Active Events である程度の経験を積めば、自問の答えは、次第に「そうする方がよさそうだ」に変わっていきます。 Active Events では、インターフェイスと抽象クラスが重点的に削減されます。

あまりにも大胆な表現に聞こえることはわかっていますが、「ほぼあり得ないソフトウェア」で取り上げた「Phosphorus Five」のプロジェクトを確認してください。 Hyperlambda (Active Events 用に作成した「言語」) では、オブジェクトを、ファイル、フォルダー、ラムダ コールバック、グラフ Node ツリーの一部、データベースから取得したテキストの一部、または HTTP 経由で送信されたデータの一部にすることができます。そして、コンピューターが理解できる実行ツリーのように、すべてのオブジェクトを実行できます。Hyperlambda では、理論上、万物についての究極の答えである数字「42」を解決できます。

7 年以上前に初めて Active Events を思い付き、本質的な美点を備えていると直感しました。問題は、60 年に及ぶこれまでのプログラミングの見識に挑戦することです。Active Events は、ベスト プラクティスでさえリファクタリングしなければならないポイントになります。それまでに習得した知識を捨て去り、自身の直感を信じるまでに 7 年かかりました。Active Events の最大の問題は、実際にはその実装にあるのではなく、開発者の考え方にあります。「Phosphorus Five」を 5 回にわたって完全に作り直した事実が、その証拠です。

本稿最初の草稿では、同じような内容を何度も作成しました。物理学や生物学など、他の科目に関する前提知識に切り込むことで、Active Events の優位性を読者に感じてもらいたいという思いがあります。2 回目の草稿では、読者を挑発しようと試みました。読者が間違いを証明したくなり、本稿の表現の中に問題点を探し出そうとしても、当然問題点は見つからないといって構成を考えました。しかし、3 回目の草稿と最終稿では、シンプルに Active Events について説明することに決めました。読者の頭の中ですんなりと受け入れられたなら、今後習得が必要になる設計パターンは、Active Events が最後になるでしょう。


Thomas Hansen は、1982 年に Oric-1 コンピューターを使用してコードを記述し始めた 8 歳の頃から、ソフトウェアを作成してきました。たまに、デメリットよりもメリットが上回るコードを作成しています。彼は、Web、AJAX、アジャイルの方法論やソフトウェア アーキテクチャに情熱を傾けています。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの James McCaffrey に心より感謝いたします。