従来の .NET ライブラリを移行して最新のプラットフォームをターゲットにする
Josh Lane
Microsoft .NET Framework の大きな強みの 1 つは、このプラットフォームをターゲットにした、サード パーティ製のオープン ソース ライブラリや商用ライブラリが豊富なことです。.NET 開発エコシステムが成熟している証拠に、.NET Framework そのものの優れた API を選択できるだけでなく、HTTP 要求への応答、デスクトップ アプリケーションでのグリッドの描画、構造化されたデータのファイル システムへの格納などを目的とした、何千ものマイクロソフト以外のライブラリも選択できます。実際、人気のある .NET コード リポジトリをざっと見ると、CodePlex には 32,000 以上のプロジェクト、code.msdn.microsoft.comには 5,000 以上のコード サンプル、NuGet ギャラリーには 10,000 以上のユニーク パッケージが見つかります。
Windows Phone 8 と Windows 8 という新しいソフトウェア プラットフォームが出現したことで、これらの実績のあるコードベースに新しい風が吹き込まれる可能性があります。デスクトップとサーバーで何年もの間役立ってきた .NET ライブラリは、新しいプラットフォームをターゲットにするために必要な移行の手間をいとわない限り、これまで同様 (場合によってはそれ以上に) これらの新しい環境で役立ちます。このような作業は従来は難しく退屈でしたが、現在では、成功を約束するにはやはり配慮と明確な計画が必要なものの、Visual Studio 2012 には、潜在的な課題を最小化してプラットフォーム間での再利用の機会を最大化する機能が複数用意されています。
この記事では、私がオープン ソースの Sterling NoSQL オブジェクト指向データベース (OODB) プロジェクトを実際に移行しているときに直面した課題について説明します。まずライブラリの概要を説明し、次に移行時に直面した課題とその解決策を紹介します。最後にまとめとして、皆さんのライブラリ移行作業でアドバイスとして活用できるパターンとベスト プラクティスについて検討します。
Sterling とは
Sterling は、.NET クラス インスタンスをすばやくインデックスを使用して取得できる、軽量の NoSQL データ ストレージ ライブラリです。更新、削除、バックアップと復元、切り捨てなどはサポートされますが、他の NoSQL テクノロジと同様に、汎用の SQL 言語ベース クエリ機能は提供されません。その代わり、"クエリ" の概念は、次のような明確で順序付けられた一連の操作で構成されています。
- 最初に、遅延読み込みされるクラス インスタンスにマッピングされた、定義済みのキーまたはインデックスのコレクションを取得します。
- 次に、結果セットの候補全体を迅速に初期フィルター処理するために、キーのコレクションにインデックスを付けます。
- 最後に、フィルター処理したキーと値のペアに対して標準的な LINQ to Objects クエリを使用して、さらに結果を絞り込みます。
Sterling (および類似の NoSQL データベース) の使用モデルは、SQL Server のような従来のリレーショナル データベースの使用モデルとは明らかに異なります。正式で明確なクエリ言語がないことは、初心者にとって特に奇妙に思えるでしょう。実のところ、NoSQL の支持者は、クエリの世界とコードの世界の間での入出力のマッピングに関する潜在的な複雑さとオーバーヘッドを考えれば、この特徴は強みであると主張しています。Sterling などの NoSQL ソリューションでは、クエリとコードが同一の言語なので、マッピングはありません。
Sterling の詳細についてこの記事では説明しませんが (詳細については、Jeremy Likness による記事「Windows Phone 7 の分離ストレージ用の Sterling」(msdn.microsoft.com/magazine/hh205658を参照してください)、心に留めていただきたい主要なメリットとトレードオフをここで紹介します。
- フットプリントが少なく (ディスク上で約 150 KB)、インプロセス ホスティングに最適です。
- シリアル化可能な .NET 型の標準的な範囲ですぐに使用できます。
- 基本機能の使用に必要な概念が少なく、5 行の C# コードでも Sterling を適切に実行できます。
- 詳細なセキュリティ、連鎖更新と連鎖削除、構成可能なロック セマンティクス、原子性、一貫性、分離性、持続性 (ACID) の保証など、従来のデータベース機能は Sterling ではサポートされていません。これらの機能が必要な場合は、SQL Server のような完全なリレーショナル エンジンを検討する必要があります。
Sterling の作成者 (Wintellect での私の同僚である Jeremy Likness) は、当初から複数のプラットフォームをターゲットにすることを意図しており、.NET Framework 4、Silverlight 4、Silverlight 5、および Windows Phone 7 用にバイナリを作成しました。そのため、.NET Framework 4.5、Windows Phone 8、および Windows ストア アプリをターゲットにするよう Sterling を更新するために必要な作業を検討したとき、私はこのアーキテクチャが更新作業に適していることは知っていましたが、プロジェクトに何が必要か正確には把握していませんでした。
この記事で紹介するアドバイスは、.NET Framework 4.5、Windows Phone 8、および Windows ストア アプリをターゲットにするために Sterling を更新した経験から直接得られたものです。この経験の細部には、Sterling プロジェクト固有の部分もありますが、他の大半はマイクロソフト エコシステムの幅広いプロジェクトや移植作業に関連しています。
課題と解決方法
新しいターゲット プラットフォームに Sterling を移植した際に直面した課題を思い返すといくつかの大雑把なカテゴリが明らかになり、これらのカテゴリに基づいて、直面した課題をまとめることができ、この種のプロジェクトに取り組むすべての人により汎用性が高いガイダンスを提供できます。
さまざまな設計哲学への対応: 1 種類目の潜在的な問題は、実施する移行作業全体に対して実際に影響を及ぼしますが、本来はかなり哲学的です。「移行するライブラリのアーキテクチャと設計は、新しいターゲット プラットフォームの共通パターンと使用モデルにどの程度対応しているか」と自分に問いかけてください。
この問いは簡単に答えられるものではなく、適切でどんな場合にも適用できる解答はなかなか見つかりません。優れたカスタム Windows フォーム レイアウト マネージャーでさえ、Windows Presentation Foundation (WPF) への移植が難しいか不可能な場合があります。確かに API は異なっていますが、長期的な失敗の原因になりやすいのは、これらの 2 つの世界で、中心的な設計哲学と、コントロールの管理や位置付けの概念がまったく異なる点です。別の例を挙げると、キーボードとマウスを使用した従来の入力スタイルで適切に機能するカスタム UI 入力コントロールは、Windows Phone や Windows 8 などのタッチが主体の環境では UX が低下することがあります。コードベースを移行するという願望だけでは不十分で、新旧プラットフォーム間の基盤となる設計の互換性に加え、存在するあらゆるわずかな差異を調整する意欲が必要です。Sterling の場合、このようなジレンマをいくつか解決する必要がありました。
最も顕著な設計上の問題は、同期的な Sterling データ更新 API と、Windows Phone 8 アプリや Windows ストア アプリをターゲットにしたライブラリで対応する動作に想定される、非同期な性質との不一致でした。数年前に Sterling が設計された当時は、非同期 API がまだ珍しく、非同期 API を作成するツールや技術は、あったとしても未熟なものでした。
移行前の Sterling における典型的な Save メソッド シグネチャを次に示します。
object Save<T>(T instance) where T : class, new()
ここで重要なのは、このメソッドが同期的に実行されることです。つまり、インスタンス引数の保存にかかる時間に関係なく、呼び出し元がブロックされて、メソッドが完了するまで待機します。このため、応答しない UI、サーバーのスケーラビリティの大幅な低下など、スレッドのブロックに関するお決まりの一連の問題が発生するおそれがあります。
ここ数年で、応答性の高いソフトウェア設計に関するユーザーの期待は非常に高まっています。保存処理が完了するまで数秒間 UI が停止するのを甘受するユーザーはいません。これに応えて、Windows Phone 8 や Windows 8 のような新しいプラットフォーム用の API 設計ガイドラインでは、Save などのパブリック ライブラリ メソッドを非同期でブロックしない操作にするよう規定しています。ありがたいことに、.NET のタスクベースの非同期パターン (TAP) プログラミング モデルや、C# の async キーワードと await キーワードなどの機能があるので、このような規定の達成が簡単になっています。更新後の Save のシグネチャを次に示します。
Task<object> SaveAsync<T>(T instance) where T : class, new()
このシグネチャでは、Save はすぐに応答するようになり、呼び出し元には、結果 (この場合、新しく保存したインスタンスの一意のキー) の最終的な取得に使用する待機可能なオブジェクト (Task) があります。バックグラウンドで保存処理が完了するまでの間、呼び出し元で他の処理がブロックされることはありません。
明確さを期すために付け加えますが、ここではメソッド シグネチャだけを示しました。メソッド内部を同期実装から非同期実装に変換するには、ターゲット プラットフォームごとに追加のリファクタリングと非同期ファイル API への切り替えが必要でした。たとえば、Save の同期実装では、次のようにファイル システムへの書き込みに BinaryWriter を使用していました。
using ( BinaryWriter instanceFile = _fileHelper.GetWriter( instancePath ) )
{
instanceFile.Write( bytes );
}
しかし、BinaryWriter では非同期セマンティクスがサポートされていないので、この実装をリファクタリングして、各ターゲット プラットフォームに適した非同期 API を使用できるようにしました。たとえば、図 1 は、SaveAsync で Sterling の Windows Azure テーブル ストレージ ドライバーを検索する方法を示しています。
図 1 SaveAsync で Sterling の Windows Azure テーブル ストレージ ドライバーを検索する方法
using ( var stream = new MemoryStream() )
{
using ( var writer = new BinaryWriter( stream ) )
{
action( writer );
}
stream.Position = 0;
var entity = new DynamicTableEntity( partitionKey, rowKey )
{
Properties = new Dictionary<string, EntityProperty>
{
{ "blob", new EntityProperty( stream.GetBuffer() ) }
}
};
var operation = TableOperation.InsertOrReplace( entity );
await Task<TableResult>.Factory.FromAsync(
table.BeginExecute, table.EndExecute, operation, null );
}
ここでも BinaryWriter を使用してメモリ内ストリームに不連続値を書き込んでいますが、その後は Windows Azure DynamicTableEntity、CloudTable.BeginExecute、および CloudTable.EndExecute を使用して、ストリームのバイト配列の内容を Windows Azure テーブル ストレージ サービスに非同期に格納しています。Sterling の非同期データ更新動作を実現するには、他にもいくつか同様の変更が必要でした。重要な点は、表面的なレベルの API リファクタリングは、このような再設計の目標を達成するために必要な段階のうち、最初の数段階にすぎないことです。作業内容とそれに応じた労力の見積もりを計画し、その変更がそもそも合理的な目標かどうか現実的に考えてください。
実際、Sterling に関する私の経験では、このような非現実的な目標が判明しました。Sterling の中心的な設計特性とは、すべてのストレージ操作が、標準的な .NET データ コントラクト シリアル化 API および拡張機能を使用して、厳密に型指定されたデータに対して実行されることです。この特性は、Windows Phone、.NET 4.5 クライアント、および C# ベースの Windows ストア アプリでは適切に機能します。しかし、Windows ストア アプリの HTML5 クライアントや JavaScript クライアントの世界には、厳密な型指定の概念はありません。Likness との調査と議論の結果、私はこれらのクライアントで Sterling を使用できるようにする簡単な方法はないと判断し、これらのクライアントをサポート オプションから除外することにしました。もちろん、このような潜在的な不一致はその都度検討する必要がありますが、不一致が発生する可能性を念頭に置いて、選択肢を現実的に考えてください。
ターゲット プラットフォームでのコードの共有: 私が直面した次の大きな課題は、だれもがいつか体験する課題、つまり複数のプロジェクトで共通のコードを共有する方法でした。
プロジェクトで共通のコードを特定して共有することは、製品化に要する時間とダウンストリームにおけるメンテナンスの問題を最小化するための、実績のある手法です。この手法は .NET で何年も実施されています。典型的なパターンでは、Common アセンブリを定義し、これを複数の消費者向けプロジェクトから参照します。もう 1 つのお気に入りの手法は、Visual Studio の "リンクとして追加" 機能です。この機能を使用すると、図 2 に示すように、1 つのマスター ソース ファイルを複数のプロジェクトで共有できます。
図 2 Visual Studio 2012 の "リンクとして追加" 機能
現在でも、すべての消費者向けプロジェクトが同じ基盤となるプラットフォームをターゲットにしていれば、これらの選択肢は有効です。しかし、(今回の Sterling の場合のように) 複数のプラットフォームに共通の機能を公開する必要がある場合、そのコード用に 1 つの Common アセンブリを作成することは、開発の大きな負担になります。複数のビルド ターゲットの作成とメンテナンスが不可欠になるので、プロジェクト構成とビルド プロセスの複雑さが増大します。プリプロセッサ ディレクティブ (#if、#endif など) を使用して特定のビルド構成向けのプラットフォーム固有の動作を条件に応じて含めることは、事実上必要な処理ですが、コードの判読、移動、および推測が難しくなります。このような構成の課題に費やす労力は、コードを使って本当の問題を解決するという当初の目標からかけ離れています。
さいわい、マイクロソフトはより簡単なクロスプラットフォーム開発に対する需要を予期していたので、.NET Framework 4 から、ポータブル クラス ライブラリ (PCL) と呼ばれる新しい機能を追加しました。PCL を使用すると、1 つの Visual Studio .NET プロジェクトで、複数バージョンの .NET Framework、Silverlight、Windows Phone、Windows ストア、および Xbox 360 を選択的にターゲットにできます。PCL プロジェクト テンプレートを選択すると、Visual Studio によって自動的に、選択した各ターゲット プラットフォームに存在するライブラリのみがコードで使用されるようになります。このため、扱いにくいプリプロセッサ ディレクティブや複数のビルド ターゲットが必要なくなります。一方、ライブラリから呼び出せる API に関してはいくらか制限があります。この制限を回避する方法については後で詳しく説明します。PCL の機能と使用法の詳細については、「.NET Framework を使用したクロスプラットフォーム開発」(msdn.microsoft.com/library/gg597391、英語) を参照してください。
PCL は、Sterling によるクロスプラットフォームの目標達成に適していました。Sterling コードベースの 90% 以上を、変更することなく .NET Framework 4.5、Windows Phone 8、および Windows 8 で共通して使用できる 1 つの PCL にリファクタリングできました。これは、プロジェクトの長期的な存続可能性に関する大きなメリットです。
ここで、単体テスト プロジェクトについて簡単に注意を述べておきます。現在、PCL には単体テスト コードに相当する機能がありません。このような PCL の作成における最大の課題は、複数のプラットフォームで機能する単一の統合された単体テスト フレームワークがないことです。この現状から、Sterling の単体テストについては、私は .NET 4.5、Windows Phone 8、および Windows 8 用に個別のテスト プロジェクトを定義しました。この手法では、.NET 4.5 プロジェクトにはテスト コードの完全なコピーを含め、他のプロジェクトでは前述の "リンクとして追加" 手法を使用してテスト コードを共有します。各プラットフォーム プロジェクトでは、そのプラットフォームに固有のテスト フレームワーク アセンブリを参照します。さいわい、名前空間と型名は各プラットフォームで同一なので、すべてのテスト プロジェクトで同じコードが修正されずにコンパイルされます。このしくみの例については、GitHub (bit.ly/YdUNRN、英語) で更新済み Sterling コードベースを参照してください。
プラットフォーム固有の API の利用: PCL は統一されたクロスプラットフォーム コードベースの作成に非常に便利ですが、PCL コードから呼び出せないプラットフォーム固有の API を使用するにはどうすればよいかという、ちょっとしたジレンマも発生します。その良い例が、前述の非同期コードのリファクタリングです。特に .NET 4.5 および Windows ストア アプリについてはさまざまな強力な非同期 API を選択できますが、PCL から呼び出せる API はありません。このトレードオフをうまく解決できるでしょうか。
実は、少しの作業で解決できます。考え方としては、直接呼び出せないプラットフォーム固有の動作をモデル化するインターフェイスを 1 つ以上 PCL の内部に定義し、こうしたインターフェイスの抽象化の観点から PCL ベースのコードを実装します。その後、個別のプラットフォーム固有ライブラリで、インターフェイスごとの実装を指定します。最後に、実行時に PCL 型のインスタンスを作成してにんいの処理を実行し、現在のターゲット プラットフォームに適した固有のインターフェイス実装を組み込みます。抽象化を使用することで、PCL コードをプラットフォームの詳細から切り離したままにできます。
ここまでの内容に聞き覚えがあるとしたら、それはここで述べたパターンが、制御の反転 (IoC) として知られる、モジュール式の切り離しと分離を実現するための実績のあるソフトウェア設計手法だからです。IoC の詳細については、ja.wikipedia.org/wiki/%E5%88%B6%E5%BE%A1%E3%81%AE%E5%8F%8D%E8%BB%A2を参照してください。
Sterling の移植では、このアプローチを使用して何件もの API の互換性に関する問題を解決しました。問題になった API の大半は、System.Reflection 名前空間に含まれていました。皮肉なことに、各ターゲット プラットフォームでは Sterling に必要なすべてのリフレクション機能が公開されていますが、各プラットフォームにわずかな癖や微妙な差異が存在するため、PCL で機能を一貫してサポートすることができません。そのため、この IoC ベースの手法が必要になります。bit.ly/13FtFgO(英語) にアクセスすると、これらの問題を回避するために Sterling 用に定義した最終的な C# インターフェイスの抽象化を確認できます。
全般的なアドバイス
Sterling の移行手法の概要について説明してきましたが、ここで少し戻って、この経験から全般的な場合に適用できる教訓を考えましょう。
まず、どんなに強調しても足りないくらいですが、PCL 機能を使用してください。PCL は、クロスプラットフォーム開発で大成功を収めており、たいていのニーズに対応できるほど構成が柔軟です。既存のライブラリを移行 (または新しいライブラリを作成) しているところで、そのライブラリが複数のプラットフォームをターゲットにしている場合は、PCL を使用する必要があります。
次に、更新した設計目標に対応するための、一定のリファクタリング作業を想定してください。言い換えると、コード移行を、ある API 呼び出しを別の API 呼び出しと置換するような単純で機械的なプロセスと想定しないでください。必要な変更が表面的なレベルにとどまらない可能性や、元のコードベース記述時の中心的な想定を 1 つ以上変更する必要がある可能性は、十分にあります。ダウンストリームに大きな影響を与えずに既存のコードに適用できるコード チャーンの合計には、実際的な制限があります。制限がどこにあるのか、制限をいつ超えるのか、および制限を超えているかどうかについては、自分で判断する必要があります。ある人の移行作業は、他の人のソース コードでまったく新しいプロジェクトへの分岐を引き起こすものです。
最後に、自分の引き出しに持っている、既存のパターンや設計手法をあきらめないでください。この記事では、Sterling で IoC の原則と依存関係の挿入を組み合わせて使用して、プラットフォーム固有の API を活用する方法を示しました。他の類似アプローチも、間違いなく適切に機能するでしょう。Strategy (ja.wikipedia.org/wiki/Strategy_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3)、Adapter (ja.wikipedia.org/wiki/Adapter_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3)、Template Method (ja.wikipedia.org/wiki/Template_Method_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3)、Façade (ja.wikipedia.org/wiki/Facade_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3) などの古典的なソフトウェア設計パターンは、新しい目的に合わせて既存のコードをリファクタリングする場合に非常に便利です。
すばらしい新世界
移行作業の最終的な結果として、.NET Framework 4.5、Windows 8、および Windows Phone 8 の 3 つのターゲット プラットフォームに、完全に機能する Sterling NoSQL 実装ができあがりました。手元の Surface タブレットや Nokia Lumia 920 スマートフォンなど、Windows ベースの最新デバイスで Sterling が実行されているのを見るのはとてもうれしいものです。
この Sterling プロジェクトは、Wintellect GitHub サイト (bit.ly/X5jmUh、英語) にホストされており、完全な移行済みソース コードと、各プラットフォーム用の単体テストやサンプル プロジェクトが含まれています。また、Windows Azure テーブル ストレージを使用する、Sterling ドライバー モデルの実装も含まれています。GitHub リポジトリを複製し、この記事で紹介したパターンや設計の選択肢を試してください。皆さんが同様の移行作業を行うときに、このリポジトリが有効な出発点となることを願っています。
くれぐれも、以前のコードを破棄するのではなく、移行してください。
Josh Lane は、アトランタに拠点を置く Wintellect LLC のシニア コンサルタントです。マイクロソフト プラットフォームでのソフトウェアのアーキテクチャ、設計、および構築に 15 年間携わっており、コール センターの Web サイトからカスタム JavaScript コンパイラまで、幅広いテクノロジ ソリューションを実現してきました。彼は、ソフトウェアを通じて有意義なビジネス価値を生み出すという難題を楽しんでいます。連絡先は jlane@wintellect.com(英語のみ) です。
この記事のレビューに協力してくれた技術スタッフの Jeremy Likness (Wintellect) に心より感謝いたします。