😽

均衡度で決めるリファクタリング方針

に公開

はじめに

先日、下記の書籍を読みました。
https://book.impress.co.jp/books/1124101149
その中で紹介されていた「均衡度」という指標がとても勉強になりました。システムが変更しづらくなる原因は、モジュール間の結合度が高いことだけではありません。結合度に加えて「距離」や「変動性」も合わせて捉えることで、モジュール間の関係が変更に強いのか、それとも変更に弱いのかをより具体的に見立てられます。これが、均衡度という考え方です。

本記事ではまず、均衡度の式が「統合強度・距離・変動性」という3つの次元の組み合わせによって、どのようにモジュール性(変更に強い状態)と複雑性(変更に弱い状態)を判別するのかを整理します。次に、いくつかの具体例を通して、均衡度の見立てから「距離を近づけるべきか」「統合強度を下げるべきか」といったリファクタリングの方向性を考えていきます。

均衡度について

均衡度は、モジュール間の関係がモジュール性(変更容易性が高い)なのか、複雑性(変更容易性が低い)なのかを、統合強度距離変動性の3つの次元で考えます。
統合強度、距離、変動性を0か1の2値スケールで表す場合、均衡度は下記の式で求まります。

均衡度 = (強度 XOR 距離) OR NOT 変動性

上記の式の場合、均衡度が1であればモジュール性を意味し、均衡度が0であれば複雑性を意味します。強度・距離・変動性の3つの次元がどのような組み合わせの時に、均衡度がモジュール性 or 複雑性になるのかを例を挙げて整理します。

※ 均衡度は連続値として扱うこともできますが、本記事では理解を簡単にするため0/1の二値で説明します。

高い強度・遠い距離・高い変動性の場合

この場合、均衡度は0になり複雑性に分類されます。

均衡度 = (1 XOR 1) OR NOT 1 
    = 0

モジュール間の統合強度が高いということは、それらのモジュールは同時に変更される可能性が高いです。にもかかわらず、モジュール間の距離が離れていることによって、変更する際のコストが高い状態です。さらに、変動性が高いので、上流モジュールが変更されるたびに、これらの高強度・遠い距離による変更しづらい影響を頻繁に受けてしまいます。何かを変更する際に頻繁に痛みを伴う最悪な状態と言えます。

高い強度・遠い距離・低い変動性の場合

この場合、均衡度は1になりモジュール性に分類されます。

均衡度 = (1 XOR 1) OR NOT 0
    = 1

モジュール間の統合強度が高く距離も遠いので、ここだけを見たら変更容易性が低い状態です。しかし、変動性が低くそもそも上流モジュールが変更されることがないので、これらの変更しづらい影響を受けることはありません。統合強度が高く距離も遠いことによる悪い影響を、変動性の低さが打ち消していると言えます。

低い強度・近い距離・高い変動性の場合

この場合は、均衡度は0になり複雑性に分類されます。

均衡度 = (0 XOR 0) OR NOT 1
    = 0

モジュール間の統合強度が低いということは、本来それらのモジュールは同時に変更される可能性が低く、全く関連し合っていません。にもかかわらず距離が近い(同じクラス内など)と、変更時に「関係の薄い要素」が視界に入りやすくなり、読む範囲や調査範囲が広がって認知負荷が増えます。また、そのモジュールに複数の変更の理由があるということなので、無駄に変動性を増加させる原因にもなります。上流モジュールが頻繁に変更されるたびに、これらの痛みを伴う最悪な状態と言えます。

低い強度・遠い距離・高い変動性の場合

この場合は、均衡度は1になりモジュール性に分類されます。

均衡度 = (0 XOR 1) OR NOT 1
    = 1

モジュール間の統合強度が低いということは、それらのモジュールは同時に変更される可能性が低く、全く関連し合っていません。さらに距離が遠いため、物理的にも論理的にも分離されています。この組み合わせは「弱い結合は遠くに置く」というバランスが取れた配置であり、たとえ変動性が高く上流モジュールが頻繁に変更される状況でも、変更の影響が広がりにくく変更コストが増えにくいです。つまり、変動性が高くても、低い強度が変更の巻き込みを抑え、遠い距離が独立性をさらに強めるため、全体として変更容易性を保ちやすい状態と言えます。

高い強度・近い距離・高い変動性の場合

この場合、均衡度は1になりモジュール性に分類されます。

均衡度 = (1 XOR 0) OR NOT 1
    = 1

モジュール間の統合強度が高いということは、それらのモジュールは同時に変更される可能性が高いです。さらに、距離が近い(同じクラス内など)ので、変更箇所が近くにまとまっており、修正や確認を比較的容易に行えます。変動性が高く変更が頻繁に起きる状況でも、変更の影響が小さい範囲に閉じるのであれば変更容易性は損なわれないので、結果としてモジュール性に分類されます。

均衡度の式を使うと、モジュール間の関係がモジュール性なのか複雑性なのかを推定できます。
その結果をもとに、今リファクタリングをして結合や配置を見直すべきか、あるいは変動性が低いなどの理由で当面は現状維持でよいかを判断するための材料の一つになります。

均衡度の具体例

ここでは、実際に経験したことがある類似の実例を挙げて、均衡度の推定値からどのようにモジュール間の関係をリファクタリングしていくべきなのかをまとめます。

侵入結合

サービスBがサービスAの所有しているDBに読み書きを行っており侵入結合しているとします。

図1: サービスBは汎用サブドメインであるサービスAと侵入結合している
サービスAは汎用サブドメインに属するシステムであり変動性は低いです。この場合、サービスAとサービスBの均衡度は下記のようになります。

統合強度 = 1(侵入結合しているので統合強度は高い)
距離 = 1(異なるシステムであり距離は遠い)
変動性 = 0(上流モジュールはサービスAであり、サービスAの変動性は低い)

均衡度 = (1 XOR 1) OR NOT 0
    = 1

統合強度の高さと距離の遠さによる複雑な状態を、変動性の低さが緩和してくれています。この場合、侵入結合してしまっていますが、この部分のリファクタリングに時間を割いても見返りは少ないので優先度は低いです。

今度は、図1と同じく、サービスBがサービスAと侵入結合しているとします。ただし、サービスAはコアサブドメインに属するシステムであり変動性が高いです。

図2: サービスBはコアサブドメインであるサービスAと侵入結合している
この場合、サービスAとサービスBの均衡度は下記のようになります。

統合強度 = 1(侵入結合しているので統合強度は高い)
距離 = 1(異なるシステムであり距離は遠い)
変動性 = 1(上流モジュールはサービスAであり、サービスAの変動性は高い)

均衡度 = (1 XOR 1) OR NOT 1
    = 0

上流モジュールであるサービスAの変動性が高いことにより、統合強度の高さと距離が遠いことによる複雑性の影響を頻繁に受けてしまうような、非常に痛みを伴う状態になってしまっています。

この場合、サービスAとサービスBの距離を近づけるか、統合強度を下げるようなリファクタリングを行っていく必要があります。例えば、サービスAとサービスBをコントラクト結合にして、統合強度を下げると下記のようになります。

図3: サービスAとサービスBの統合強度をコントラクト結合にした

統合強度 = 0(コントラクト結合なので統合強度は低い)
距離 = 1(異なるシステムであり距離は遠い)
変動性 = 1(上流モジュールはサービスAであり、サービスAの変動性は高い)

均衡度 = (0 XOR 1) OR NOT 1
    = 1

サービスAとサービスBの統合強度が低くなったことにより、サービスAの変更の影響がサービスBに波及しにくくなりました。これにより、サービス間の距離の遠さと、サービスAの変動性の高さによる影響を緩和することができました。

集約の重複

サービスAとサービスBで同じデータ群をDBに書き込んでおり、集約がサービス間で重複している場合を考えます。

図4: サービスAとサービスBは機能結合している
サービスAとサービスBはコアサブドメインであり変動性が高いとします。この場合、サービス間で集約が重複しているため機能結合してしまっています。そのため、仕様変更の度に、サービスAとサービスBを同時に変更して同時にデプロイしないといけない可能性が高いです。この場合の均衡度を下記に示します。

統合強度 = 1(対象機能結合なので統合強度は高い)
距離 = 1(異なるシステムであり距離は遠い)
変動性 = 1(サービスAとサービスBはコアサブドメインであり、変動性は高い)

均衡度 = (1 XOR 1) OR NOT 1
    = 0

統合強度の高さと距離が遠いことによる複雑性の影響を、変動性の高さが助長してしまっています。そのため、サービス間の統合強度を低くするか距離を近づけるようなリファクタリングを行う必要があります。

今回の場合、統合強度をこれ以上下げるのは難しいので、サービスAとサービスBを一つのサービスに共通化して距離を近づけるリファクタリングを行うのが選択肢の一つとして挙げられます。

責務の混在

下記に示す生年月日を表すオブジェクトを例として考えます。

namespace App\Domain\Model;

use DateTimeImmutable;
use InvalidArgumentException;

class BirthDate
{
    private DateTimeImmutable $value;

    private DateTimeImmutable $currentDate;

    private const RESTRICTED_AGE = 20;

    private const WEEK = [
        '日', '月', '火', '水', '木', '金', '土'
    ];

    public function __construct(
        DateTimeImmutable $birthDate,
        DateTimeImmutable $currentDate
    )
    {
        if ($birthDate >= $currentDate) {
            throw new InvalidArgumentException('生年月日は未来の日付を指定できません。value: ' . $birthDate->format('Y-m-d'));
        }

        $this->value = $birthDate;
        $this->currentDate = $currentDate;
    }

    /**
     * 一部の機能の利用が制限されているかどうかを判定する
     * ビジネスロジックの想定です
     */
    public function isRestrictedUse(): bool
    {
        // 満20歳未満の場合は制限対象
        return $this->getAge() < self::RESTRICTED_AGE;
    }

    /**
     * 生年月日を画面用にフォーマットする
     * 表示に関するロジックの想定です
     */
    public function formatValue(): string
    {
        return $this->value->format('Y年m月d日') . '(' . self::WEEK[$this->value->format('w')] . ')';
    }

    private function getAge(): int
    {
        $diff = $this->currentDate->diff($this->value);
        return $diff->y;
    }
}

isRestrictedUseメソッドはビジネスロジックで、formatValueメソッドは表示に関するロジックなのでそれぞれで責務が異なります。よって、それぞれのメソッドは同時に変更される可能性が低いです。ここで、formatValueメソッドの変更頻度が高いとすると、均衡度は下記のようになります。

統合強度 = 0(それぞれのメソッドは全く異なる責務を有しているので統合強度は低い)
距離 = 0(同じオブジェクトのメソッド同士なので距離は近い)
変動性 = 1(formatValueメソッドの変更頻度が高く、BirthDateオブジェクト自体の変動性は高い)

均衡度 = (0 XOR 0) OR NOT 1
    = 0

均衡度は複雑性になってしまいました。表示に関するformatValueメソッドを変更する際に、全く関係ないビジネスロジックも多少なりとも考慮する必要があり認知的負荷が高そうです。また、変更頻度が高いformatValueメソッドがBirthDateオブジェクトにあることで、不必要にBirthDateオブジェクトの変動性を上げてしまっています。BirthDateオブジェクトはドメイン層に属しており、最上位のオブジェクトです。このオブジェクトの変動性が高いと、頻繁にシステムのあちこちに変更したときの影響が波及してしまい改修が大変になってしまいます。

これらの事から、isRestrictedUseメソッドとformatValueメソッドの距離を遠くすることで、低い統合強度とのバランスを取る必要があります。下記にリファクタリング後のコード例を示します。

namespace App\Domain\Model;

use DateTimeImmutable;
use InvalidArgumentException;

class BirthDate
{
    private DateTimeImmutable $value;

    private DateTimeImmutable $currentDate;

    private const RESTRICTED_AGE = 20;

    public function __construct(
        DateTimeImmutable $birthDate,
        DateTimeImmutable $currentDate
    )
    {
        if ($birthDate >= $currentDate) {
            throw new InvalidArgumentException('生年月日は未来の日付を指定できません。value: ' . $birthDate->format('Y-m-d'));
        }

        $this->value = $birthDate;
        $this->currentDate = $currentDate;
    }

    public function isRestrictedUse(): bool
    {
        return $this->getAge() < self::RESTRICTED_AGE;
    }

    private function getAge(): int
    {
        $diff = $this->currentDate->diff($this->value);
        return $diff->y;
    }
}
namespace App\Presentation;

use DateTimeImmutable;


class BirthDateFormatter
{
    private const WEEK = [
        '日', '月', '火', '水', '木', '金', '土'
    ];

    public function __construct(private DateTimeImmutable $birthDate) {}

    /**
     * 生年月日を画面用にフォーマットする
     */
    public function formatValue(): string
    {
        return $this->birthDate->format('Y年m月d日') . '(' . self::WEEK[$this->birthDate->format('w')] . ')';
    }
}

formatValueメソッドをBirthDateオブジェクトから切り離し、ビジネスロジックに関するオブジェクトと表示に関するロジックのオブジェクトをそれぞれ別々のパッケージに分離して距離を広げました。これにより、BirthDateオブジェクトの変動性を下げることができ、認知的負荷の問題点も解消することができました。

まとめ

※ 下記のまとめの文章のベースは生成AIが作成しました。

均衡度は、「結合度が高い=悪い」「分離した=良い」といった単純な判断を避け、統合強度・距離・変動性の3軸で変更の痛みを見立てるための考え方でした。特に重要なのは、強度と距離が不利でも、変動性が低ければ実害は小さい一方で、変動性が高い領域で強度と距離が噛み合っていないと、痛みが頻発して複雑性が顕在化する点です。

本記事の具体例で見たように、均衡度は設計の良し悪しを断定する指標というより、「今、どこにリファクタリングの投資をすべきか」「強度を下げるべきか/距離を変えるべきか」を考えるためのレンズになります。
変更が頻繁に起きる場所(高変動)ほど、強度と距離のバランスを意識して整える――この視点を持つだけでも、設計判断の納得感と再現性が上がるはずです。

補足情報

本文中の用語を補足します。

上流と下流の関係について

モジュールAとモジュールBがあり、モジュールBがモジュールAに依存している場合、モジュールAを「上流」モジュールBを「下流」と呼んでいます。

統合強度について

統合強度には下記に示す4つのレベルがあります。

  • コントラクト結合
  • モデル結合
  • 機能結合
  • 侵入結合

侵入結合

上流モジュールの実装詳細に関するすべての知識が下流モジュールと共有されることを前提としている。
ソフトウェア設計の結合バランス, P178

侵入結合の例を下記に挙げます。

  • リフレクションを使用して、クラスのprivateなプロパティやメソッドを無理やり呼び出す
  • 他のサービスからアクセスされることを意図していないDBから、無理やりデータをread/writeする

下流モジュールが、本来であれば外部に公開されていない上流モジュールの実装詳細に無理やり依存してしまっているのが侵入結合です。

機能結合

密接に関連した機能を実装するモジュール間で起こり、その結果、ビジネスドメインや要件に関する知識がそのモジュール間で共有される。
ソフトウェア設計の結合バランス, P178

機能結合はその度合いによって、シーケンシャル機能結合、トランザクション機能、対象機能結合の3つのタイプに分けられます。

機能結合の度合いは下記の関係があります。

モデル結合

ビジネスドメインのモデルが境界を越えて共有されるときに発生する
ソフトウェア設計の結合バランス, P178

あるサービスのドメインモデルを外部にそのまま公開している場合はモデル結合にあたります。

// そのドメイン固有のオブジェクト
class Order
{
    private OrderId $id;
    private OrderStatus $status;
}

上記のOrderオブジェクトのようなドメイン固有のオブジェクトを、下流側のモジュールがそのまま参照します。その結果、上流側のドメインオブジェクトの変更が下流側にそのまま影響してしまいます。

// 下流側のモジュールは、下記のように上流側のドメインオブジェクトをそのまま使う
$orderId = $order->id()->value;
$status = $order->status()->value;

このように、内部の実装モデルが外部に公開されており、モジュール間の結合が強くなってしまうのがモデル結合です。モデル結合では、ビジネスロジックのコードはモジュール間で共有していません。データのみ共有しています。モデル結合の度合いは、静的コナーセンスで評価できます。

コントラクト結合

上流コンポーネントの知識をできるだけ多くカプセル化し、最小限の知識のみ共有することを目的とした統合コントラクトの公開によって実現される。
ソフトウェア設計の結合バランス, P178

モジュール間で統合のために設計されたモデルを共有してやり取りを行う場合はコントラクト結合に当てはまります。

// そのドメイン固有のオブジェクト
class Order
{
    private OrderId $id;
    private OrderStatus $status;
}

// 統合専用のDTO
class OrderDto
{
    readonly string $id;
    readonly string $status;
}

例えば上記のOrderオブジェクトは、OrderIdやOrderStatusといった、そのドメイン固有のオブジェクトを保持しています。そのようなドメイン固有のオブジェクトを外部にそのまま公開せずに、OrderDtoのような統合のために設計されたオブジェクトに変換して外部に公開するようにします。これにより、内部のドメインモデルを隠すことができ、カプセル化を促進できます。これがコントラクト結合です。
コントラクト結合の度合いも、静的コナーセンスで評価できます。

距離

モジュール間には様々な距離があります。ソフトウェア設計の結合バランスの書籍内で例として挙げられている距離を下記に列挙します。[1]

  • 同じオブジェクトのメソッド
  • 同じ名前空間・パッケージのオブジェクト
  • 異なる名前空間・パッケージのオブジェクト
  • 異なるライブラリ
  • 分散システムのサービス
  • 異なるベンダーによって実装されたシステム

モジュール間の距離が離れれば離れるほど、変更する際のコストは増加します。

変動性

あるモジュールがどれだけ頻繁に変更されるかどうかを表しています。
DDDのドメイン分析を行うことで、モジュールの変動性が見えてきます。

  • コアサブドメイン: 変動性が高い
  • 汎用サブドメイン: 変動性が低い
  • 支援サブドメイン: 変動性が低い

コナーセンスについて

コナーセンスは、2つのモジュールのライフサイクルが強く結びついている状態を指します。例えば、モジュールAとモジュールBがあり、モジュールAを変更したときにモジュールBも同時に変更しなければいけない場合、モジュールAとモジュールBにはコナーセンスがあると言えます。

ソフトウェア設計の結合バランスに書かれているコナーセンスの定義を下記に引用します。

「コナーセントである(コナーセンスがある)」とは、本質的には、一方のモジュールの変更が他方の対応する変更を必要とするか、少なくとも破壊的変更を防ぐために最新の注意を払う必要があることを意味する。さらに言うならば、2つのモジュールの同時変更を引き起こすような合理的な要件の変更が仮定できるならば、それらのモジュールにはコナーセンスがあるということになる。
ソフトウェア設計の結合バランス, P92

コナーセンスには静的コナーセンスと動的コナーセンスの2種類があります。

  • 静的コナーセンス: ソースコード上の関係(コンパイル時の関係)
  • 動的コナーセンス: 実行時の関係(動作中にお互いがどう影響するか)

静的コナーセンスに比べて動的コナーセンスの方が結合度が高いです。

静的コナーセンス

静的コナーセンスには、2つのモジュール間でどのくらいの量の知識を共有するか[2]よって、5つのレベルに分けられます。
静的コナーセンスの共有する知識の大小関係を下記に示します。

名前のコナーセンス

名前のコナーセンスは、同じものを参照するために、接続されたモジュールがその名前に合意しなければならないことを意味する。
ソフトウェア設計の結合バランス, P93

名前のコナーセンスのコード例を示します。

class UserService
{
    public function findUserNameById(int $id): string
    {
        return "taro";
    }
}

class Client
{
    public function handle(): void
    {
        // UserServiceというクラス名が存在していることに依存している
        // そのため、このクラス名を変更したい場合は、
        // 下記のコードも同時に変更する必要がある
        $userService = new UserService();

        // UserServiceにはfindUserNameByIdというメソッド名が存在していることに依存している
        // そのため、このメソッド名を変更したい場合は、
        // 下記のコードも同時に変更する必要がある
        echo $userService->findUserNameById(1);
        
        // $userService変数も名前のコナーセンスの関係がある
        // あたりまえだが、$userService変数の名前を変更する場合は、
        // 上記2行分のコードを同時に変更する必要がある
    }
}

上記コード例のように、変数名やメソッド名、クラス名などのありとあらゆる名前を変更するのに、それらの名前を使っているモジュールも同時に変更する必要がある関係が名前のコナーセンスです。

型のコナーセンス

型のコナーセンスは、2つのコンポーネントが特定の型に使用に合意する必要がある場合に発生する
ソフトウェア設計の結合バランス, P94

名前のコナーセンスの型バージョンです。
下記に例を示します。

class UserService
{
    public function findUserNameById(int $id): string
    {
        return "taro";
    }
}

class Client
{
    public function handle(): void
    {
        $userService = new UserService();
        
        // Clientはここで「intを渡せばよい」という型の前提に依存している
        // そして「返ってくるのはstring」という型の前提にも依存している
        echo $userService->findUserNameById(1);
        
        // 例えば、findUserNameByIdメソッドの引数の型をstringに変更した場合、
        // 上記の処理も下記のようにstringを渡せるように同時に変更する必要がある
        // $userService->findUserNameById('1');
    }
}

意味のコナーセンス

2つのコンポーネントが特定の値に対する意味を共有している場合、両者の間には意味のコナーセンスがある。
ソフトウェア設計の結合バランス, P95

class OrderService
{
  // 出荷できるかどうかを判定しているつもりです
    public function canShip(int $status): bool
    {
        return $status !== 2;
    }
}

上記コード中の「2」は「出荷済み」であることを表しています。しかし、「2」が「出荷済み」を表す、という意味がコード上のどこにも明示されていません。もし「出荷済みは2じゃなくて20に変更」のような改修をしたら、2を使っている箇所を全部探して同時に変更する必要があります。このようなモジュール間の関係が意味のコナーセンスです。

アルゴリズムのコナーセンス

2つのモジュールが、インターフェイスを通じて伝達される値を理解するために特定のアルゴリズムの使用に合意する必要がある場合、それらの間にはアルゴリズムのコナーセンスがある。
ソフトウェア設計の結合バランス, P96

パスワード検証の例を下記に挙げます。

function verifyPassword(string $plain, string $storedHash): bool 
{
    return hash('sha256', $plain ) === $storedHash;
}

function setPassword(string $plain): string 
{
    return hash('sha256', $plain);
}

この場合、「sha256」というアルゴリズムを、verifyPasswordとsetPasswordのメソッド間で共有しています。このようなモジュール間の関係がアルゴリズムのコナーセンスです。

位置のコナーセンス

複数のモジュールが要素の特定の順序に合意する必要がある場合、それらのモジュールの間には位置のコナーセンスがある
ソフトウェア設計の結合バランス, P97

位置のコナーセンスのコード例を下記に挙げます。

function sendEmail(array $data): void
{
    $from = $data[0];
    $to = $data[1];
    $subject = $data[2];
    $body = $data[3];

    // 省略
}

上記メソッドでは、配列内の最初の要素が「差出人」で次の要素が「宛先」のように、配列内の要素の順序を、メソッドとその呼び出し元の間で共有する必要があります。このような関係が位置のコナーセンスです。

動的コナーセンス

動的コナーセンスも静的コナーセンスと同じく、モジュール間の知識の共有量で5つのレベルに分けられます。
動的コナーセンスの共有する知識の大小関係を下記に示します。

実行のコナーセンス

複数のモジュールを特定の順序に従って実行する必要があるときには、それらのモジュールの間には実行のコナーセンスがある。
ソフトウェア設計の結合バランス, P99

実行のコナーセンスのコード例を下記に示します。

$mailer = new Mailer();
$mailer->setFrom('from@example.com');
$mailer->setTo('to@example.com');
$mailer->setBody('hello');
$mailer->send();

sendメソッドが実行される前に、setFrom・setTo・setBodyメソッドの3つのメソッドを実行する必要があります。このように、処理の実行順序が定められている関係が実行のコナーセンスです。

タイミングのコナーセンス

タイミングのコナーセンスでは、2つのモジュールの機能は、特定の順序だけでなく、特定の時間間隔をもって実行する必要がある。
ソフトウェア設計の結合バランス P100

タイミングのコナーセンスのコード例を下記に示します。

// ファイルに書き込む処理を非同期で実行
dispatch(fn() => file_put_contents('/tmp/done', 'ok'));

sleep(1);

if (!file_exists('/tmp/done')) {
    throw new RuntimeException('ファイルの書き込みが完了していません');
}

/tmp/doneファイルに「ok」という文字列を書き込む処理を非同期で実行しています。そして、1秒後にファイルへの書き込みが完了していなかったら例外を投げています。この場合、ファイルへの書き込み処理と sleep(1) で待つ処理が、「1秒で完了するはず」というタイミング前提で結合しています。これがタイミングのコナーセンスです。

値のコナーセンス

アトミックトランザクションのように同時に変更しなければいけない値や、そうしないとシステムが不正な状態になる値がある場合は、それらの間には値のコナーセンスがある。
ソフトウェア設計の結合バランス P102

ここでは、注文をキャンセルする場合を例に挙げます。注文をキャンセルする場合に、注文ステータスと出荷ステータスを同時にキャンセル済みにするというビジネスの仕様があるとします。

enum OrderStatus: string
{
    case CANCELED = '1';
}

enum ShipmentStatus: string
{
    case CANCELED = '1';
}

class Order
{
    private OrderStatus $status;

    private ShipmentStatus $shipmentStatus;

    public function cancel(): void
    {
        $this->status = OrderStatus::CANCELED;
        $this->shipmentStatus = ShipmentStatus::CANCELED;
    }
}

この場合、OrderStatusとShipmentStatusの間に値のコナーセンスの関係があります。

同一性のコナーセンス

同一性のコナーセンスは、2つのオブジェクトが正しく動作するために第3のオブジェクトの全く同じインスタンスを参照する必要がある場合に生じる
ソフトウェア設計の結合バランス P103

同一性のコナーセンスのコード例を下記に示します。

class Counter
{
    private int $value = 0;

    public function add(): void
    {
        $this->value++;
    }

    public function get(): int
    {
        return $this->value;
    }
}

class ModuleA
{
    public function __construct(private Counter $counter) {}

    public function run(): void
    {
        $this->counter->add();
    }
}

class ModuleB
{
    public function __construct(private Counter $counter) {}

    public function run(): void
    {
        $this->counter->add();
    }
}

$sharedCounter = new Counter();
$a = new ModuleA($sharedCounter);
$b = new ModuleB($sharedCounter);

$a->run();
$b->run();

echo $sharedCounter->get(); // 2

ModuleAとModuleBが全く同じsharedCounterインスタンスを参照しており、sharedCounterインスタンスの状態も共有しています。さらに、ModuleAとModuleBが正しく動作するためにCounterクラスの同一インスタンスを共有する必要がある場合、この関係は同一性のコナーセンスです。

脚注
  1. ソフトウェア設計の結合バランス, P185 ↩︎

  2. 2つのモジュール間で共有する知識が多くなるほど、片方の変更がもう片方に影響しやすくなり結合度が高くなります。 ↩︎

TREASURE FACTORY TECH BLOG

Discussion