👛

インスタンスと変数を不変にする

2023/11/26に公開

レビュア陣が豪華な「良いコード/悪いコードで学ぶ設計入門」での学びがとても良いので、TypeScriptで試した内容を記録として残していきます。

まず「不変」。

変数とクラスも全て不変にすること。
実際によく聞く話ではありますけれども、実際にどういったコードで不変にするのか。これを明確にコードで表現しています。分かりやすい。

TypeScriptで、四則演算するクラスを試しに書いてみました。
お勉強用なので、あとで何を学習したときのクラスだったか分かるようにImmutableClassとしています。

methodの引数をImmutableClassにすることで、値の受け渡しの不正を減らせます。また、各methodでvalidationも実装するとなおよしです。値オブジェクトはこんな感じで実装していくのが望ましいんじゃないかしら。ふと。

// 四則演算を行うクラス

export class ImmutableClass {
  private readonly calculated: number;

  // constructor で受け取った値のバリデーションを行う
  // 不変にしておくことで、必ず正しい値が入ることを保証できる
  constructor(value: number) {
    if (value < 0 || value > 100000000) {
      throw new Error("value is invalid");
    }
    this.calculated = value;
  }

  // 各メソッドでもバリデーションを行う動作を入れられる
  add(addend: ImmutableClass): ImmutableClass {
    return new ImmutableClass(this.calculated + addend.calculated);
  }

  subtract(subtrahend: ImmutableClass): ImmutableClass {
    return new ImmutableClass(this.calculated - subtrahend.calculated);
  }

  multiply(multiplier: ImmutableClass): ImmutableClass {
    return new ImmutableClass(this.calculated * multiplier.calculated);
  }

  divide(divisor: ImmutableClass): ImmutableClass {
    return new ImmutableClass(this.calculated / divisor.calculated);
  }

  getCalculated(): number {
    return this.calculated;
  }
}

ついでにGitHub Copilotにはテストコードも準備してもらいました。ホントマジでCopilotは優秀です。コードを書く時間と考える時間が最小化されます。

テストコードはJest用です。

// testImmutableClass is a test function for ImmutableClass with jest.
import { ImmutableClass } from "../src/immutableClass";

describe("normal testImmutableClass", () => {
  test("add 100 + 20 = 120", () => {
    const immutableClass = new ImmutableClass(100);
    const added = immutableClass.add(new ImmutableClass(20));
    expect(added.getCalculated()).toBe(120);
  });

  test("subtract 100 - 20 = 80", () => {
    const immutableClass = new ImmutableClass(100);
    const subtracted = immutableClass.subtract(new ImmutableClass(20));
    expect(subtracted.getCalculated()).toBe(80);
  });

  test("multiply 100 * 20 = 2000", () => {
    const immutableClass = new ImmutableClass(100);
    const multiplied = immutableClass.multiply(new ImmutableClass(20));
    expect(multiplied.getCalculated()).toBe(2000);
  });

  test("divide 100 / 20 = 5", () => {
    const immutableClass = new ImmutableClass(100);
    const divided = immutableClass.divide(new ImmutableClass(20));
    expect(divided.getCalculated()).toBe(5);
  });
});

describe("exception testImmutableClass", () => {
  test("add 100 + 100000001 = Error", () => {
    const immutableClass = new ImmutableClass(100);
    expect(() => {
      immutableClass.add(new ImmutableClass(100000001));
    }).toThrowError("value is invalid");
  });

  test("subtract 100 - 100000001 = Error", () => {
    const immutableClass = new ImmutableClass(100);
    expect(() => {
      immutableClass.subtract(new ImmutableClass(100000001));
    }).toThrowError("value is invalid");
  });

  test("multiply 100 * 100000001 = Error", () => {
    const immutableClass = new ImmutableClass(100);
    expect(() => {
      immutableClass.multiply(new ImmutableClass(100000001));
    }).toThrowError("value is invalid");
  });

  test("divide 100 / 100000001 = Error", () => {
    const immutableClass = new ImmutableClass(100);
    expect(() => {
      immutableClass.divide(new ImmutableClass(100000001));
    }).toThrowError("value is invalid");
  });
});

Discussion