🐈

読書メモ『良いコード/悪いコードで学ぶ設計入門』 3~4章

2024/03/31に公開

書籍について

https://gihyo.jp/book/2022/978-4-297-12783-1

クラス設計のポイント

大事なことはクラス単体で正常に動作するように設計すること

良いクラスの構成要素

  1. インスタンス変数
  2. インスタンス変数を不正状態から防御し、正常に操作するメソッド

データクラスについて

class Currency
{
    string $value;
}

インスタンス変数のみを持つクラスのこと
データクラスはインスタンス変数を操作するロジックが別のクラスに実装されるため、低凝集となる

良いクラスの設計術

1. コンストラクタで確実に正常値を設定する

コンストラクタで確実に正常値を設定することで、クラスを使用する時に不正値のことを考慮する必要がなくなる

メソッドの先頭で値のバリデーションを定義する方法をガード節と言う

public function __construct(
    public readonly int $amount,
    public readonly Currency $currency,
) {
    if ($amount < 0) {
        throw new InvalidArgumentException("金額には0以上を指定してください。");
    }
}

また、インスタンス変数をすべて初期化できるだけの引数を持ったコンストラクタとそのコンストラクタ内でガード節を用いて不正値を弾く設計パターンを完全コンストラクタと呼ぶ

2. 状態を不変にする

インスタンス変数やメソッドの引数を不変にすることで思わぬ副作用が発生しないクラスとなる
状態を変更したい時には、新たな状態を持つインスタンスを生成する

public function add(Money $other): self
{
    if ($this->currency != $other->currency) {
        throw new InvalidArgumentException("通貨単位が違います。");
    }

    $added = $this->amount + $other->amount;

    return new self($added, $this->currency);
}

3. 「値の渡し間違い」を型で防止する

プリミティブ型のみを使用しているとプリミティブ型では対応しきれない不正値を間違って渡すことによる不正値の混入を防ぐことが出来ないので、型によってそれを防止する

引数の型をint型ではなくMoney型とすることで、不正値の混入を防ぐことができる

public function add(Money $other): self
{
    if ($this->currency != $other->currency) {
        throw new InvalidArgumentException("通貨単位が違います。");
    }

    $added = $this->amount + $other->amount;

    return new self($added, $this->currency);
}

このような値をクラス(型)として表現する設計パターンを値オブジェクトと呼ぶ

ここまでのサンプルコード
// データクラスとなっているCurrencyクラス
class Currency
{
    string $value;
}

// 完全コンストラクタと値オブジェクトパターンを使用したMoneyクラス
class Money
{
    // インスタンス変数が不変
    public function __construct(
        public readonly int $amount,
        public readonly Currency $currency,
    ) {
        // ガード節
        if ($amount < 0) {
            throw new InvalidArgumentException("金額には0以上を指定してください。");
        }
    }

    // 型による不正値の防止
    public function add(Money $other): self
    {
        if ($this->currency != $other->currency) {
            throw new InvalidArgumentException("通貨単位が違います。");
        }

        $added = $this->amount + $other->amount;

        // 状態を変更する時は新しいインスタンスを生成
        return new self($added, $this->currency);
    }
}

4. 関数の影響範囲を限定する

以下の3つを満たすように関数を設計することで影響範囲を限定して副作用のない関数となる

  1. データ、つまり状態を引数で受け取る
  2. 状態を変更しない
  3. 値は関数の戻り値として返す

5. 正しく状態変更するメソッドを設計する

インスタンス変数やメソッドの引数などの状態はデフォルトでは不変にする
しかし、状態を変更しなければいけない場合には正しく状態を変更する

ガード節で不正値を防ぐだけでなく、状態を変更する際にも不正値を防ぐ

class Hitpoint
{
    private const MIN = 0;

    public function __construct(
        public readonly int $amount,
    ) {
        if ($amount < self::MIN) {
            throw new InvalidArgumentException();
        }
    }

    public function damage(int $damageAmount): void
    {
        $nextAmount = $this->amount - $damageAmount;
        
        $this->amount = max(self::MIN, $nextAmount);
    }
}

状態変更を発生させるメソッドをミューテーターと呼ぶ

Discussion