メインコンテンツまでスキップ

ソフトウェアアーキテクチャの基礎

1. イントロダクション

ソフトウェアアーキテクチャとは

  • 明確な定義はないが「重要なもの」
  • 非常に広範囲である
  • 変化と拡大を続けている
  • 文脈なしに成り立つ銀のアーキテクチャは存在しない

ソフトウェアアーキテクチャを構成する 4 要素

  • 構造
    • 単にスタイルのこと(マイクロサービス/レイヤード...)
    • これだけではアーキテクチャ全体は定まらない
  • アーキテクチャ特性
    • システムに直接的には関わらないものの、成功のために必要な基準のこと
    • ...ility (可用性、信頼性、スケーラビリティ、セキュリティ...)
  • アーキテクチャ決定
    • システムをどう構築するかのルール
    • 許されることと許されないことを決めたもの
  • 設計指針
    • ルールではなく、こうしたほうがいいよ的なガイドライン

アーキテクトがやるべきこと

  • アーキテクチャ決定を下したり、設計指針を定義したりする
  • アーキテクチャを継続的に分析・改善する
  • 最新のトレンドを把握する
  • 決定の遵守を徹底する
  • 広く浅く多様なものに触れ、経験し、知る
  • 事業ドメインの知識を学ぶ
  • 対人スキルを身につける
  • 政治を理解し、舵取りをする

プロセスとエンジニアリングプラクティスを分けて考えろ、後者に注目せよ

  • プロセス
    • チームの作り方、ミーティングの進め方、ワークフローの整理方法などを決めるもの
    • e.g. スクラム、ウォーターフォール...
    • 未知の未知には対処しやすいのはイテレーティブなプロセス
  • エンジニアリングプラクティス
    • 再現性のある、プロセスに依存しない、技術的な方法論
    • e.g. 継続的インテグレーション、テスト駆動開発、自動化...

開発から運用までトータルで見た方が良い結果を生むよね、というのが DevOps という考え方。

  • アーキテクチャの法則
    • すべてはトレードオフである
    • How よりも Why を重視せよ

2. アーキテクチャ思考

アーキテクチャ思考とは、アーキテクトの目線で物事を見ること。

アーキテクチャはアーキテクトが作り、設計は開発者が作るもの。 ただし、その流れは一方向ではなく、双方向に影響し合うものでなければならない。

アーキテクトは、技術の深さよりも幅が大事。 これを認識しておかないと、全ての技術を習得しようとしてボロボロになったり、古く陳腐化した専門性で戦ったりしてしまう。

アーキテクチャとは Google で見つけられないものである。 つまり、"It depends"であり、すべてはトレードオフなのだ。 プラス面とマイナス面を洗い出し、どれを大事なのか考えよう。

ビジネスドライバーの求める要件を、アーキテクチャ特性(ility)に変換せよ。 このためには事業ドメインの知識と、ステークホルダーとの協力的な関係が必要。

アーキテクチャのスキルだけではなく、コーディングスキルを維持することも大事。うまくやるには以下がおすすめ。

  • ボトルネックにならない部分の開発をする。例えば、負債解消、バグ修正、小さな機能追加など。
  • PoC を頻繁に行う。この時、なるべくプロダクションレベルのコードにすること。ひどいコードが再利用されることを防ぐためと、自身の鍛錬のため。

3. モジュール性

定義

モジュールとは、関連するコードを論理的にグループ化したもの。モジュール性とは、グループ化がどれだけうまく行われているかを示す指標。

ほとんどの言語にはモジュールを定義するための仕組み、例えばクラスや関数や名前空間などが備わっている。開発者はそれらを活用しながらモジュール性を実現していく。

ソフトウェアは常にエントロピー増大の危機にさらされており、良い状態を保つには常にエネルギーを費やし続けて秩序(≒ モジュール性)を維持する必要がある。

モジュール性の話はアーキテクチャ特性には一般的には含まれず暗黙的ではあるものの、重要な要素である。

モジュール性の指標とその計測手法

凝集度 / Cohesion

モジュール内の要素がどれだけ密接に関連しているかを示す指標で、高いほど良い。凝集度が低いということは、別のモジュールと結合しないかぎり有益な結果がえられないということだから。

LCOM という、フィールドを介して結合されていないメソッド群の数により、クラスの凝集度の低さを計測する方法がある。

結合度

モジュール間の依存関係の強さを表す指標。

求心性結合(Afferent Coupling)は外部からの接続数であり、これが多いということは、他の複数のモジュールからよく利用されている状態を指す。システムにとって重要なサービスを提供している可能性がある。

遠心性結合(Efferent Coupling)は外部に向いた接続数であり、これが多いということは、他の複数のモジュールを利用している状態を指す。他のモジュールに多く依存しているため、変更の影響を受けやすい可能性がある。

不安定度

求心性・遠心性結合のバランスを示す指標。両方の結合に占める遠心の割合が高いほど、不安定度が高いといえる。

抽象度

モジュール内における、抽象(抽象クラスやインターフェース等)と実装の比率のこと。抽象度が高いということは、モジュールが抽象的で、実装の詳細が隠蔽されているということ。

main メソッドしかない巨大な関数なら抽象度は 0 で、逆にインターフェースだけのクラスなら抽象度は 1 になる。

主系列からの距離

主系列からの距離は、ソフトウェアのモジュール性において、抽象度と不安定度という 2 つの指標のバランスを評価するための概念である。主系列からの距離が遠いということは、以下の 2 つの可能性を示唆する。

  • 無駄ゾーン: モジュールの利用者が少ないにもかかわらず、抽象化が進んだ状態。全部ベタで書けばすむんじゃないの?的な。
  • 苦痛ゾーン: モジュールの利用者が多いにもかかわらず、抽象化がされていない状態。変更と保守を容易にするために抽象化しようぜ!的な。

コナーセンス / Connascence

コナーセント = 接続

コナーセンスは、ソフトウェアの変更容易性を測るための概念である。2 つのコンポーネント間にコナーセンスがある場合、一方を変更すると他方にも変更が必要になる。コナーセンスの種類とその解説は以下の通り。1-5 は静的なコナーセンス、6-9 は動的なコナーセンスである。1 に近いほど、より良く、より弱く、より変更容易性が高い。

  1. 名前のコナーセンス (Connascence of Name - CoN)

    • 変数名、関数名、メソッド名、クラス名、モジュール名等、名前による参照のこと
    • 例: あるメソッド名が変更されると、そのメソッドを呼び出している全ての箇所も修正が必要になる
  2. 型のコナーセンス (Connascence of Type - CoT)

    • 型による参照のこと
    • 例: ある型が変更されると、その型を使っている全ての箇所も修正が必要になる
  3. 意味のコナーセンス (Connascence of Meaning - CoM)

    • あるコンポーネントが、他のコンポーネントの動作や意味に依存している状態です。Magic Number など。
    • 例: 真を1と仮定して書かれているコード群は、その変数の意味が変更されると全て修正が必要になる。なお、Matci Number 等の代わりに名前付き定数を使うことで「名前のコナーセンス」に昇格できる。
  4. 位置のコナーセンス (Connascence of Position - CoP)

    • あるコンポーネントが、他のコンポーネントの物理的な位置に依存している状態です。
    • 例: 引数の順序
  5. アルゴリズムのコナーセンス (Connascence of Algorithm - CoA)

    • 複数のコンポーネントが、同じアルゴリズムに依存している状態です。
    • 例: ある暗号化アルゴリズムを使用しているコードは、そのアルゴリズムが変更されると修正が必要になります。
  6. 実行順序のコナーセンス (Connascence of Execution - CoE)

    • あるコンポーネントの実行が、他のコンポーネントの実行順序に依存している状態です。
    • 例: ある関数が他の関数の後に実行されることを前提としているコードは、実行順序が変更されると修正が必要になります。
  7. タイミングのコナーセンス (Connascence of Timing - CoT)

    • あるコンポーネントの実行が、他のコンポーネントの実行タイミングに依存している状態です。
    • 例: 複数スレッド間で同時に実行されると競合が発生するため、同期処理が必要になるケースなど
  8. 値のコナーセンス (Connascence of Value - CoV)

    • 複数のコンポーネントが、同じ値に依存している状態です。
    • 例: 分散トランザクションのように、全ての値を同時に更新するか、全く更新しないかのどちらかでなければならない場合など
  9. アイデンティティのコナーセンス (Connascence of Identity - CoI)

    • 複数のコンポーネントが、同じオブジェクトのインスタンスに依存している状態です。
    • 例: あるシングルトンオブジェクトにアクセスするコードは、そのシングルトンオブジェクトが変更されると修正が必要になります

コナーセンスのベストプラクティス

  • 強いコナーセンスは弱いコナーセンスに置き換える
  • コード間の距離が遠くなるにつれ、弱いコナーセンスを使用する
  • システムを分割して全体的なコナーセンスを最小に抑える

4. アーキテクチャ特性

アーキテクチャ特性(architectural characteristics)とは、ドメイン領域とは直接関連しないがソフトウェアが満たすべき考慮事項のこと。 非機能要件(non-functional requirement)や品質特性(quality characteristics)という呼び名を改善したもの。 すべての特性を満たそうとするのではなく、設計の構造に変更を与えうるものや、成功のために不可欠または重要であるものだけを選択することが大事。

特性の定義は曖昧だったり、重複していたり、文脈によって異なる意味を持ったりする。 また、特性は個々のソフトウェア固有の要因を元に、独自に発明すべきものである。 とはいえ、ISO/IEC 25010 という ISO が定義している特性のリストが一つの参考になりそう。

https://iso25000.com/index.php/en/iso-25000-standards/iso-25010

以下の 9 つのカテゴリと、配下にサブカテゴリがある。

  • Performance efficiency
    • 時間、リソース、容量の効率
  • Compatibility
    • 共存可能性や相互運用性
  • Interaction Capability
    • ユーザビリティのこと。ユーザーが効果的・効率的・満足に利用できるか。
    • Appropriateness recognizability / Learnability / Operability / Inclusivity ...
  • Reliability
    • 指定した条件と期間においてシステムが正常に動作し続けるか
    • Faultlessness / Availability / Fault tolerance / Recoverability
  • Security
    • 意図的な悪意ある行動に対して、システムが保護された状態にあるか
    • Confidentiality / Integrity / Non-repudiation / Accountability / Authenticity / Resistance
  • Maintainability
    • 変更や修正が容易か
    • Modularity / Reusability / Analyzability / Modifiability / Testability
  • Flexibility
    • 多様な目的に応じて柔軟に対応できるか
    • Adaptability / Scalability / Installability / Replaceability
  • Safety
    • 非意図的あるいは非人為的な行為に起因して、人の生命、健康、財産、または環境が危険にさらされることがないか
    • Operational constraint / Risk identification / Fail safe / Hazard warning / Safe integration
  • Functional Suitability
    • ユーザーのニーズを満たしているか
    • 著者はこれは特性には含まれないと主張している

サポートするアーキテクチャ特性を増やせば増やすだけ複雑さは増し、うまくいかなくなる。 最初からすべての特性を満たす「最善のアーキテクチャ」を狙ってはいけない。 「少なくとも最悪ではないアーキテクチャ」からスタートし、必要に応じてイテレーティブに改善していけ。

5. アーキテクチャ特性を明らかにする

特性を明らかにする方法は以下の 3 つ。

ドメインの関心ごとからアーキテクチャ特性を導出する

ドメイン領域で大事にされていることを ility に変換する。ステークホルダーと議論して決めていく。

  • 時間と予算 -> シンプルさ
  • ユーザー満足度 -> パフォーマンス、可用性、耐障害性、テスト容易性、デプロイ用意性、アジリティ、セキュリティ
  • 合併・買収 -> 相互運用性、スケーラビリティ、拡張性

要件からアーキテクチャ特性を導出する (明示的な特性)

要件に書いてある条件をアーキテクチャ特性に変換する。 例えば、ユーザー数やユースケースから必要なパフォーマンスやスケーラビリティを導出するなど。

要件に書いていないアーキテクチャ特性を導出する (暗黙的な特性)

可用性、信頼性、セキュリティなどはわざわざ要件に書いていないことが多い。 必要であればこれらも選択肢に加える。

6. アーキテクチャ特性の計測と統制

アーキテクチャ特性は定義が様々かつ複合的であるため、議論が難しい。 これらを解決するには、客観的定義、つまり計測が有用である。

  • 運用面の計測
    • Performance を測るために First Contentful Paint を使うなど
  • 構造面の計測
    • Maintenability を測るために循環的複雑度を使うなど
  • プロセス面の計測
    • Agility を測るためにテストカバレッジやデプロイ失敗率を使うなど

アーキテクチャ特性を守ることを開発者に徹底してもらう、つまり統制も大事。 例えばモジュール性の統制を取るには、以下のような事柄について CI などでチェックできる仕組みを作るとよい。

  • 循環依存
  • 主系列からの距離
  • コード上のふさわしくない場所での DB アクセス

7. アーキテクチャ特性のスコープ

アーキテクチャ量子とは、アーキテクチャ特性を考える時に一度に見渡すべき最小の範囲のこと。 システム全体とは限らず、より狭い範囲になることもある。まあモノリスなら必然的にシステム全体になるけどな。 以下の 3 つの勘案して決定する。

まずは、機能的凝集性を持つこと。目的が単一なら求められる特性も限定しやすいから。DDD の境界づけられたコンテキストと合わせるのが一般的である。

つぎに、スコープ内の要素同士は同期的なコナーセンスを持つこと。コナーセンスを持つもの同士は、片方を変更したらもう片方も変更しないといけないので、同じアーキテクチャ量子に含まれなければならない。ただし、動的かつ非同期なコナーセンスを持つもの同士は、相手を待つ必要がなく独立しているため、同じアーキテクチャ量子に含まれる必要はない。

最後に、単独でデプロイが可能であること。例えば API サーバーと DB は同じ量子に含める必要がある。

9. アーキテクチャスタイル

アーキテクチャパターンともいう。

基礎的なパターン

例えば「レイヤー」の概念は古くから存在する。

巨大な泥団子 / Big Ball of Mud はアーキテクチャ構造が存在しないクソコードのこと。残念ながら現実世界で非常によく見られる。

1 層アーキテクチャ(ユニタリーアーキテクチャ)は、ただ 1 つのコンピューターとその上で動くソフトウェアのこと。コンピューター黎明期のメインフレームなどが該当する。現代にはあまり存在しない。

2 層アーキテクチャ(クライアント・サーバー)は、Access のようなデスクトップアプリ + DB サーバーの構成や、現在の Web アプリのようなブラウザ + Web サーバーの構成を指す。

3 層アーキテクチャもある。JS が動くフロントエンド + Java などのアプリケーションサーバー + 強力な商用 DB サーバーなどの構成からなる。90 年代後半に流行った。

モノリシック vs 分散

  • モノリシック
    • レイヤードアーキテクチャ
    • パイプラインアーキテクチャ
    • マイクロカーネルアーキテクチャ
  • 分散
    • サービスベースアーキテクチャ
    • イベント駆動アーキテクチャ
    • スペースベースアーキテクチャ
    • サービス指向アーキテクチャ
    • マイクロサービスアーキテクチャ

分散アーキテクチャは強力だが、以下のような問題に対処する必要があり、大きなトレードオフが発生する。


ネットワークは信頼できないので、タイムアウトやサーキットブレーカーのような機構が必要になる。

関数呼び出しのレイテンシーが大きい。ローカル呼び出しならナノ秒で住むが、RPC などはミリ秒単位になる。ロングテールレイテンシー(95-99%タイルのレイテンシー)を把握して必要な対処をすることが大事。

帯域幅は有限であるため、例えば GraphQL のような必要最低限のデータを取得する仕組みがないと、あっという間に食いつぶす。

ネットワークは安全ではない。分散システムは攻撃を受ける表面積が飛躍的に増えるので、高度なセキュリティ対策が必要になる。

常に変化するネットワークトポロジー(接続形態)への対応が必要。些細な機器の更新がレイテンシーに影響を与えた結果、タイムアウトを引き起こしてシステムダウンするようなこともある。

ネットワーク管理者はたくさんいる。コミュニケーションを取るべき管理者は一人では済まず、コストがかかる。

転送コストがかかる。これはレイテンシーの話ではなく RESTful 呼び出しにかかる費用の話で、モノリスと比べるとかなり高い。

ネットワークにはムラがある。ネットワーク内には違なるメーカーの様々な機器が存在するし、それらが協調して完全にうまく動く保証はない。

ロギングが難しい。分散ロギングしないと問題の追跡すらできない。

トランザクションが難しい。分散トランザクションにより結果整合性を担保するのが関の山。ACID トランザクションのように即時の一貫性は保証できない。