要旨
アーキテクチャは一般に、Word文書に主として見られるような極めて実体のない、ソフトウェアシステムの概念的な側面であるか、または完全に技術によって駆動されるものかのいずれかです(「私たちはXMLアーキテクチャを使用します」)。そのどちらも間違っています。前者はアーキテクチャを扱い難くし、後者は技術のハイプの背後にアーキテクチャ概念を隠してしまいます。
では、どう対処すればよいでしょうか? アーキテクチャの開発と同時に、このアーキテクチャに基づいてシステムを記述できる言語を進化させてください。実際の多数のプロジェクトにおける私の経験からすると、これによってアーキテクチャが実体化され、技術決定とは切り離したままで(自覚して別の手順で実行できます)、具体的なシステムおよびアーキテクチャ構成要素の明確な記述が可能になります。
この記事の最初のパートでは、実際のストーリーを使ってアイデアを説明します。2つめのパートでは、アプローチのキーポイントを要約します。
ストーリー
システム背景
私は、通常のコンサルティング業務の1つにおいて、ある顧客と一緒にいました。顧客は、新しい飛行管理システムを構築したいという結論に達しました。航空会社は、このようなシステムを、飛行機が所定の空港に着陸したかどうかや、遅延状況や、航空機の技術の現状などに関するさまざまな情報を追跡および公開するために使用します。また、飛行管理システムは、インターネットや空港の情報モニターなどにおけるオンライン追跡システムを実装します。このシステムは、いろいろな意味で典型的な分散システムであり、システム全体のさまざまな部分を実行する多数のマシンで構成されています。大量の演算処理の一部を実行する中央のデータセンターがありますが、それに加えて、比較的広い地域にわたって分散されたマシンもあります。
私の顧客は長年このようなシステムを構築してきており、次世代システムの導入を計画しているところでした。新システムは15~20年の期間にわたり発展できる必要がありました。この要件だけからしても、ある種の技術の抽象化が必要であることが明らかでした。なぜなら、技術はおそらく15~20年にわたり8つのハイプサイクルを経過するからです。技術を抽象化するもう1つのもっともな理由は、システムのさまざまな部分がさまざまな技術(Java、C++、C#)で構築されるだろうということです。いずれにせよ、これは大規模な分散システムにとって一般的でない要件とはいえません。多くの場合、バックエンドにはJava技術を使用し、Windowsフロントエンドには.NET技術を使用しています。
システムの分散した性質により、システムのすべての部分を同時に更新することは不可能です。そのため、システムを要素ごとに発展させることができる必要性が生じ、さらにそれによってさまざまなシステムコンポーネントのバージョニング(コンポーネントAとコンポーネントBとの通信が、Bが新バージョンにアップグレードされた後も可能になるようにすること)の必要性が生じました。
開始点
私がプロジェクトに就いたとき、すでに顧客はシステムのバックボーンをメッセージング基盤にすることを決定しており(この種のシステムには良い決定です)、さまざまなメッセージングバックボーンのパフォーマンスとスループットを評価していました。また、このシステムで扱うデータを記述するためのシステムワイドな(システム全体を網羅する)ビジネスオブジェクトモデルを選ぶことをすでに決定していました(実のところ、この種のシステムにはあまり良い決定ではありませんが、このストーリーの落ちには重要でありません)。
したがって、私がプロジェクトに就いたとき、顧客はシステムの全詳細とすでに行ったアーキテクチャに関する決定について私に概要を説明し、簡単に言えば、そのすべてが道理にかなっているかどうかを尋ねてきました。顧客は、多数の要件を把握しており、特定のアーキテクチャ側面に関するピンポイントの決定を行う一方で、私が一貫したアーキテクチャと呼ぶもの、すなわち、実際のシステムを構築するビルディングブロックの定義(要素の種類)を有していないことがすぐに判明しました。顧客は、システムについて解説するための言語がありませんでした。
これは、私が手助けとしてプロジェクトに就く際、実際に非常によく見られることです。そしてもちろん、私が非常に大きな問題だと思っていることがまさにそれです。もし、システムを構成している要素の種類を知らなければ、一貫性を維持してシステムについて実際に解説することや、システムを記述することや、構築することは非常に困難です。言語を定義する必要があります。
背景: 言語とは?
アーキテクチャの観点からシステムについて解説する言語がある場合は、一貫したアーキテクチャを備えているということはお分かりでしょう1。では、言語とは何でしょうか? 言うまでもなく、第1に言語とは明確に定義された用語のセットです。「明確に定義された」とは主として、すべての利害関係者が用語の意味に同意することを意味します。形式張らない視点から言語を見た場合、用語とその意味だけでおそらくすでに言語を定義するのに十分でしょう。
しかし、意外と思われるかもしれませんが、私はアーキテクチャの記述に関して形式的な言語を支持しています2。形式言語を定義するには、複数の用語と意味が必要です。それらの用語から「文」(すなわち、モデル)を構築する方法を表す文法が必要であり、さらに、それらを表現するための具象構文が必要です3。
アーキテクチャの記述に形式言語を使用することは、いくつかの利点があります。それについては、このストーリーの続きの部分で明らかになります。また、この記事の終盤で利点を要約しています。
アーキテクチャ記述用の言語の開発
ストーリーの続きを進めましょう。私と顧客は、いくつかの技術的要件を調べることに1日を費やした結果、それらの要件を実現できるアーキテクチャ用の形式言語を構築する価値があるかもしれないことに同意しました。私たちは、アーキテクチャについて議論したとおり、文法、いくつかの制約、およびエディタ(oAWのXtextツールを使用)を実際に構築することになりました。
開始
私たちはコンポーネントの概念から取り掛かりました。その時点では、コンポーネントの概念は比較的緩く定義されています。それは単に、アーキテクチャ上関連する最小のビルディングブロックで、カプセル化されたアプリケーション機能の1要素でした。また、私たちは、OOプログラミングのクラスとアーキテクチャ面で同等になるよう、コンポーネントをインスタンス化できることを想定しました。ここに、私たちが定義した初期の文法に基づいて構築した初期のモデル例を示します。
component DelayCalculator {}
component InfoScreen {}
component AircraftModule {}
ここで、次の2つのことを私たちがどのように行ったかに注目してください。まず、コンポーネントの概念が存在することを定義しました(コンポーネントを、構築しようとしているシステムのビルディングブロックにする)。そして、DelayCalculator、InfoScreen、およびAircraftModuleの3つのコンポーネントがあることを(あらかじめ)決めました。アーキテクチャのビルディングブロックのセットを概念アーキテクチャと呼び、それらのビルディングブロックの具体的な例のセットをアプリケーションアーキテクチャと呼びます4。
インターフェース
もちろん、コンポーネントは相互作用できないため、上記のコンポーネントの概念はほぼ役に立ちません。DelayCalculatorはAircraftModuleから情報を受け取り、フライトの遅延状況を計算し、その結果をInfoScreenに転送する必要があることは、この領域から明らかです。私たちは、それらのコンポーネントがどうにかしてメッセージ交換するだろうと分かっていましたが(注: すでにメッセージングの決定は行っていました)、メッセージを導入するのではなく、関連するメッセージのセットをインターフェースに抽象化することに決めました5。
component DelayCalculator implements IDelayCalculator {}
component InfoScreen implements IInfoScreen {}
component AircraftModule implements IAircraftModule {}
interface IDelayCalculator {}
interface IInfoScreen {}
interface IAircraftModule {}
上記のコードはJavaコードにかなり似ていることに気付きました。私の顧客はJavaの経験があり、システムの主要ターゲット言語がJavaであったため、驚くべきことではありません。扱いに慣れている言語からのよく知られた概念が、私たち自身の言語にも浸透したのでしょう。しかし、これがあまり役に立たないことにすぐに気付きました。コンポーネントが特定のインターフェースを使用することを(提供することとは対照的に)表現することができませんでした。コンポーネントのインターフェース要件について知ることは重要です。コンポーネントが持つ依存性を理解(その後、ツールで分析)できるようになりたいからです。これは、どんなシステムにも重要ですが、バージョニング要件において特に重要です。
そこで、文法を少し変更し、次の表現をサポートしました。
component DelayCalculator {
provides IDelayCalculator
requires IInfoScreen
}
component InfoScreen {
provides IInfoScreen
}
component AircraftModule {
provides IAircraftModule
requires IDelayCalculator
}
interface IDelayCalculator {}
interface IInfoScreen {}
interface IAircraftModule {}
システムの記述
次に、これらのコンポーネントがどのように使用されるかに注目しました。すると、コンポーネントをインスタンス化可能にする必要があることがすぐに明らかになりました。当然、多くの航空機が存在し、それぞれがAircraftModuleコンポーネントを実行し、さらに多くのInfoScreensが存在します。多数のDelayCalculatorを保持するかどうかは完全には明らかになっていませんでしたが、私たちはこの議論を延期して、インスタンス化の考えに取り組むことに決めました。
そのため、私たちは、コンポーネントのインスタンスを表現できるようになる必要がありました。
instance screen1: InfoScreen
instance screen2: InfoScreen
...
次に、システムの「配線」方法について議論しました。つまり、特定のInfoScreenが特定のDelayCalculatorと通信することを、どのように表現するかについてです。どうにかして、インスタンス間の関係を表現する必要があります。タイプはそれぞれ「互換性のある」インターフェースを備えており、DelayCalculatorはInfoScreenと通信することができました。しかし、この「通信」関係はまだ把握しにくいものでした。また、一般に1つのDelayCalculatorインスタンスは多数のInfoScreenインスタンスと通信することに気付きました。そのため、カーディナリティ(基数)はどうにか言語を習得する必要がありました。
あれこれ考えた後、私はポートの概念を紹介しました(これは実際にコンポーネント技術やUMLでもよく知られている概念ですが、私の顧客には比較的新しい考えでした)。ポートは、コンポーネントタイプ上で定義された通信終端であり、所有コンポーネントがインスタンス化されるたびにインスタンス化されます。そこで、コンポーネント記述言語をリファクタリングして次を表現できるようにしました。ポートは、providesおよびrequiresキーワードで定義され、その後にポート名、コロン、ポートに関連付けされたインターフェースが続きます。
component DelayCalculator {
provides default: IDelayCalculator
requires screens[0..n]: IInfoScreen
}
component InfoScreen {
provides default: IInfoScreen
}
component AircraftModule {
provides default: IAircraftModule
requires calculator[1]: IDelayCalculator
}
上記のモデルは、どのDelayCalculator インスタンスも多数のInfoScreensと接続することを表しています。DelayCalculator implementation実装コードの観点からすると、このInfoScreensのセットにはscreensポートを介して対処できます。AircraftModuleは1つのDelayCalculatorと通信する必要があり、[1]が表現しているものがそれに相当します。
インターフェースのこの新しい概念は、私の顧客にIDelayCalculatorを変更する気を起こさせました。なぜなら、通信パートナーによって異なるインターフェース(したがって、ポート)が必要であることに気付いたからです。私たちは、アプリケーションアーキテクチャを次のように変更しました。
component DelayCalculator {
provides aircraft: IAircraftStatus
provides managementConsole: IManagementConsole
requires screens[0..n]: IInfoScreen
}
component Manager {
requires backend[1]: IManagementConsole
}
component InfoScreen {
provides default: IInfoScreen
}
component AircraftModule {
requires calculator[1]: IAircraftStatus
}
今では特定の役割を果たすインターフェース(IAircraftStatus, IManagementConsole)を持っているため、ポートの導入によって、どのようにより優れたアプリケーションアーキテクチャがもたらされたかに注目してください。
ポートがある今、通信終端に名前を付けることが可能になりました。これにより、システム(コンポーネントの接続インスタンス)を容易に記述できるようになりました。新しいconnect コンストラクタに注目してください。
instance dc: DelayCalculator
instance screen1: InfoScreen
instance screen2: InfoScreen
connect dc.screens to (screen1.default, screen2.default)
概要の維持
当然ある時点で、すべてのコンポーネント、インスタンス、およびコネクタで迷うことがないように、何らかの名前空間の概念を導入する必要があることが明白になりました。そしてもちろん、私たちは、要素を異なったファイルに分散することができます(ツールサポートは、go to definitionとfind referencesが機能し続けることを確実にします)。
namespace com.mycompany {
namespace datacenter {
component DelayCalculator {
provides aircraft: IAircraftStatus
provides managementConsole: IManagementConsole
requires screens[0..n]: IInfoScreen
}
component Manager {
requires backend[1]: IManagementConsole
}
}
namespace mobile {
component InfoScreen {
provides default: IInfoScreen
}
component AircraftModule {
requires calculator[1]: IAircraftStatus
}
}
}
当然ながら、コンポーネントとインターフェース定義(基本: タイプ定義)をシステム定義(接続インスタンス)と切り離して維持することは名案です。そこで、システムを次のように定義します。
namespace com.mycompany.test {
system testSystem {
instance dc: DelayCalculator
instance screen1: InfoScreen
instance screen2: InfoScreen
connect dc.screens to (screen1.default, screen2.default)
}
}
実際のシステムでは、DelayCalculator はランタイム時に利用可能なすべてのInfoScreen を動的に検出する必要があります。手動でそれらの接続を記述することは無意味です。そこで、さぁ始めましょう。何らかの命名/トレーダー/ルックアップ/レジストリインフラストラクチャに対してランタイム時に実行されるクエリーを指定します。そのクエリーは、オンラインになったInfoScreen を検出するために60秒ごとに再実行されます。
namespace com.mycompany.production {
instance dc: DelayCalculator
// InfoScreen instances are created and
// started in other configurations
dynamic connect dc.screens every 60 query {
type = IInfoScreen
status = active
}
}
同様のアプローチを使用して、負荷分散または耐障害性を実現できます。静的なコネクタはプライマリインスタンスとバックアップインスタンスを指し示すことができます。また、動的クエリーは、現在使用されているコンポーネントインスタンスが利用不能になると再実行できます。
インスタンスの登録をサポートするために、さらなる構文を定義に追加します。登録インスタンスは、その名前(名前空間で修飾された名前)と提供されたすべてのインターフェースを使用して、自動的にレジストリに登録されます。追加パラメータを指定することが可能で、次の例では、DelayCalculator のプライマリインスタンスとバックアップインスタンスを登録しています。
namespace com.mycompany.datacenter {
registered instance dc1: DelayCalculator {
registration parameters {role = primary}
}
registered instance dc2: DelayCalculator {
registration parameters {role = backup}
}
}
インターフェース、パートII
これまで、インターフェースについてあまり定義しませんでした。メッセージングインフラストラクチャに基づいてシステムを構築したいと分かっていたので、インターフェースをメッセージの集合として定義する必要があることが明らかでした。最初のアイデア(メッセージの集合)を次に示します。ここで、各メッセージは名前と型付きパラメータを持ちます。
interface IInfoScreen {
message expectedAircraftArrivalUpdate(id: ID, time: Time)
message flightCancelled(flightID: ID)
...
}
当然、データ構造を定義できる能力も必要なため、それを次のように追加しました。
typedef long ID
struct Time {
hour: int
min: int
seconds: int
}
ここで、このインターフェースについてしばらく議論した後、単にインターフェースをメッセージのセットとして定義するだけでは十分でないことに気付きました。私たちが実行したい最低限のことは、メッセージの方向、つまり、メッセージがポートの内と外のどちらに送信されるのかを定義できるようにすることです。より一般的に言えば、どの種類のメッセージ対話パターンが存在するかです。私たちは複数特定しました。oneway とrequest-replyの例を次に示します。
interface IAircraftStatus {
oneway message reportPosition(aircraft: ID, pos: Position )
request-reply message reportProblem {
request (aircraft: ID, problem: Problem, comment: String)
reply (repairProcedure: ID)
}
}
それは本当にメッセージか?
私たちは長い時間さまざまなメッセージ対話パターンについて話しました。しばらくして、メッセージの中心的なユースケースの1つが、さまざまな利害関係者にさまざまな資産のステータス更新をプッシュ配信することであると判明しました。たとえば、航空機の技術的問題によってフライトが遅れる場合、この情報はシステムのすべてのInfoScreens にプッシュ配信されなくてはなりません。特定のステータス項目の完全な更新、増分更新、無効化などの「ブロードキャスト通知」に必要ないくつかのメッセージのプロトタイプを作成しました。
そして、ある時点で、間違った抽象化を利用していたということに気付きました! メッセージングがこうしたことに適した転送抽象化である一方で、私たちは複製データ構造について真剣に話し合いをしています。基本的に、複製データ構造すべてに対しても同じように機能します。
- データ構造を定義します(FlightInfoなど)。
- システムは、そのようなデータ構造の集合を追跡します。
- この集合は数個のコンポーネントによって更新され、通常は他の多数のコンポーネントによって読み取られます。
- パブリッシャーから受信者への更新戦略には、集合内の全項目の完全な更新、1つまたは複数の項目の増分更新、無効化などが常に含まれます。
もちろん、メッセージのほかにもシステムにこのさらなる中核の抽象化があることを理解してからは、それをアーキテクチャ言語に追加して、次のようなものを記述できるようになりました。データ構造と複製項目を定義しています。コンポーネントはそれらの複製データ構造を発行または利用することができます。
struct FlightInfo {
from: Airport
to: Airport
scheduled: Time
expected: Time
...
}
replicated singleton flights {
flights: FlightInfo[]
}
component DelayCalculator {
publishes flights
}
component InfoScreen {
consumes flights
}
メッセージに基づく記述と比べて、これは言うまでもなくはるかに簡潔です。システムは、完全な更新、増分更新、無効化などに必要なメッセージの種類を自動的に導出できます。また、この記述は実際のアーキテクチャの意図をはるかに明確に反映します。この記述は、どのようにそれを実行したいか(状態更新メッセージを送信する)の低いレベルの記述と比べて、何を実行したいか(状態を複製する)をより良く表現します。
もちろん、そこで終わりではありません。状態の複製を「第一級市民(first class citizen)」として扱う今、その仕様に詳細な情報を追加することができます。
component DelayCalculator {
publishes flights { publication = onchange }
}
component InfoScreen {
consumes flights { init = all update = every(60) }
}
基本データ構造で何かが変更されるとパブリッシャーが複製データを発行するということを記述しています。しかし、InfoScreen は60秒ごとの更新(および起動時のデータの全ロード)しか必要としていません。その情報に基づいて、参加者のために必要なすべてのメッセージと更新スケジュールを導出することができます。
さらなる補足
続く議論で、私たちはその他いくつかのアーキテクチャの側面を特定し、それらに対する言語抽象化を追加しました。
- バージョニングをサポートするために、あるコンポーネントが既存のコンポーネントの新しいバージョン(代替)となるように指定する方法を追加しました。ツールは「プラグインの互換性」を保証します。
- メッセージの意味とそのシステム状態に対する影響を表現できるようにするために、事前条件と事後条件を導入しました。また、コンポーネントの概念を任意でステートフルなものに拡張しました。
- 最後に、設定パラメータをコンポーネントに追加しました。コンポーネントはパラメータを指定し、コンポーネントのインスタンスはパラメータの値を指定する必要があります。
結論
このアプローチを使用して、システムのアーキテクチャ全体をすばやく把握できるようになりました。また、システムに実行してほしいことと、システムがそれをどのように達成するかを切り離すことができるようになりました。技術の議論はすべて、単にここで示した概念記述の実装詳細にすぎませんでした(もちろん非常に重要な実装詳細ですが)。また、私たちは、異なる用語が意味するものの明確で明白な定義を得ました。コンポーネントの不明瞭な概念は、このシステムにおいては形式的で明確に定義された意味を持ちます。
そして当然、ここで終わりませんでした。次のステップとして、コンポーネントの実装を実際にどのようにコーディングするか、そして、システムのどの部分を自動的に生成できるかに関する議論が必要になりました。これに関する詳細は次のセクションで説明します。
要約および利点
実行内容の要約
このアプローチには、あなたのプロジェクトまたはシステムの概念アーキテクチャのための形式言語の定義が含まれています。アーキテクチャに関する理解が進むと同時に、言語を開発します。したがって、言語は、常にアーキテクチャに関して明確かつ明白な方法で完全に理解することのようなものです。私たちは言語を拡張すると同時に、その言語を使用してアプリケーションアーキテクチャも記述します。
背景: DSL
上記で構築した言語はDSL(domain-specific language; ドメイン固有言語)です。私がどのようにDSLを定義したいのかを次に示します。:
DSLは、特定のドメインにおけるシステム構築時に特定の問題を記述するための、特化した、処理可能な言語です。使用される抽象化と表記法は、その特定の問題を指定する利害関係者に応じます。
DSLは、ソフトウェアシステムのあらゆる側面を指定するために使用できます。DSLを使用してビジネス機能(保険システムの計算規則など)を記述することは大きなハイプに包まれています。これは非常に有意義なDSLの使用ですが、さらに、ソフトウェアアーキテクチャの記述にDSLを使用することも価値があります。それが、私たちがここで実行することです。
上記で構築したアーキテクチャ言語(および私がこの記事でおおむね支持しているアプローチ)は、DSL技術を使用して、アーキテクチャを表現するDSLを定義することです。
利点
関与する誰もが、システムの記述に用いられる概念を明確に理解できるでしょう。アプリケーションを記述するための明確で明白な語彙(ボキャブラリ)があります。作成されたモデルを分析し、コード生成の基礎として使用することができます(下記を参照)。アーキテクチャは実装詳細から解放されます。言い換えれば、概念アーキテクチャと技術決定が切り離されるため、どちらもより簡単に進化させることができます。また、概念アーキテクチャに基づいて明確なプログラミングモデル(上記のアーキテクチャの特徴すべてを使用してコンポーネントをモデリングおよびコーディングする方法)を定義できます。最後になりましたが、アーキテクトは、チームの他のメンバーが実際に使用できる資産を構築(または構築をサポート)することで、プロジェクトに直接貢献することができます。
なぜテキスト記法なのか?
...または、なぜ図式記法を使用しないのでしょうか? テキストのDSLには、いくつかの利点があります。まず、言語も優れたエディタもはるかに簡単に構築できることです。2つ目に、テキストの資産は、何らかのレポジトリに基づくグラフィカルなモデルよりも既存の開発者ツール(CVS/SVN diff/merge)とはるかに良い形で統合します。3つ目に、「本当の開発者は絵を描かない」ため、テキストのDSLはたいてい開発者に気に入られます。
図式記法がアーキテクチャ要素間の関係を確認するのに役立つというシステム側面のために、GraphvizやPrefuseなどのツールを使用できます。モデルには、明確かつ汚されていない形式で関連データが含まれているため、GraphVizやPrefuseなどのツールで読み取り可能な形式でモデルデータを簡単にエクスポートできます。
ツール
上記で紹介したアプローチを実現可能にするには、DSLの効率的な定義をサポートするツールが必要です。私たちはopenArchitectureWareのXtextを使用します。Xtextは、次のことを実行します。
- 文法を指定する方法を提供します。
- この文法から、ツールは実際の構文解析を行うためのantlr文法を生成します。
- また、EMF Ecoreメタモデルを生成します。生成されたパーサーは、言語の文からこのメタモデルをインスタンス化します。その後、すべてのEMFベースのツールを使用してモデルを処理できます。
- 生成されたEcoreモデルに基づいて制約を指定することもできます。制約は、oAWのCheck言語(本質的に、合理化されたOCL)を使用して指定します。
- 最後に、このツールは、コード畳み込み、構文色付け、カスタマイズ可能なコード補完、アウトライン表示、複数ファイルのgo-to-definition およびfind references を提供する、DSL用の強力なエディタも生成します。また、言語制約をリアルタイムで評価してエラーメッセージを出力します。
少しの練習で、Xtextはあなたを自由にし、アーキテクチャの詳細を理解してアーキテクチャの決定を行いながら言語を指定できるようになります。コード補完のカスタマイズには少し時間がかかるかもしれませんが、言語探検段階が停滞状態に達した時にカスタマイズすることができます。
モデルの有効性確認
アーキテクチャを形式的に正しく記述したい場合は、文法で表現できるもの以外にもモデルを制約する有効性確認ルールを導入する必要があります。簡単な例を挙げると、典型的な名前一意性の制約、タイプチェック、非ヌル制約などがあります。そのような(比較的)ローカルな制約を表現することは、単純にOCLまたはOCLライクな言語を使用することです。
しかし、私たちはより複雑でローカル性の低い制約を検証することも望んでいます。たとえば、上記のストーリーの状況では、制約は新しいバージョンのコンポーネントやインターフェースが前バージョンと実際に互換性があることをチェックするため、同じ状況において使用できます。そのようなnon-trivial(重要)な制約を実装できるようにするには、次の2つの前提条件が必要です。
- 制約自体は実際に形式的に記述可能である必要があります。つまり、制約が成立するかどうかを判断する何らかのアルゴリズムがなくてはなりません。アルゴリズムを把握してしまえば、ご使用のツールでサポートされているどんな制約言語でも(ここの例では、OCLライクなXtendまたはJava)制約を実装できます。
- もう1つの前提条件は、上記の制約チェックアルゴリズムの実行に必要なデータが実際にモデルで利用可能であることです。たとえば、特定の配備方式が実現可能であるかどうかを検証したい場合は、利用可能なネットワーク帯域幅と特定のメッセージのタイミングおよびプリミティブデータ型をモデルに導入する必要があるかもしれません6。しかし、それらのデータをキャプチャすることは負担のように思えますが、これは中核的なアーキテクチャ知識であるため、実際は利点といえます。
コードの生成
アーキテクチャDSLを開発する(および使用する)主要な利点は「あいまい性を排除して用語を形式的に定義し、概念を理解すること」であると、この記事から明らかになったはずです。これで、システムについて理解し、不要な技術インターフェースを排除できるようになります。
しかし当然、概念アーキテクチャの形式モデル(言語)および構築しているシステムの形式的記述(言語を使用して定義した文(モデル))がある今、それを使用してさらに効果的なことを行ったほうがよいでしょう。
- 実装をコーディングするAPIを生成します。そのAPIは、さまざまなメッセージングパラダイム、複製状態などを考慮するとnon-trivialである場合があります。生成したAPIにより、開発者はどんな技術決定にも依存しない方法で実装をコーディングできます。生成したAPIはそれらをコンポーネント実装コードから隠します。私たちは、この生成したAPIと、それを使用するイディオムのセットをプログラミングモデルと呼びます。
- 私たちが、何らかのコンポーネントコンテナまたはミドルウェアプラットフォームがコンポーネントを実行することを見込んでいることを覚えておいてください。そのため、選択した実装技術でコンポーネント(技術中立的な実装を含む)を実行するために必要なコードも生成します。私たちは、このコード層を技術マッピングコード(またはグルーコード)と呼びます。通常、関連する各種プラットフォーム用の膨大な数の設定ファイルも含まれます。場合によっては、プラットフォームの設定の詳細を指定する「mix inモデル」の追加が必要になります。副次的効果として、ジェネレータは、あなたが使うことに決めた技術の扱いに関するベストプラクティスをキャプチャします。
いくつものターゲット言語に対するAPIを生成することや(各種言語でのコンポーネント実装をサポートする)、いくつものターゲットプラットフォームに対するグルーコードを生成すること(異なるミドルウェアプラットフォーム上での同じコンポーネントの実行をサポートする)は、もちろん完全に実現可能です。これは、潜在的なマルチプラットフォーム要件を適切にサポートするほか、長期にわたりインフラストラクチャを拡大または進化させる方法を提供します。
注目に値するもう1つのことは、通常はいくつもの段階で生成を行うということです。第1段階では、タイプ定義(コンポーネント、データ構造、インターフェース)を使用してAPIコードを生成し、実装をコーディングできるようにします。第2段階では、グルーコードとシステム設定コードを生成します。結果として、モデルのシステム定義とタイプ定義を切り離すことが多くの場合は賢明なことです。それらは、プロセス全体で異なる時間に使用され、たいていは異なる人物によって作成、修正、処理されます。
要約すると、生成コードは、効率的かつ技術に依存しない実装をサポートし、内在する技術的な複雑さの多くを隠し、開発をより効率的にします。
ADLとUMLをどのように比較するか?
形式言語でアーキテクチャを記述することは、新しいアイデアではありません。さまざまなコミュニティが、アーキテクチャの記述にアーキテクチャ記述言語(ADL)または統一モデリング言語(UML)を使用することを推奨しています。結果としてできたモデルからコードを生成する(しようとする)人もいます。しかし、そうしたアプローチのすべてが、アーキテクチャをドキュメント化するのに既存の汎用言語を使用することを推奨しています(UMLなど一部の言語はカスタマイズできます)。
しかし(上記のストーリーから分かるとおり)、これは完全に的外れです! 私は、アーキテクチャ記述を、事前定義済み/標準化された言語で提供される(一般に非常に制限された)コンストラクトに押し込むことは、あまり有益であるとは考えていません。この記事で説明したアプローチの中心的な活動の1つは、実際に独自の言語を構築してシステムの概念アーキテクチャを取得するプロセスです。アーキテクチャをADLまたはUMLで提供されるわずかな概念に適合させることは、あまり役に立ちません。
UMLおよびプロファイルに関する注意: そうです、上記で説明したアプローチとUMLを使用して、テキスト言語とは対照的なプロファイルを構築できます。私はこれをいくつかのプロジェクトで実行しましたが、結論は、ほとんどの環境で効果がないということです。その理由の一部を次に示します。:
- アーキテクチャ概念について考えずに、UMLを扱うには、意図することを多少なりとも合理的に表現するためにUMLの既存のコンストラクトをどのように使用できるかについてもっとよく考えることが必要です。それは間違った解釈です!
- また、UMLツールは一般的に既存の開発インフラストラクチャ(エディタ、CVS/SVN、diff/merge)とあまり良く統合されません。それは、何らかの分析または設計段階時にUMLを使用する際は大した問題ではありませんが、モデルをソースコードとして使用すると(モデルはシステムのアーキテクチャを明確に反映し、それらのモデルから実際のコードを生成する)、これは大問題になります!
- 最後に、UMLツールは頻繁に重くかつ複雑になり、たいてい「本当」の開発者によって「描画ツール」または「ブロートウェア」として提供されます。優れたテキスト言語を使用することで、受け入れのハードルをはるかに低くすることができます。
なぜ単にプログラミング言語を使用しないのか
メッセージやコンポーネントなどのアーキテクチャの抽象化は、今日の3GLプログラミング言語において第一級市民(first class citizen)ではありません。もちろん、クラスを使用してそれらをすべて表現できます。アノテーション(属性とも呼ばれる)を使用して、メタデータをクラスやその内容の一部(演算、フィールド)に関連付けることができます。したがって、どうにか3GLでかなり多くを表現できます。しかし、このアプローチには次のような問題があります。
- 上記で説明したUMLの場合と同様に、このアプローチは、明確なドメイン固有の概念を事前に構築した抽象化に押し込むことを余儀なくします。いろいろな意味で、アノテーション/属性はUMLステレオタイプおよびタグ付き値と同等であるため、同様の問題に直面すると考えられます。
- モデルの分析可能性は限られています。Java用のSpoonのようなツールはありますが、形式モデルよりも扱いやすく処理しやすいものはありません。
- 最後に、「アーキテクチャで強化されたJavaまたはC#」を使って表現すると、アーキテクチャの問題と実装の問題を結び付けたくなります。これは、明確な区別をあいまいにし、技術に依存しない性質を犠牲にする可能性があります。
コンポーネントに関する私の概念
コンポーネントとは何かに関する(多少なりとも正式な)定義は多数あります。その定義は、ソフトウェアシステムのビルディングブロックから、明示的に定義されたコンテキスト依存性を持つもの、ビジネスロジックに含まれコンテナ内部で実行されるものまでさまざまです。
私の理解は(実質的定義があると言っているわけではありません)、コンポーネントはアーキテクチャの最小のビルディングブロックであるということです。システムのアーキテクチャを定義するとき、コンポーネントの内部は確認しません。コンポーネントは、アーキテクチャ上関連するプロパティをすべて宣言的に指定する必要があります(メタデータ、またはモデルの別名)。 結果として、コンポーネントはツールで分析可能かつ設定可能になります。コンポーネントは通常、データのランタイム関連部分を実行するフレームワークとして機能するコンテナの内部で動作します。コンポーネントの境界は、コンテナがログ記録、モニタリング、またはフェールオーバーなどの技術サービスを提供できるレベルです。
私は、コンポーネントにどのようなメタデータが実際に含まれるか(しがたって、どのプロパティが指定されるか)について、特定の要件を持っていません。コンポーネントの具体的な概念が、それぞれの(システム/プラットフォーム/製品ライン)アーキテクチャに対して定義されなければならないと思います。そして、これはまさに私たちが上記で紹介した言語アプローチを使って実行していることです。
コンポーネント実装
デフォルトでは、コンポーネント実装は手動で発生させます。実装コードは、上記で説明した、生成したAPIコードに対応して記述されます。開発者は手動で記述したコードをコンポーネントスケルトンに追加します。その方法は、生成されたクラスにコードを直接追加するか、または、はるかに良い方法として、継承クラスや部分クラスなどの他の集約(コンポジション)手段を使って追加します。
ただし、コンポーネント実装には他の代替手段もあります。それは、コンポーネント実装に3GLプログラミング言語を使用しないで、指定のビヘイビアの種類に特化した形式を使用します。
- 極めて規則的なビヘイビアは、明確に定義されたパラメータをいくつか設定してモデル内でパラメータ化した後、ジェネレータを使用して実装できます。フィーチャモデルは、実装を生成できるように決定する必要がある可変性を表現することに優れています。
- 状態ベースのビヘイビアに関しては、状態マシンを使用できます。
- ビジネスルールなどに関しては、ルールを直接表現してルールエンジンでそれらを評価するようDSLを定義できます。いくつかのルールエンジンは容易に入手できます。
- ドメイン固有計算(保険ドメインなどで見られるものなど)に関しては、ドメインに必要な数学演算を直接サポートする特定の表記法を提供できます。そのような言語は多くの場合、インタープリタで解釈されます。コンポーネント実装は技術的に、実行すべきプログラムでパラメータ化されたインタープリタで構成されます。
また、Action Semantics Languages(ASL)を使用する代替手段もあります。ASLがドメイン固有の抽象化を提供しないことについて指摘することは重要ですが、たとえばUMLが汎用モデリング言語であるのと同じようにASLは汎用です。しかし、より特定の表記法を使用する場合でも、一般的にビヘイビアの小さな断片を指定することが必要な場合があります。良い例が、状態マシンでの動作です。
ビヘイビアを指定するさまざまな方法とコンポーネントの概念を組み合わせるために、メタレベルでsubtypingを使用して、それぞれビヘイビアを指定するための独自の表記法を持つ、さまざまなコンポーネントを定義することが有益です。以下の図はそのアイデアを示しています。
コンポーネント実装はビヘイビアに関するものであるため、技術的に、コンポーネント内部でカプセル化されたインタープリタを使用することは、しばしば役に立ちます。
最後になりますが、このセクションでの議論は、すべての実装コードに関するものではなく、アプリケーション固有のビヘイビアのみに関するものであることに注意してください。大量の実装コードがアプリケーションの技術インフラ(リモーティング、持続性、ワークフローなど)に関連付けられており、アーキテクチャモデルから得ることができます。
パターンの役割
パターンは、今日のソフトウェア工学の方式における重要な部分です。パターンは、適用性、トレードオフ、結果を含め、繰り返し発生する問題に対する効果的なソリューションを得るための実証された方法です。では、上記で説明したアプローチに、どのようにパターンを組み込むのでしょうか?
- アーキテクチャパターンとパターン言語は、正常に使用されてきたアーキテクチャの青写真を記述します。これらは、システムのアーキテクチャを構築するためのインスピレーションとなります。パターンの使用を決定(および、それを特定のコンテキストに適応)すると、パターンで定義された概念を、DSLの第一級市民にすることができます。言い換えれば、パターンはアーキテクチャに影響を与えるため、DSLの文法となります。
- デザインパターンは、その名が示すように、アーキテクチャパターンよりも具体的で、より実装に特有のものです。デザインパターンがアーキテクチャDSLで主要な概念になるとは考えられません。しかし、モデルからコードを生成する際、コードジェネレータは一般に、多数のパターンのソリューション構造に似たコードを生成します。ただし、ジェネレータはパターンを使用すべきかどうかを決定できないことに注意してください。これが、(ジェネレータ)開発者が手動で行う必要のあるトレードオフです。
DSL、生成、およびパターンについて話すときは、パターンを完全に自動化できるわけではないことについて言及することが重要です! パターンは、単にソリューションのUMLダイヤグラムで構成されるだけではありません! パターンの重要な部分は、パターンを使用した結果だけでなく、パターンを適用できるとき、できないときに、どのフォースがパターンのソリューションに影響するかを説明します。また、パターンは、それぞれ異なる利点および不利点を持つ可能性のある多数のパターンのバリエーションをドキュメント化します。変換のコンテキストで実装されたパターンはこれらの側面を考慮しません。変換の開発者はそうした側面を考慮して、適宜に決定を行う必要があります。
何をドキュメント化する必要があるか?
私は、システムの概念アーキテクチャとアプリケーションアーキテクチャを形式的に記述する方法として、上記のアプローチを宣伝します。つまり、それがある種のドキュメンテーションとしての役割を果たすということを意味しているのでしょうか?
それもそうなのですが、あなたが他に何もドキュメント化する必要がないということを意味しているわけではありません。以下に、ドキュメント化する必要があるものを示します。:
- 理論的根拠/アーキテクチャ上の決定: DSLはアーキテクチャがどのようなものかを記述しますが、理由を説明しません。アーキテクチャ上および技術上の決定に対する理論的根拠をドキュメント化する必要があります。通常は、ここで(非機能的)要件に戻って参考にするべきです。文法は非常に良いベースラインであることに注目してください。アーキテクチャDSL文法の各コンストラクトは、多数のアーキテクチャ上の決定によってもたらされます。そのため、文法の各要素が存在している理由(そして、その他の選択肢が選ばれなかった理由)について説明する場合、重要なアーキテクチャ上の決定をドキュメント化することについては、もう手の届くところまで近づいています。同様のアプローチを、アプリケーションアーキテクチャ、すなわちDSLのインスタンスに利用できます。
- ユーザーガイド: 言語文法は、アーキテクチャを得るための明確に定義された形式的な方法としての役割を果たしますが、優れた教材ではありません。そのため、アーキテクチャの使用方法について、ユーザーのためにチュートリアルを作成する必要があります。これには、何をどのように(DSLを使用して)モデリングするのか、そして、コードの生成方法やプログラミングモデルの使用方法(生成されたスケルトンに実装コードを追加する方法)などが含まれます。
ドキュメント化する価値があると思われるアーキテクチャの側面は他にもありますが、上記の2つが最も重要です。
参考資料
この記事で説明したアプローチに興味を持った方は、私のアーキテクチャパターンのコレクションをお読みください。それらは、パターンの観点から、本質的に同じ話題を考察しており、ここで説明したことの理論的根拠を提供します。少し古い文書ですが、本質的には同じ話題を考察しています。http://www.voelter.de/data/pub/ArchitecturePatterns.pdfでご覧になれます。
もう1つの考察事項は、ドメイン固有言語とモデル駆動のソフトウェア開発の全領域です。私は、この話題に関して多くのことを書いており、第1に『 Model-Driven Software Development - Technology, Engineering, Management 』という本を共同執筆しました。この本を考察することをおすすめします。詳細については、http://www.voelter.de/publications/books-mdsd-en.htmlをご覧ください。
もちろん、一般のEclipse Modeling、openArchitectureWare、Xtextに関する詳細を考察することもよいでしょう。公式のoAW docsや数多くの入門ビデオなど、eclipse.org/gmt/oaw から多数の情報を入手できます。
謝辞
この記事の以前のバージョンで非常に役立つ意見をくださった、Iris Groher氏、Alex Chatziparaskewas氏、Axel Uhl氏、Michael Kircher氏、Tom Quas氏、Stefan Tilkov氏(順不同)に感謝申し上げます。
著者について
Markus Völter はソフトウェアの技術と工学を専門とする独立コンサルタントおよびコーチとして働いています。彼は、ソフトウェアアーキテクチャ、モデル駆動のソフトウェア開発、およびドメイン固有言語、ならびにプロダクトライン工学に重点的に取り組んでいます。Markusは、ミドルウェアやモデル駆動のソフトウェア開発に関する多くの雑誌記事、パターン、本の(共同)著者であり、世界中の会議で講義をしています。Markusの連絡先は、voelter at acm dot orgまたはwww.voelter.de です。
2いいえ、私はADLやUMLについて話していません。お読みください。
3言語で文を書くための何らかのツールも必要です。その詳細はまた後ほど。
4これは、このアプローチが大規模システム、製品ライン、およびプラットフォームに特に適しているという事実もほのめかしています。
5具体的なコンポーネントのセット、その責務、および結果を打ち出しても、それらのインターフェースは必ずしもtrivial(些細)なわけではありません。CRCカードのような手法がここでは役立ちます。
6 実際にそれらを異なるファイルに入れたほうがよい場合もあるため、この側面はコアモデルを汚しません。しかしこれはツールの問題です。
(このArticleは2008年2月27日に原文が掲載されました)