はじめに
「開発者として優秀な人がアーキテクトになったとき、何が変わるのか?」
キャリアを積んでいく中で、設計レビューやアーキテクチャの議論に関わる場面が増えてきた、という方は多いのではないでしょうか。自分もそうでした。しかし、いざ「アーキテクチャ的な視点で考えてほしい」と言われたとき、正直なところ何を見ればいいのかよくわかりませんでした。
開発者とアーキテクトでは、実は「見ているもの」が根本的に違います。
この記事では、この視点の転換を具体的に解説します。
TL;DR
- 開発者は「技術的な深さ」を追求するが、アーキテクトは「技術的な幅」をより重視する
- アーキテクチャの意思決定に正解はなく、全てはトレードオフである
- モジュール性はシステムの構造的な健全性を評価するための分析レンズ
- 凝集度は「モジュール内の要素のまとまり」、結合度は「モジュール間の依存の強さ」を測る
- コナーセンスは結合の種類を精密に記述するための語彙である
- コナーセンスの活用では、弱い結合への変換とモジュール間距離に応じた結合強度の制御がポイントになる
- アーキテクトもコーディングに関わり続けることが重要であり、ボトルネックにならない形での関わり方が求められる
開発者とアーキテクトでは「見ているもの」が違う
「チームで一番技術力がある人がアーキテクトになる」――そう思われがちですが、実はアーキテクトに求められるのは、特定技術の深い知識だけではありません。技術選択全体を俯瞰する力です。
このセクションでは、開発者とアーキテクトの視点がどう違うのか、そしてその違いを具体的にどう身につけるかを見ていきます。
アーキテクチャとデザインの境界はスペクトラムである
まず「アーキテクチャ」と「デザイン」の違いについて整理しておきます。
家を建てるとき、「何階建てにするか」「屋根の形はどうするか」「部屋はいくつか」といった判断は家の構造を決めるもの、つまりアーキテクチャです。
一方、「床はフローリングにするかカーペットにするか」「壁の色は何色か」といった判断は家の内装を決めるもの、つまりデザインです。
ソフトウェアでも同様です。マイクロサービスを採用するかモノリスにするかはシステムの構造を決める判断(アーキテクチャ)であり、UIの見た目やレイアウトはデザインの領域です。
ただし、実際の判断の多くは「アーキテクチャかデザインか」の二択には収まりません。たとえば「サービスをさらに小さく分割するか」「UIフレームワークを何にするか」といった判断は、その中間のどこかに位置します。
アーキテクチャ寄りかデザイン寄りかを判断するための基準として、3つの観点があります。
アーキテクチャ寄り デザイン寄り
◄━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━►
判断基準:
1. 戦略的か、戦術的か
長期的 ◄──────────────────────────► 短期的
2. 変更にどれだけの労力がかかるか
大きい ◄──────────────────────────► 小さい
3. トレードオフの重大さはどの程度か
重大 ◄──────────────────────────► 軽微
たとえば、モノリスからマイクロサービスへの移行は、多くの人を巻き込み、長期間にわたる戦略的な判断であり、労力も大きく、トレードオフも重大です。これは明らかにアーキテクチャ寄りの判断です。一方、画面のフィールドの並びを変えるだけであれば、短期間で済み、労力もトレードオフも小さいので、デザイン寄りと言えます。
あるソフトウェアアーキテクトは、こう述べています。
アーキテクチャとは「変更が困難なもの」である
変更が困難であるほど、その判断はアーキテクチャ寄りということです。
知識ピラミッド ― 技術的な深さと幅
開発者とアーキテクトの違いを最も端的に表しているのが、技術知識の捉え方です。技術知識は3層のピラミッドで整理できます。
┌──────────┐
│ 知っている │ ← 日々使っている技術の専門知識
│ │ (例: Java を熟知している)
├──────────┤
│ 知らないと │ ← 聞いたことはあるが使えない技術
│ 知っている │ (例: Clojure の名前は知っているが書けない)
├──────────┤
│ 知らない │
│ ことすら │ ← 存在自体を知らない技術
│ 知らない │ (解決策として最適かもしれないが、
│ │ その存在を知らない)
└──────────┘
↑ 上ほど小さく、下ほど大きい
「知っていること」
ピラミッドの頂上にあたる、日々使っている技術です。Java プログラマなら Java の深い知識がここに入ります。ただし、ここは最も小さい領域です。誰でも全ての技術の専門家になることはできないからです。
「知らないと知っていること」
ピラミッドの中間層です。聞いたことはあるけど実際に使った経験がない技術がここに入ります。たとえば「Clojure は Lisp ベースの言語」だと知っていても、実際にコードは書けないという状態です。この層は頂上よりもずっと大きく、人は専門性を持てる数よりも多くの技術に「聞いたことがある」レベルで触れることができます。
「知らないことすら知らないこと」
ピラミッドの底辺であり、最も巨大な層です。ある問題に対して最適な解決策になりうる技術やツールが世の中にはたくさん存在しますが、そもそもその存在を知らなければ候補にすら上がりません。
開発者のキャリアでは、ピラミッドの頂上を伸ばすこと、つまり 技術的な深さ(Technical Depth) を磨くことが価値を生みます。特定の言語やフレームワークに精通することで、複雑な問題を効率的に解決できるようになるからです。
しかし、アーキテクトにとっては技術的な幅(Technical Breadth)、つまりピラミッドの中間層をどれだけ底辺に向かって広げられるかが重要になります。なぜなら、アーキテクトの価値は「ある問題に対して5つの解決策を知っていること」にあるからです。1つの技術に深い専門性を持つよりも、5つの選択肢を知っていて状況に応じて比較検討できるほうが、アーキテクトとしては有用です。
アーキテクトへの転身で陥りやすい2つの罠
開発者として深さを磨いてきた人がアーキテクトに転身するとき、2つの罠に陥りやすいと言われています。
- 全方位の専門家を目指して疲弊する: あらゆる分野で専門性を維持しようとして、どれも中途半端になってしまうパターンです。アーキテクトへの転換は、深さの一部を意識的に手放し、その時間を幅の拡大に充てることを意味します。
- 古い知識を最新だと思い込む(Frozen Caveman Antipattern): かつての専門領域の知識が古くなっているにもかかわらず、まだ通用すると思い込むパターンです。特に、開発者時代に会社を支えた技術にこだわり続けるベテランに多く見られます。
技術的な幅を広げる具体的な方法
「幅を広げろ」と言われても、具体的に何をすればいいのかわからない――そう感じた方のために、実践的なテクニックを2つ紹介します。
20分ルール
1日20分、新しい技術トピックの学習に充てるという習慣です。「たった20分?」と思うかもしれませんが、毎日続けることで着実に「知らないことすら知らないこと」を「知らないと知っていること」に引き上げていけます。
ポイントは、朝一番、メールをチェックする前に行うことです。昼休みや仕事終わりにやろうとすると、仕事や予定に押されて結局やらないことが多いものです。朝、頭がフレッシュなうちに取り組むのが最も効果的とのことです。
情報源としては以下が紹介されています。
パーソナルレーダーを作る
もう一つの方法が、Thoughtworks の Technology Radar という仕組みを個人に応用するパーソナルレーダーです。
Thoughtworks Technology Radar は、ソフトウェア業界の技術動向を4つの象限と4つのリングで可視化したものです。
4つの象限:
Tools ... 開発ツールから統合ツールまで
Languages & ... プログラミング言語、ライブラリ、フレームワーク
Frameworks
Techniques ... 開発プラクティスや手法
Platforms ... DB、クラウド、OS などの技術プラットフォーム
4つのリング(外側から内側へ):
Hold → 新規では使わないほうがよい技術
Assess → 調査する価値がある技術
Trial → パイロットプロジェクトで試す段階
Adopt → 積極的に採用すべき技術
これを個人利用に応用する場合、各リングの意味を次のように調整します。
| リング | 個人利用での意味 | 例 |
|---|---|---|
| Hold | 避けたい技術や断ちたい習慣 | 価値の低い情報源を読む習慣 |
| Assess | 気になっているが未調査の技術 | 将来のリサーチ候補 |
| Trial | 積極的にリサーチ・実験中 | スパイク実験をしている技術 |
| Adopt | 実戦投入している技術やベストプラクティス | 最も注力している新技術 |
技術ポートフォリオは金融ポートフォリオのように分散させることが効果的です。広く需要のある技術を押さえつつ、生成AIやIoTのような「賭け」も含めてバランスを取るというアプローチです。
パーソナルレーダーは、完成したレーダー図そのものよりも、作成するプロセス自体に価値があると言われています。忙しい日常の中で、技術動向について立ち止まって考える時間を確保する仕組みとして機能するからです。Thoughtworks が公開している Build Your Own Radar というツールを使えば、Google スプレッドシートからレーダー図を生成できます。
全ての判断はトレードオフである
技術的な幅を持つことで、アーキテクトは多くの選択肢を知ることができます。しかし、選択肢が増えるだけでは意味がありません。重要なのは、複数の選択肢のメリットとデメリットを分析し、文脈に応じた判断を下す力です。
ここからは、アーキテクチャ思考の核心とも言える「トレードオフ分析」の考え方と実例を見ていきます。
「It depends」の本当の意味
「REST とメッセージングのどちらが良いか?」「マイクロサービスは正しい選択か?」
こうした問いに対して、アーキテクトの答えは決まって「It depends(場合による)」です。これは逃げの回答ではありません。デプロイ環境、ビジネスドライバー、予算、期限、開発チームのスキルセット、企業文化......こうした無数の文脈依存要素があるため、万人に通用する正解が存在しないのです。
アーキテクチャの判断には正解も不正解もなく、あるのはトレードオフだけです。では、トレードオフをどう分析すればいいのか、具体例で見てみます。
トレードオフ分析の実例: オークションシステム
オンラインオークションの入札システムを考えます。Bid Producer(入札生成サービス)が入札情報を、Bid Capture(入札取得)、Bid Tracking(入札追跡)、Bid Analytics(入札分析)の3つのサービスに送る必要があるとします。
この非同期通信の実現方法として、トピック(Pub/Sub)方式とキュー(P2P)方式の2つが候補に挙がります。
【トピック方式(Pub/Sub)】
┌──────────────┐
┌─►│ Bid Capture │
┌──────────┐ │ └──────────────┘
│ Bid │───┤ ┌──────────────┐
│ Producer │ Topic│ Bid Tracking │
└──────────┘ │ └──────────────┘
│ ┌──────────────┐
└─►│ Bid Analytics│
└──────────────┘
Producer は 1つのトピックに送るだけ
【キュー方式(P2P)】
┌──────────┐ Queue A ┌──────────────┐
│ Bid │────────────►│ Bid Capture │
│ Producer │ Queue B ┌──────────────┐
│ │────────────►│ Bid Tracking │
│ │ Queue C ┌──────────────┐
│ │────────────►│ Bid Analytics│
└──────────┘ └──────────────┘
Producer は 3つのキューそれぞれに送る
一見すると、トピック方式のほうが良さそうに見えます。理由は明快です。
トピック方式のメリット:
- 拡張性が高い: 新しいサービス(たとえば Bid History)を追加したい場合、そのサービスがトピックを購読するだけで済みます。既存のサービスやインフラを変更する必要がありません
- 結合が弱い: Bid Producer はトピックに送信するだけで、誰がデータを使うかを知りません
しかし、ここでClojureの作者の言葉が効いてきます。
プログラマーはあらゆるもののメリットを知っているが、トレードオフは何も知らない。アーキテクトは両方を理解する必要がある
メリットだけでなく、デメリットも見なければなりません。
トピック方式のデメリット:
- データセキュリティの問題: トピックは誰でも購読可能なため、不正なサービスが入札データを盗み見るリスクがあります。キュー方式であれば、各キューのデータは特定のコンシューマしかアクセスできず、不正アクセスがあれば即座に検知できます
- コントラクトの柔軟性がない: トピックを使う場合、全てのコンシューマが同じデータ契約を受け入れる必要があります。たとえば Bid History だけが「現在の希望価格」も必要とする場合、全コンシューマに影響するコントラクト変更が必要になります。キュー方式ならチャネルごとに独立したコントラクトを持てます
- オートスケーリングが困難: トピック内のメッセージ数を個別に監視できないため、コンシューマごとの独立したスケーリングが難しくなります
この「トピックではオートスケーリングが困難」という点は技術依存です。たとえば AMQP(Advanced Message Queuing Protocol)では、Exchange(送信先)と Queue(受信元)が分離されているため、Pub/Sub のような構造でもキューごとのモニタリングやロードバランシングが可能です。
これらをまとめると、以下のようになります。
| 観点 | トピック方式 | キュー方式 |
|---|---|---|
| 拡張性 | 高い(新サービスの追加が容易) | 低い(新キュー追加 + Producer 変更が必要) |
| 結合の強さ | 弱い(Producer はコンシューマを知らない) | 強い(Producer が全キューを知っている) |
| セキュリティ | 弱い(誰でも購読可能) | 強い(特定コンシューマのみアクセス可) |
| コントラクトの柔軟性 | 低い(全コンシューマ同一) | 高い(チャネルごとに独立) |
| スケーリング | 困難(個別監視不可) | 容易(キューごとに監視・スケール可) |
では、どちらが正しいのか? 答えはやはり「It depends」です。「拡張性とセキュリティのどちらがこのシステムにとって重要か?」という問いに答えることが、アーキテクトの仕事です。そしてその答えは、ビジネスドライバー、セキュリティ要件、チームの体制、将来の拡張計画など、文脈によって変わります。
トレードオフを分析するためには、技術的な観点だけでなく、ビジネスが何を求めているか(スケーラビリティ、パフォーマンス、可用性などのアーキテクチャ特性)の理解も不可欠です。ビジネスドライバーからアーキテクチャ特性を導き出す方法については、後続の記事で詳しく扱います。
モジュール性 ― アーキテクトの分析レンズ
ここまで、アーキテクトの視点(技術的な幅)と思考法(トレードオフ分析)を見てきました。では、その視点で具体的に何を見るのでしょうか。
その答えの一つがモジュール性です。アーキテクトはシステムの構造的な健全性を評価するために、モジュール性を分析します。
モジュール性とは何か
モジュール性(Modularity)とは、関連するコードの論理的なまとまりのことです。Java であれば package、.NET であれば namespace といった、言語ごとにモジュールを表現する仕組みがあります。
ここで強調しておきたいのは、モジュール性は物理的な分離ではなく論理的なまとまりの概念だということです。たとえばモノリシックなアプリケーションの中でも、パッケージ構造を使って論理的にコードをまとめることは可能です。ただし、物理的な分離がないまま多数のクラスを一つにまとめてしまうと、いざアーキテクチャを再構成しようとしたときに、緩い区分が生んだ結合が足かせになります。
なぜアーキテクトがモジュール性に注意を払うのかというと、ソフトウェアシステムは放っておくとエントロピー(無秩序)に向かうからです。物理学と同じで、秩序を保つにはエネルギー(意識的な設計努力)が必要です。プロジェクトの要件に「モジュール性を良好に保つこと」と明記されることはまずありませんが、持続可能なコードベースにはモジュール性の秩序と一貫性が欠かせません。
モジュール性のように要件に明記されないがシステムの成功に不可欠な特性を、暗黙のアーキテクチャ特性と呼びます。詳しくは後続の記事で扱います。
モジュール性と粒度は違う
モジュール性を実践する際、最初にぶつかる問いが「どの粒度で分割するか」です。しかし、モジュール性(Modularity)と粒度(Granularity)は異なる概念です。
- モジュール性: システムが適切に部品に分割されている度合い(例: モノリス内のパッケージ分割やマイクロサービス化)
- 粒度: その部品のサイズをどう決めるか(例: 1つのサービスがどれだけの責務を持つか)
「モジュール性を受け入れよ、しかし粒度には注意せよ(Embrace modularity, but beware of granularity.)」という言葉があるように、粒度の判断を誤ると、サービス間の不要な結合が生まれ、分散モノリス(Distributed Monolith) やスパゲッティアーキテクチャといったアンチパターンに陥ります。「分割したのに、なぜか全部を一緒にデプロイしないと動かない」という状態は、粒度の判断ミスから来ていることが少なくありません。
では、粒度の判断を誤らないためにはどうすればよいのか。その判断基準となるのが、次のセクション以降で解説する凝集度と結合度です。
凝集度 ― モジュール内のまとまりを測る
モジュール性を評価するための最初の観点が**凝集度(Cohesion)**です。凝集度とは、モジュール内の要素がどれだけまとまっているかを表す指標です。
凝集度が高いモジュールは、全ての要素が同じ目的に向かっており、そのモジュールだけで機能が完結します。逆に、凝集度が低いモジュールは責務が不明確で、変更の影響範囲が予測しにくくなります。
構造化設計の分野では、こう述べられています。
Attempting to divide a cohesive module would only result in increased coupling and decreased readability.
(凝集したモジュールを分割しようとしても、結合の増加と可読性の低下を招くだけである)
つまり、まとまるべきものを無理に分けると、かえって悪い結果を招くということです。
凝集度の7つの種類
コンピュータサイエンスでは、凝集度を望ましい順に7段階に分類しています。
望ましい
▲ 1. 機能的凝集 全要素が一つの機能に関連し、必要なもの全てが含まれている
│ 2. 逐次的凝集 一方の出力がもう一方の入力になる
│ 3. 通信的凝集 同じデータを操作する、または同じ出力に寄与する
│ 4. 手続き的凝集 特定の順序で実行する必要がある
│ 5. 時間的凝集 タイミングの依存関係でまとめられている
│ 6. 論理的凝集 論理的にはカテゴリが同じだが、機能的には無関係
▼ 7. 偶発的凝集 要素間に関連性がなく、たまたま同じファイルにあるだけ
望ましくない
それぞれ具体例を交えて見ていきます。
1. 機能的凝集(Functional Cohesion)
最も望ましい状態です。モジュール内の全要素が一つの機能に関連し、その機能に必要なもの全てが含まれています。たとえば、ユーザー認証モジュールに認証に必要なロジックが全て揃っているような状態です。
2. 逐次的凝集(Sequential Cohesion)
あるモジュールの出力が、次のモジュールの入力になるパイプライン的な関係です。データを読み込むモジュールと、その読み込んだデータを変換するモジュールの関係が該当します。
3. 通信的凝集(Communicational Cohesion)
複数の処理が同じデータを操作する、または同じ出力に寄与する関係です。たとえば、データベースにレコードを追加する処理と、その情報に基づいてメールを生成する処理が同じモジュールにある場合です。
4. 手続き的凝集(Procedural Cohesion)
特定の順序で実行する必要がある処理がまとめられている状態です。「ファイルを開く → 読み込む → 閉じる」という一連の処理が典型例です。
5. 時間的凝集(Temporal Cohesion)
タイミングの依存関係でまとめられた処理です。システム起動時に実行する初期化処理群が該当します。個々の処理自体には機能的な関連がなく、「同じタイミングで実行する必要がある」という点だけで結びついています。
6. 論理的凝集(Logical Cohesion)
論理的にはカテゴリが同じですが、機能的には無関係な要素がまとめられている状態です。Java の StringUtils が典型です。文字列に対する様々なユーティリティメソッドが集まっていますが、各メソッドの機能自体に関連性はありません。
7. 偶発的凝集(Coincidental Cohesion)
最も望ましくない状態です。モジュール内の要素に関連性がなく、たまたま同じソースファイルにあるだけです。
凝集度の判断は「It depends」
凝集度の分類は理解できても、実際の判断となると一筋縄ではいきません。具体例で考えてみます。
以下のような Customer Maintenance モジュールがあるとします。
Customer Maintenance
├── add customer
├── update customer
├── get customer
├── notify customer
├── get customer orders ← これは顧客?注文?
└── cancel customer orders ← これは顧客?注文?
最後の2つの操作を Order Maintenance として別モジュールに分離すべきでしょうか?
Customer Maintenance Order Maintenance
├── add customer ├── get customer orders
├── update customer └── cancel customer orders
├── get customer
└── notify customer
この判断は、まさにトレードオフ分析の実践です。
- Order 関連の操作がこの2つだけなら: 分離するほどの規模ではないかもしれません。操作が2つしかない独立モジュールはかえって管理のオーバーヘッドになります
- Customer Maintenance が今後大きくなるなら: 早めに分離しておいた方が、後のリファクタリングコストを抑えられます
- 分離した場合に Customer 情報への依存が大きいなら: Order Maintenance が Customer のデータに頻繁にアクセスする必要がある場合、分離によってモジュール間の結合がかえって増えてしまいます。これは先述のとおり、凝集したモジュールの無理な分割です
正解は文脈によって変わります。先ほどのオークションシステムの例と同じく、ここでも「It depends」なのです。
LCOM ― 凝集度を数値で測る
凝集度の判断は主観的な面がありますが、構造的な凝集度を数値で測定する方法も存在します。それが LCOM(Lack of Cohesion in Methods) というメトリクスです。
LCOM は、クラス内のメソッドがフィールド(インスタンス変数)をどの程度共有しているかを測定します。「メソッドの凝集度の欠如」を測るという、少し回りくどい名前ですが、要するに「このクラス、本当に1つのクラスであるべきか?」を数値で判断するための指標です。
3つのクラスを例に見てみます。
Class X(LCOM 低い = 凝集度が高い)
┌─────────────────────────────────┐
│ [a] ─── method1() │
│ │ │
│ ├───── method2() │
│ │ │
│ [b] ─── method3() │
│ │ │
│ └───── method1() │
│ │
│ メソッドがフィールドを広く共有 │
└─────────────────────────────────┘
Class Y(LCOM 高い = 凝集度が低い)
┌─────────────────────────────────┐
│ [a] ─── method1() │
│ │
│ [b] ─── method2() │
│ │
│ [c] ─── method3() │
│ │
│ メソッドとフィールドが完全に分離 │
│ → 実質的に3つの別クラスにすべき │
└─────────────────────────────────┘
Class Z(混合 = リファクタリング候補あり)
┌─────────────────────────────────┐
│ [a] ─── method1() │
│ │ │
│ └───── method2() │
│ │
│ [b] ─── method3() ← 分離可能 │
│ │
│ 一部が独立 → 分離候補 │
└─────────────────────────────────┘
この3つのパターンのうち、実務で最も多く遭遇するのは Class Z のような混合状態です。Class Y のように完全に分離しているケースは発見しやすいのですが、Class Z は「なんとなく1つのクラスにまとまっている」ため見落とされがちです。
LCOM はアーキテクチャの再構成やコードベースの移行を検討する際に、「意図せず1つのクラスにまとめてしまった無関係な責務」を発見するのに役立ちます。共通ユーティリティクラスの分割判断などでも使えます。
LCOM の限界: LCOM が測定できるのは構造的な凝集度のみです。「論理的にこれらの要素は一緒にあるべきか?」という判断はできません。これはアーキテクチャの第2法則「How よりも Why が重要(構造がどうなっているかより、なぜそうなっているかが重要)」に通じる話で、メトリクスはあくまで判断材料の一つにすぎません。
結合度 ― モジュール間の依存を測る
凝集度が「モジュール内部のまとまり」を見るものだったのに対し、結合度(Coupling) は「モジュール同士の依存関係」を見る指標です。結合度が高いシステムは、一箇所の変更が広範囲に波及し、変更コストが大きくなります。
求心性結合と遠心性結合
結合を方向で分類する2つの基本メトリクスがあります。グラフ理論に基づいています。メソッドの呼び出しと戻り値がコールグラフを形成するため、数学的に分析が可能です。
依存している
Module A ──────────────► Module X
Module B ──────────────►
Module C ──────────────►
Module X ──────────────► Module P
から見ると Module Q
依存している
Module X の求心性結合(Ca)= 3(A, B, C が依存してくる)
Module X の遠心性結合(Ce)= 2(P, Q に依存している)
- 求心性結合(Afferent Coupling, Ca): あるモジュールへの「入ってくる」接続の数です。「このモジュールに依存しているモジュールがいくつあるか」を表します。求心性結合が高いモジュールを変更すると、多くのモジュールに影響が及びます
- 遠心性結合(Efferent Coupling, Ce): あるモジュールからの「出ていく」接続の数です。「このモジュールが依存しているモジュールがいくつあるか」を表します。遠心性結合が高いモジュールは、依存先のいずれかが変更されると壊れやすくなります
抽象度と不安定度
求心性結合と遠心性結合は個々のモジュールの結合の方向と量を示しますが、コードベース全体の構造的な健全性を俯瞰するにはまだ不十分です。そこで、これらを組み合わせた派生メトリクスが定義されています。
抽象度(Abstractness) は、抽象的な要素(インターフェース、抽象クラスなど)と具象的な要素(具体的なクラス)の比率です。
抽象的な要素の数
抽象度 = ─────────────────────
具象的な要素の数 + 抽象的な要素の数
たとえば、5,000行のコードが全て1つの main() メソッドに書かれていたら、抽象度はほぼ0です。逆に、Spring Framework に実在する AbstractSingletonProxyFactoryBean のような何層にも抽象化されたクラスだらけのコードベースは、抽象度が高すぎて何がどう結びついているのか理解しにくくなります。
不安定度(Instability) は、遠心性結合の割合を示す指標で、コードベースの変更に対する脆さを表します。
遠心性結合(Ce)
不安定度 = ───────────────────────────
遠心性結合(Ce)+ 求心性結合(Ca)
不安定度が高いということは、他のモジュールへの依存が大きく、依存先の変更によって壊れやすいことを意味します。
主系列からの距離 ― 抽象度と不安定度のバランスを測る
抽象度と不安定度を組み合わせた総合的な評価指標が、主系列からの距離(Distance from the Main Sequence) です。
主系列からの距離 = |抽象度 + 不安定度 - 1|
抽象度と不安定度はどちらも0から1の値を取るため、この2つを縦軸と横軸にしたグラフを描くことができます。
抽象度
1.0 ┌──────────────────────────────┐
│\ 無用ゾーン │
│ \ (Zone of Uselessness) │
│ \ 抽象化しすぎて │
│ \ 使いにくい │
│ \ │
│ 理想線 \ ● あるクラス │
│ (Main \ ↗ この距離を │
│ Sequence) \ 測る │
│ \ │
│ 苦痛ゾーン \ │
│ (Zone of Pain) \ │
│ 具象的で多くの依存を抱え \ │
│ 変更が困難 \ │
0.0 └──────────────────────────────┘
0.0 1.0
不安定度
理想線(Main Sequence)に近いほど、抽象度と不安定度のバランスが取れた健全な状態です。
抽象度が高く不安定度が低い領域(グラフ左上)に外れると「無用ゾーン(Zone of Uselessness)」
不安定度が低い(=多くのモジュールから依存されている安定した位置にいる)にもかかわらず抽象化しすぎているため、実際に使うのが困難なコードです
抽象度が低く不安定度も低い領域(グラフ左下)に外れると「苦痛ゾーン(Zone of Pain)」
具象的な実装ばかりで、かつ不安定度が低い(=求心性結合が大きく、多くのモジュールから依存されている)ため、変更したくても変更が困難で苦痛を伴うコードです
多くのプラットフォームには、これらのメトリクスを計算するツールが用意されています。コードベースの把握、移行計画の策定、技術的負債の評価などに活用できます。
コナーセンス ― 結合をより精密に語る
求心性結合と遠心性結合は結合の「量」を測りますが、結合の「種類」については何も教えてくれません。
たとえば、2つのモジュールが結合しているとして、それが「メソッド名が一致しなければ動かない」という結合なのか、「実行のタイミングが一致しなければ動かない」という結合なのかで、影響の深刻さは全く異なります。前者はリファクタリングツールで簡単に対処できますが、後者はレースコンディションのような厄介なバグにつながります。
コナーセンス(Connascence) は、こうした結合の種類を精密に言語化するためのフレームワークです。
コナーセンスとは
コナーセンスの定義はシンプルです。「あるコンポーネントを変更したとき、システム全体の正しさを保つために別のコンポーネントも変更しなければならない」 という関係をコナーセンスと呼びます。1996年に提唱されました。
重要なのは、コナーセンスは結合の「メトリクス(数値指標)」ではなく、結合の種類を記述するための 「語彙」 だということです。デザインパターンが共通の語彙を提供するのと同じように、コナーセンスは結合の種類について精密にコミュニケーションするための言葉を提供します。
たとえば、コードレビューで単に「マジックストリングを定数にしてください」と言う代わりに、「この結合は暗黙の意味への依存なので、名前による依存に変換しましょう」のように、結合の種類と改善方針を精密に伝えられるようになります。
コナーセンスは大きく静的コナーセンスと動的コナーセンスの2つに分かれます。
静的コナーセンス ― ソースコード上の結合
静的コナーセンスは、ソースコードの解析だけで検出できる結合です。弱い(望ましい)順に5種類あります。
1. 名前のコナーセンス(Connascence of Name)
複数のコンポーネントが、エンティティの名前に合意する必要がある結合です。メソッド名やパラメータ名がこれにあたります。最も一般的で、最も望ましい結合です。現代のリファクタリングツール(IDE のリネーム機能など)を使えば、コードベース全体で名前を一括変更できるため、対処が容易です。
2. 型のコナーセンス(Connascence of Type)
複数のコンポーネントが、エンティティの型に合意する必要がある結合です。静的型付け言語では変数やパラメータの型定義がこれに該当します。Clojure のように、動的型付け言語でも選択的に型指定ができるものもあります。
3. 意味のコナーセンス(Connascence of Meaning)
特定の値の意味に合意する必要がある結合です。Connascence of Convention(慣例のコナーセンス)とも呼ばれます。
典型例はマジックナンバーです。int TRUE = 1; int FALSE = 0 という定義がコードベースのどこかにあったとして、もし誰かがこの値を逆にしたらどうなるか......想像するだけで背筋が冷えます。
4. 位置のコナーセンス(Connascence of Position)
値の順序に合意する必要がある結合です。
たとえば void updateSeat(String name, String seatLocation) というメソッドを updateSeat("14D", "Ford, N") と呼び出してしまった場合、型としては正しい(どちらも String)のにセマンティクスが壊れます。引数の順序を間違えただけでバグになり、しかもコンパイラでは検出できません。
5. アルゴリズムのコナーセンス(Connascence of Algorithm)
特定のアルゴリズムに合意する必要がある結合です。静的コナーセンスの中では最も強い結合です。
たとえば、サーバーとクライアントで同一のハッシュアルゴリズムを使って認証を行うシステムでは、片方のアルゴリズムが変わるとハンドシェイクが失敗します。どちらか一方の変更が、必ずもう一方の変更を要求するという、高い結合です。
動的コナーセンス ― 実行時の結合
動的コナーセンスは、実行時にのみ検出できる結合です。ソースコードを読んだだけでは発見しにくく、静的コナーセンスより一般に扱いが難しい種類です。
1. 実行順序のコナーセンス(Connascence of Execution)
複数コンポーネントの実行順序が重要な結合です。プロパティを特定の順序でセットしないと正しく動作しないコードが典型例です。「先に A を設定してから B を設定しないとエラーになる」という暗黙の制約があるケースです。
2. タイミングのコナーセンス(Connascence of Timing)
実行のタイミングが重要な結合です。2つのスレッドが同時に実行されることで発生するレースコンディションが代表例です。タイミングのずれが結果に影響するため、テストでの再現も難しい厄介な結合です。
3. 値のコナーセンス(Connascence of Values)
複数の値が互いに依存しており、一緒に変更しなければならない結合です。
四角形を4つの点で表現した場合、1つの点だけを変更すると四角形としての整合性が壊れます。分散システムにおけるトランザクションも同様で、複数データベースにまたがる値は全て同時に更新されるか、全て更新されないかのどちらかでなければなりません。
4. 同一性のコナーセンス(Connascence of Identity)
複数のコンポーネントが同じエンティティを参照する必要がある結合です。独立した2つのコンポーネントが同一の分散キューを共有・更新するケースが典型例です。
コナーセンスの3つの性質と活用ルール
コナーセンスの種類を覚えただけでは実務には活かしきれません。活用するために知っておくべき3つの性質があります。
強度(Strength) ― そのコナーセンスをどれだけ容易にリファクタリングできるかを表します。
弱い(望ましい) 強い(望ましくない)
◄━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━►
静的コナーセンス 動的コナーセンス
名前 → 型 → 意味 → 位置 → アルゴリズム → 実行順序 → タイミング → 値 → 同一性
静的コナーセンスはソースコードの解析で検出でき、ツールで対処しやすいため、動的コナーセンスよりも望ましいとされます。たとえば、意味のコナーセンス(マジックナンバー)を名前のコナーセンス(名前付き定数)にリファクタリングすることで、結合の強度を下げることができます。
局所性(Locality) ― コナーセンスを持つモジュール同士が、コードベース上でどれだけ近いかを表します。
同じモジュール内であれば、強いコナーセンスがあっても大きな問題にはなりません。しかし、モジュールをまたいで強いコナーセンスがあるのは問題です。たとえば、同じモジュール内で意味のコナーセンスがある場合と、異なるモジュール間で意味のコナーセンスがある場合では、後者のほうがはるかにダメージが大きくなります。
この「実装の結合はなるべく狭いスコープに閉じ込める」という考え方は、DDD(ドメイン駆動設計)の**境界づけられたコンテキスト(Bounded Context)**の発想と同じです。1996年に述べられた洞察が、後のDDDで改めて体系化されたとも言えます。
度合い(Degree) ― 変更が影響するモジュールの数です。少数のモジュールに閉じた動的コナーセンスは大きな問題にはなりませんが、コードベースが成長するにつれて問題は拡大していきます。
これらの性質を踏まえ、3つのガイドラインが提示されています。
- システムを分割してコナーセンス全体を最小化する
- カプセル化境界を越えるコナーセンスを最小化する
- カプセル化境界内のコナーセンスを最大化する
また、この概念を再び広めたエンジニアは、2つのルールを提唱しています。
Rule of Degree: 強いコナーセンスを弱いコナーセンスに変換せよ(ここでの degree は前述の「度合い(影響モジュール数)」ではなく、コナーセンスの強さの段階を指す)
局所性のルール: ソフトウェア要素間の距離が離れるほど、弱いコナーセンスを使え
コナーセンスは主にモジュール/クラスレベルの結合を精密に記述するために生まれた語彙です。マイクロサービス間の非同期通信やイベント駆動アーキテクチャにおける結合パターンなど、より大きなスケールの結合については、コナーセンスだけではカバーしきれない面があります。これらはアーキテクチャスタイルの話題として後続の記事で扱います。
アーキテクトとコーディングの関係
ここまで、アーキテクト思考の3つの柱――技術的な幅、トレードオフ分析、モジュール性という分析レンズ――を見てきました。最後に、もう一つの重要なテーマである、アーキテクトとコーディングの関わり方を取り上げます。
凝集度・結合度・コナーセンスといった視点は、コードに触れ続けてこそ磨かれます。しかし、アーキテクトはフルタイムの開発者ではなくなるため、コーディングとの関わり方を意識的にデザインする必要があります。このセクションでは、アーキテクトがコーディングを続けるための具体的な方法と、陥りやすい罠を紹介します。
ボトルネックトラップを避ける
アーキテクトがコーディングに関わる際に陥りやすいのが、ボトルネックトラップです。
これは、アーキテクトがフレームワークの中核コードや複雑な部分など、クリティカルパスのコードの所有権を持ってしまうことで起きます。アーキテクトはフルタイムの開発者ではないため、ミーティングや設計業務と開発作業を兼務することになり、チームの進行を妨げるボトルネックになってしまいます。
対策はシンプルです。クリティカルな部分は開発チームに任せ、アーキテクト自身は1〜3イテレーション先の小さなビジネス機能の実装を担当します。
この方法には3つの利点があります。
- アーキテクトがボトルネックにならずに本番コードを書く経験が得られる
- クリティカルパスのコードがチームに分散され、チームの理解が深まる
- アーキテクトが開発チームと同じビジネスロジックのコードを書くことで、チームの苦労や開発環境の課題を肌で感じ、改善に動ける
コーディングを続ける5つの方法
チームの開発に直接参加できない場合でも、技術力を維持する方法はあります。
1. PoC(概念実証)の頻繁な実施
技術選定で迷ったとき、候補の技術それぞれで動くサンプルを自分の手で作ってみる方法です。実装の難易度やアーキテクチャ特性を肌で感じられます。PoC であってもなるべく本番品質で書くことが推奨されています。「捨てるつもりのコード」がいつの間にかリファレンス実装として扱われるリスクがあるためです。
2. 技術的負債への対応
技術的負債の解消はアーキテクチャ全体の見通しを持つアーキテクトに向いた作業であり、リファクタリングを通じてコードベースの実態を把握できます。優先度が低いタスクのため、イテレーション内で完了できなくても影響が小さいのも利点です。
3. バグ修正
地味ですが、実際のバグを追う過程でコードの依存関係やモジュール間の結合の実態を把握でき、アーキテクチャの問題点を発見する良い機会になります。
4. 自動化ツールの作成
開発チームの反復作業を自動化するコマンドラインツールやアナライザを作ることで、チームへの貢献と技術力の維持を両立できます。コーディング規約チェッカーやリファクタリングスクリプトなどが例として挙げられます。また、ArchUnit(アーキテクチャルールをユニットテストとして記述・検証できるライブラリ)のようなツールでアーキテクチャ準拠のテストを自動化する方法もあります。
5. コードレビュー
自分でコードを書かなくても、レビューを通じてコードに関わり続けることができます。アーキテクチャ準拠の確認やメンタリングの機会にもなります。
まとめ ― 「何を見るか」が変わるとき
この記事では、開発者からアーキテクトへの視点の転換と、アーキテクトが使う分析ツールとしてのモジュール性を解説してきました。
振り返ると、以下のストーリーラインをたどってきたことになります。
- 開発者からアーキテクトへの転換は、「深さ」から「幅」への視点シフトである
- その幅を持って、あらゆる判断をトレードオフとして分析する
- トレードオフ分析の対象としてモジュール性を見る際、凝集度・結合度・コナーセンスという語彙が武器になる
- そして、コードを書き続けることもアーキテクトにとって重要である
自分がいま書いているコードを、凝集度・結合度・コナーセンスの視点で眺めてみると、何が見えてくるでしょうか。「このクラス、LCOM が高そうだな」「このモジュール間の結合、位置のコナーセンスを名前のコナーセンスにリファクタリングできないかな」。そうした問いが自然に浮かぶようになったとき、見ているものが変わり始めているのかもしれません。