😺

結合とCQRSについて

に公開

この記事はヌーラボブログリレー2025 冬の10日目として投稿しています。


こんにちは。kenta-afkです。
今回は、11/20~21に現地参加した、アーキテクチャカンファレンス2025のセッションで取り上げられた『結合』と、企業ブースでとても分かりやすいCQRSの図があったので紹介します。

なぜこれを書こうと思ったのかというと、現在インターンで取り組んでいる実務の中で迷っていることがあったのですが、それがこのセッションやその後の学びで解消されたからです。

このカンファレンスの目的は、現在使っているアーキテクチャ設計が正しいのか判断に迷う、といった課題を解消することであり、私にとって非常に良い機会となりました。

結合

DDDが描く戦略的トレードオフのアートというセッションタイトルでVlad Khononovさんというソフトウェアアーキテクトが登壇されていました。

以下は現地でリアルタイムで書かれていたグラフィックレコーディングです。
個人的にとても良い取り組みだと思いました。

登壇者の方は以下のセッションの一部で4つの結合について例を挙げて解説していました。

  • 侵入的結合(Intrusive Coupling)
  • 機能的結合(Functional Coupling)
  • モデル結合(Model Coupling)
  • 契約結合(Contract Coupling)

これらの結合は、Vlad Khononov氏が開発した、統合強度に基づき結合度を再定義した実用的なフレームワークです。

1.侵入的結合(Intrusive Coupling)

侵入的結合は最も結合強度が高いと言われています。

この結合形態は、あるモジュールが、他のモジュールの内部データ構造、プライベートな変数、またはコード自体に直接アクセスし、場合によってはそれを変更します。

以下はその具体例です。

// module_a/src/lib.rs

pub struct UserProfile {
    pub id: u32,
    name: String, 
}

impl UserProfile {
    pub fn new(id: u32, name: String) -> Self {
        UserProfile { id, name }
    }
    
    pub fn get_name(&self) -> &str {
        &self.name
    }
}

// main.rs (侵入側)
use module_a::UserProfile;

fn main() {
    let mut user = UserProfile::new(1, "Alice".to_string());
    
        // 仮にnameフィールドがpubで、かつmodule_a側で'full_name'に変更された場合、この行はエラーになる。
    // user.name = "Bob".to_string();

    let username = user.get_name();
}

侵入的結合の存在は、モジュール内部の変更を行うたびにその依存関係にあるモジュールに対して、追加作業を強制的に生み出してしまいます。
グラフィックレコーディングにも「危険!恐ろしい!何が起こってるかわからない」と書いているのでそれほど危険なものであることが伺えます。

何でもかんでもパブリックにしないことで、変更可能性の範囲をコードレベルで抑制できると言えるでしょう。

2.機能的結合(Functional Coupling)

この結合形態では、モジュールは、データだけではなく、共通の知識や状態、あるいは制御情報を共有します。

以下がその例です。

  • モジュールがグローバル変数や共有メモリ領域にアクセスし、共通の状態を介して結合する。
  • 外部ファイル定義や通信プロトコルといった外部的なデータ形式を共有する。
  • あるモジュールがフラグや制御値を相手に渡し、相手がその情報に基づいて自身の内部ロジックを分岐させる。つまり、呼び出し元が呼び出し先の「やり方」をある程度知っている必要がある。

機能的結合による問題は、共有される状態や制御機構が変更された場合、その「知識」を共有するすべてのモジュールが影響を受ける点にあります。

これらはカプセル化ストラテジーパターンなどを使って契約結合へと変更できます。

3.モデル結合(Model Coupling)

この結合形態は、モジュールが、ビジネスドメインの内部表現や複合的なデータ構造(例:データベースエンティティ、DTO:データ転送オブジェクト)を共有する場合に発生します。

これに関しては業務でも意識していることであり、別々に定義するようにしています。ただし、ここに関しては意見が分かれているようです。

「モデルを同一視する派」は、ORMの変更追跡機能や開発速度のメリットを重視し、ORMの詳細がドメインにわずかに漏洩する(侵入的結合)ことは許容できるトレードオフだと主張しています。一方で、「モデルを分離する派」は、純粋なドメイン隔離とカプセル化の維持が長期的な技術的負債を防ぐ上で不可欠だと主張しています。

4.契約結合

契約結合は、Khononovモデルにおいて最も統合強度が弱いと評価される、理想的な結合形態です。

契約結合の特徴は、モジュール間のやり取りが、内部の実装モデルではなく、明示的に定義され、合意された統合契約のみを通じて行われる点にあります。この「契約」は、APIスキーマ、明確に定義されたインターフェースや型定義、またはメッセージキューのペイロードスキーマなど、外部に公開するために最適化された形式で表現されます。

これにより、モジュールは外部の契約を破らない限り、自身の内部の実装やデータモデルを自由にリファクタリングしたり、進化させたりすることが可能になります。これは、結合度が最も希薄であり、変更の隔離効果が最大化されることを意味します。

どこで適用させるのか?

契約結合が理想的な状態ということは分かりましたが、実際のセッションでは、何でもかんでもそれを適用すればいいわけではないということも言ってました。
グラフィックレコーディングを見ると分かりやすいのですが、そのプロジェクトチームの距離、競争優位性はどこなのかということを考慮した上で必要に応じて適用させていくことが適切だと考えられます。

もちろん競争優位性のある場所を契約結合で柔軟な形にしておくことは推奨されます。

CQRS

CQRSとは書き込み(Command)と読み取り(Query)に分離して、変更容易性やスケールしやすくするための手法です。

以下がブースに掲示されていた図です。

初めて見た時に「分かりやすい!」って思いました。なのでこれを見て説明しようと思っています。

書き込み(Command)

書き込みは、レイヤードアーキテクチャでそのままinfrastructure層のリポジトリを書き込み専用に最適化しただけです。

domain層でドメインオブジェクトを構築してそれをそのままinfrastructure層のリポジトリで保存するといった至って普通の構成になっています。

読み取り(Query)

読み取りは、見ての通りapplication層とinfrastructure層で完結していることがわかると思います。application層にQueryServiceというインターフェースを定義しておいてその具体実装をinfrastructure層に書くというものです。
そしてpresentation層とは別に読み取りの契約としてDTOを定義しておくということが大切です。
このDTOがapplication層の契約となり、application層とpresentation層を低結合にしてくれます。

ここでわかることが、書き込みと読み取りで使う部分が異なるということです。見ての通り読み取りはdomain層は経由しません。
これによりUI変更などが生じたときは変更する場所はapplicationとその読み取る場所のみとなります。

書き込み(Command)に関しては、ドメインモデルで整合性を担保した上で保存しなければならないのでドメイン層を経由する必要がありますが、読み取り(Query)はデータを読み取るだけでデータの整合性は書き込み時に保証されているので、ドメイン層を経由しなくていいということですね。

この明確な責務分離により変更容易性が上がります。

まとめ

今回は結合とCQRSについて書いてみました。
今後の展望としては、「イベントソーシングのカンファレンスである『ESカンファレンス(Event Sourcing Conference)に参加するのでそれとCQRSを交えて次の記事で書きたいと考えています。

最後まで読んでくださりありがとうございました。

Discussion