ソフトウェアアーキテクチャには、不都合な真実があります。多くのプロジェクトにおいて、アーキテクチャは主に人の頭の中に存在しているということです。古びた Wiki に図が残っていたり、新人向けドキュメントに数段落だけ記載されていたり、3年前のスプリント計画時のホワイトボード写真が残っていたりするかもしれません。しかし、生きた状態で検証可能な、機械可読なアーキテクチャ記述が存在するケースは稀です。
この「意図されたアーキテクチャ」と「実際の実装」のギャップは、単なるドキュメントの問題ではなく、品質の問題です。幸いなことに、現在のプロジェクト状況に応じて、この課題に対処するための現実的な方法はいくつか存在します。本記事では、その中でも特に開発者にとって扱いやすい「Architecture as Code」に焦点を当てます。
アーキテクチャが非公式なドキュメントとしてしか存在しない場合、コードが実際にルールに従っているかを自動的に検証する方法はありません。開発者はそれぞれ合理的なローカル判断を行いますが、その積み重ねによって、意図された構造は徐々に崩れていきます。新しいメンバーは設計を誤解し、本来存在してはいけない依存関係が入り込みます。時間が経つにつれてギャップは広がり、多くの場合、重大な問題になるまで気付かれません。
Murphy、Notkin、Sullivan によって1995年の論文で提案されたリフレクション解析(reflexion analysis)は、この問題に正面から取り組む手法です1)。考え方はシンプルです。まずアーキテクチャモデルを定義し、ソースコードのエンティティをアーキテクチャコンポーネントにマッピングし、その2つのグラフをアルゴリズム的に比較します。これにより、「一致(期待通り存在する依存関係)」、「逸脱(想定外の依存関係)」、「欠如(期待される依存関係が存在しない)」を検出できます。これにより、チームはアーキテクチャ適合性を継続的に監査するための、具体的かつ再現可能な方法を得られます2)。
これまでの課題は常に「どう実現するか」でした。特に、アーキテクチャモデルをどのように記述し、コードとコンポーネントのマッピングをどのように定義するかが問題でした。
Architecture as Code と Architecture Recovery は、特定のシナリオに限定されるものではありません。実際には、初めてアーキテクチャを文書化する場合でも、既存のアーキテクチャを強制適用する場合でも利用できます。本質的な違いは「作業方法」にあります。
Architecture as Code はプログラム的なアプローチです。ルールを書き、実行し、問題を修正し、再度実行します。コード構造とアーキテクチャの関係が規則的かつ一貫しているほど、自動化を進めやすくなります。
一方、Architecture Recovery はインタラクティブかつ手動中心のアプローチです。コードとアーキテクチャが大きく乖離している場合には、これはむしろ利点になります。構造を素早く試行錯誤しながら把握し、ルールとして固定化する前に全体像を理解できるからです。
この2つのアプローチは組み合わせることも可能です。整理されたコードベースの一部は Architecture as Code で明確に定義しつつ、混沌としたレガシーサブシステムはまずインタラクティブにマッピングする、といった使い方ができます。そして、Architecture Recovery を通じてアーキテクチャが安定してきた段階で、それらの手動マッピングを Architecture as Code のルールとして定義し直すことができます。これにより、モデルの再現性が高まり、将来的な保守も容易になります。
既にアーキテクチャを持っているチームにとって、次の課題は「どのように記述するか」です。リフレクション解析をサポートする多くのツールは、主に2つのアプローチを採用してきました。
Murphy らによる元々の手法では、正規表現を使って実装成果物をアーキテクチャコンポーネントにマッピングしていました。小規模システムでは十分な表現力を持ちますが、複雑さが増すにつれて、正規表現はすぐに扱いづらくなります。
現在の多くのツールはグラフィカルインターフェースを採用しています。コンポーネント同士をドラッグしてマッピングを定義したり、ボックスを並べ替えて階層構造を表現したりします。小規模から中規模システムでは有効ですが、大規模になると以下のような限界が顕在化します。
Axivion が高度なアーキテクチャ検証機能で採用している Architecture as Code は、異なるアプローチを取ります。グラフィカルツールや独立した DSL(ドメイン固有言語)を使うのではなく、アーキテクチャとマッピングを Python コードとして記述します。具体的には、Python に組み込まれた内部 DSL として実現されています。
アーキテクチャ自体は、提供される Python クラスとメソッドを用いて宣言的に定義します。
ARCH = Architecture(
"My Architecture",
Component("App"),
Component("EngineCtrl"),
Component("Sensor"),
ComponentWithInterface("HW")
)
app = ARCH.App
enginectrl = ARCH.EngineCtrl
app.depends_on(enginectrl)
app.depends_on(sensor)
ソースコードからアーキテクチャコンポーネントへのマッピングも、同様に明快に記述できます。
# コンポーネント内のすべてを private とする...
MAPPING.add_mapping("src/hw", hw, is_private=True)
# ...ただし公開ヘッダを除く
MAPPING.add_mapping("src/hw/hw_public.h", hw, is_private=False)
マッピングルールは「最後に一致したものを優先する(last-match-wins)」方式を採用しています。これにより、一般的なルールを定義しつつ、例外を後から簡単に追加できます。このパターンは、名前空間の可視性やビルドシステムのフィルタに慣れた開発者には自然に理解できるでしょう。
この Python ベース DSL 用ライブラリは Axivion に含まれており、必要に応じて利用できます。
このアプローチの価値は、単に構文が洗練されているだけではありません。日々の開発において具体的なメリットをもたらします。
既存ツールチェーンと統合できる
アーキテクチャ記述は単なる Python コードなので、IDE、デバッガ、リンタ、フォーマッタなどをそのまま利用できます。編集用に新しいツールを学ぶ必要はなく、内容を理解するための専用ビューアも不要です。
バージョン管理との親和性が高い
アーキテクチャ記述は、対象コードと同じ Git リポジトリ内で管理できます。diff は読みやすく、アーキテクチャ変更のコードレビューも通常のコードレビューと同じ方法で実施できます。履歴追跡も容易です。
拡張性が高い
チームは DSL コンポーネントを継承して、ドメイン固有の概念をモデル化できます。たとえば、コンポーネントの公開インターフェースと内部実装を分離する ComponentWithInterface クラスは、この拡張性の自然さを示しています。任意の Python ライブラリを利用でき、任意の Python ロジックでモデルを駆動できます。
アーキテクチャは「記述」だけでなく「生成」もできる
記述は実行可能な Python コードであるため、既存の依存関係グラフを走査して、初期アーキテクチャモデルを自動生成することも可能です。つまり、完全な白紙状態から始める必要はなく、自動生成されたスケルトンをベースに手動で洗練していくことができます。これは、Architecture Recovery の結果を Architecture as Code に移行する際の有効な橋渡しにもなります。
違反は実際に対処可能な形で提示される
リフレクション解析を実行した後、結果はチームにとって使いやすい任意の形式で出力できます。たとえば、コンソール上の違反一覧や、CI パイプライン上のレポートなどです。アーキテクチャ適合性は「期待」ではなく、「ゲート」として機能するようになります。
最適なアプローチは、現在の状況によって異なります。
どの場合でも、最終的な目標は同じです。コードに対して継続的に検証可能な、生きたアーキテクチャモデルを構築することです。
Axivion の Architecture as Code は、現在 C、C++、C#、CUDA C++、Rust をサポートしています。その他の言語についても、Language Server Protocol(LSP)経由で依存関係グラフを生成できるため、幅広い言語に適用可能です。
もしチームが既に CI 上で静的解析を実施しているのであれば、アーキテクチャ適合性チェックを追加することは自然な拡張と言えます。アーキテクチャの現状が不明な場合は、まず Architecture Recovery によって検証に値するベースラインを確立するのが最も早い方法であることが多いでしょう。その後、継続的な検証を実現するために、Axivion アーキテクチャ検証を利用できます。
いずれにしても、アーキテクチャを「見えない前提」から、「可視化され、測定可能な成果物」へと変えることは、大きな意味を持つ変化です。そして、それはコードベースが変化するたびに価値を発揮します。
1) Gail C. Murphy, David Notkin, and Kevin Sullivan. Software reflexion models: Bridging the gap between source and high-level models. In ACM SIGSOFT Symposium on the Foundations of Software Engineering, pages 18–28, 1995.
2) See R. Koschke and D. Simon, "Hierarchical reflexion models," In: Proceedings of the 10th Working Conference on Reverse Engineering, 2003. pp. 36-45, doi: 10.1109/WCRE.2003.1287235.