7
5

More than 3 years have passed since last update.

値オブジェクトを作ってimmutableな実装を心がける

Last updated at Posted at 2021-06-23

親記事 : https://qiita.com/Regpon/items/1116679adadd8fb76f3f

値オブジェクト

値を扱うための専用の小さなクラスを立てる

値オブジェクト 内容
Quantity 数量
Price 料金
Days 日数
DateOfRecord 記録日
Telephone 電話番号

などのように業務アプリケーションで使う関心ごとを扱うオブジェクトをクラス化して、そのオブジェクトの振る舞いを集約させる。
そして重要なのがSetterを実装しないことだ。

値を代入できるのはあくまでコンストラクタのみ。

値を更新するということはつまり、別の意味を持つオブジェクトに生まれ変わっているため、新たなインスタンス変数として定義すべきだからである。

以下のような注文に応じて注文の合計金額を返すメソッドについて考えたとすると、

// 注文に応じて合計金額を取得する
int getTotalPrice(Orders orders) {
  // 注文金額を取得する
  int price = orders.getAmount();
  // 割引
  price = price - getDiscount(orders);
  // 送料
  price = price + getShippingFee(orders);

  // 消費税を加算して合計金額を返す
  return (int) ((float)price * (1.1f));
}

ここでの問題点は、

  • ローカル変数を使い回しているので思わぬ副作用が起きる可能性がある(今後実装が増えていったときに、処理が増えて、priceがどう変わっていくのか追いにくくなる)
  • intの仕様上ありえない金額になる可能性がある(例えばマイナスの金額)
  • 業務仕様上、注文金額の上限値がある場合の振る舞いも実装する必要がある
  • 上記の考慮をこのメソッドの中で実装していくと複雑な処理になって一目でわからなくなってしまう
  • 他で金額を扱っているところも同様にこのような仕様の考慮をしなくてはいけなくなる

このような問題点を解決していくために実装を以下のように変更する

// 注文に応じて合計金額を取得する
Price getTotalPrice(Orders orders) {
  // 注文金額を取得する
  Price basePrice = new Price(orders.getAmount());
  // 割引
  Price discounted = basePrice.minus(getDiscount(orders));
  // 送料
  Price totalPrice = discounted.plus(getShippingFee(orders));

  // 合計金額
  return totalPrice.addTax();
}

// 値段を扱う値クラス
class Price {
  static final int MIN = 1;
  static final int MAX = 999999;
  static final float TAX = 0.1f;
  int price;

  // 完全コンストラクタ
  Price(int price) {
    isCorrectPrice(price);
    this.price = price;
  }

  // 引き算して新しいオブジェクトを返す
  Price minus(int discount) {
    int discounted = this.price - discount;
    isCorrectPrice(discounted);
    return new Price(discounted);
  }

  // 足し算して新しいオブジェクトを返す
  Price plus(int additionalPrice) {
    int added = this.price + additionalPrice;
    isCorrectPrice(added);
    return new Price(added);
  }

  // 消費税を加えて新しいオブジェクトを返す
  Price addTax() {
    return new Price((int) ((float)this.price * (1.0f + TAX)));
  }

  // 値段オブジェクトの上限下限制約に反していないかチェックする
  void isCorrectPrice(int price) {
    if (price < MIN || price > MAX)
      throw new IllegalArgumentException(String.format("値段は%d円以上、%s円以下", MIN, MAX));
  }

}

こうすることで、このアプリケーション内で扱う値段という業務的関心ごとの仕様がひとまとまりになり、使用する側は値段の仕様を意識する必要がなくなる。

また、メソッド内のインスタンス変数の値を変更するときは、別のオブジェクトとすることで一つ一つのオブジェクトの用途が限定され副作用を防ぐことができる。(immutableな実装)

このように、業務的関心ごととなる値をオブジェクトにしたものを値オブジェクトといい、このアプリケーション領域(ドメイン)の業務的関心ごとのオブジェクトを、ドメインオブジェクトという。

DDD(ドメイン駆動設計)はこのドメインオブジェクトを中心に設計がなされていく。

配列やコレクションはコレクションオブジェクトとして複雑さを閉じ込めておく

値オブジェクトと同様に配列やコレクションも小さなクラスとして、振る舞いを管理する。
配列やコレクションは値以上に複雑なものになりがち。(foreach、要素数の変化、要素の変化、空など・・・)

生徒のリストを扱うコレクションオブジェクトを定義すると、例として以下のようになる。

class Students {
  List<Student> students;

  Students(List<Student> students) {
    this.students = students;
  }

  List<Student> add(Student student) {
    List<Student> result = new ArrayList<>(students);
    result.add(student);
    return new Students(result);
  }

  List<Student> asList() {
    return Collections.unmodifiableList(students);
  }
}

注目すべきは、

  • 要素のオブジェクトとしては値オブジェクトを使用すること
  • リストとして扱いたいことがあれば、フィールド変数をそのまま返さず Collections.unmodifiableList を利用して変更不可のListにして返す

この二つを守ることで、副作用を防ぐことができる。値オブジェクトを使わずに二点目だけの制約では、要素の中身のオブジェクトを変更できてしまうためだ。

List<Student> list = students.asList();

Student student = list.find(0);

// 値オブジェクト(不変なオブジェクト)でないと、このように書き換えられてしまう
student.setName("太郎");

まとめ

  • 値を扱うときは値オブジェクトを作る
  • コレクションを扱う場合はコレクションオブジェクトを作る
  • 上記のオブジェクトを作って、値やコレクションの振る舞いを閉じ込めて使用側にはその振る舞いを意識させない(疎結合にする)
  • オブジェクトに変更を加えたい場合は、新しいオブジェクトを生成して、意味合いごとに分ける

文献

本記事は 現場で役立つシステム設計の原則 変更を楽で安全にするオブジェクト指向の実践技法  著:増田 亨 を読んで(なるべく)自分の言葉でまとめたものです。

興味を持っていただけたらこちらの本を読んでみていただけたらと思います。(勝手に宣伝)

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5