データクラスは作らない(ハガネの意志)


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

データクラスを作ると見通しが悪くなる

データクラスとはgetter setterだけを持つクラスで、データの入れ物として作られたクラス。
これを作るとアプリケーションの見通しが悪くなってしまう。

見通しが悪いというのはどういうことかというと、

  • 同じ業務ロジックがあちこちに書かれている
  • どこに業務ロジックが書かれているのかわからない

といった現象である。

業務アプリケーションは以下の三つに分かれた三層アーキテクチャと呼ばれる構造で作られていることが一般的。

名前 説明
プレゼンテーション層 画面や外部接続インターフェース
アプリケーション層 業務ロジック、ルール
データソース層 データベースなどのデータソースとの入出力

それぞれの層でデータクラスを使って実装するとそのデータに関するロジックをどの層のクラスにも書けてしまう。また、それらは重複されてしまう。
(getter,setterを記述するのが面倒なのでlombokのアノテーションを頼ってしまいますことをお許しください。)

class Member {
  @Getter @Setter
  String firstName;

  @Getter @Setter
  String lastName;

  @Getter @Setter
  Age age;

  Member(String firstName, String lastName, Age age) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }
}

このようなクラスがあった場合に利用する側で姓名合わせてフルネームとして扱う場合に、

void hoge() {
  String fullName = member.getFirstName() + member.lastName();
  ...
}

void fuga() {
  Member member = new Member("Jon", "Demelar", age);
  String fullName = member.getFirstName() + member.lastName();
  ...
}

void hogefuga() {
  String fullName = member.getFirstName() + " " + member.lastName();
  ...
}

このように、フルネームとは何かという振る舞いを自由に記述できてしまい、後々フルネームとはこういうものであるという仕様を訂正したときに一体どこを見ればいいのか、どこで使われているのか探すのに苦労し、不安定で危うい実装になっている。

こういったことを防ぐためにデータとロジックを一体にして、業務ロジックを整理することでそのデータの利用や仕様の変更を楽にすることを心がけます。

class Member {
  String firstName;
  String lastName;
  Age age;

  Member(String firstName, String lastName, Age age) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }

  // フルネームを返すロジック
  String fullName() {
    return String.format("%s %s", member.getFirstName(), member.lastName());
  }
}

void hoge() {
  String fullName = member.fullName();
  ...
}

void fuga() {
  Member member = new Member("Jon", "Demelar", age);
  String fullName =  member.fullName();
  ...
}

void hogefuga() {
  String fullName =  member.fullName();
  ...
}

このようにすることで、フルネームとは「 firstName と lastname を半角スペースを開けて文字列結合したものである」という仕様が明らかになり、変更する場合はMemberクラスだけに集中すれば良いということになり、逆に使用する側はどのようなロジックの上でフルネームが返ってくるかは考慮する必要がない状態になる。

最初から完璧にはできない

こういった整理は、実装前から完全に行うことはなかなかできないので、実装を進めていって、とりあえず動くものができたらリファクタリングをして整理していくといい。

その時の手順としては

  • ロジックが生まれたらメソッドとして切り出す
  • ロジックをデータを持つクラスに移動する
  • データを使う側のクラスにロジックを描き始めたら設計を見直す
  • メソッドを短くしてロジックを移植しやすくする
  • メソッド内では必ずインスタンス変数を使う
  • クラスが肥大化したら小さく分ける
  • クラスが増えてきたらパッケージを使って整理する
  • Utilクラス、Commonクラスは作らない(業務データとロジックを切り離すと、いうほど共通化できない)

メソッド内では必ずインスタンス変数を使う

大体はイメージがつくと思うが、 メソッド内では必ずインスタンス変数を使う については、フルネームを返すロジックを例に挙げるなら、

  // フルネームを返すロジック
  String fullName(String firstName, String lastName) {
    return String.format("%s %s", firstName, lastName);
  }

のようなメソッドである。インスタンス変数を使わなメソッドの欠点は以下の通り。

  • どこに書いてあるか分かりにくい
  • そのクラスに書いてある理由がわかりにくい
  • 引数の順番を間違える可能性がある(firstNameってどっちだっけ?)

なので、このようなメソッドを見かけたらリファクタリングの対象とするといいだろう。

クラスが肥大化したら小さく分ける

また、クラスが肥大化した場合については

class Member {
  String firstName;
  String lastName;

  String postalCode;
  String address;

  Email email;
  Telephone telephone;
  boolean isMobilePhone;

  Age age;

  // フルネームを返すロジック
  String fullName() {
    return String.format("%s %s", member.getFirstName(), member.lastName());
  }
...
}

このようにインスタンス変数が増えてくると、それを扱うロジックも増えてきて、次第にクラスが肥大化していく。そして変更を加えたときに副作用の影響範囲が広がってしまう。
そうなってきたら、関連性の強いデータとロジックを抜き出して新しいクラスを作れないか検討する。

インスタンス変数とメソッドの関係を見ると、切り出す範囲が見えてくる。

例えばフルネームを返すメソッドで利用しているのが firstName と lastname というインスタンス変数なのでそれを切り出して、MemberNameというクラスにしてみる。

そのほかにも関連性の強いインスタンス変数同士やメソッドを切り出して小さくすると、

class Member {
  MemberName memberName;
  Address address;
  MemberContact memberContact;
  Age age;
  }
...
}

class MemberName {
  String firstName;
  String lastName;

  // フルネームを返すロジック
  String fullName() {
    return String.format("%s %s", member.getFirstName(), member.lastName());
}
...

のようにまとめることができそう。といった具合で、クラスを小さくしていくことでより独立性が高まり、再利用性も上がっていく。

このような状態を 凝集度が高い と表現される。
凝集したクラスは意図が明確で扱いやすくなる。

クラスが増えてきたらパッケージを使って整理する

当然小さく分けるとクラスがどんどん増えていく。それでいい。
ただ、どこにどのクラスがあるのか見つけにくくなる。そのときに整理するために使うのがパッケージ。

関連性の強いクラスは同じパッケージに集め、クラスやメソッドのスコープは可能な限りパッケージスコープにする。
つまり public宣言をしない
public なクラスやメソッドが少ないほど影響範囲をパッケージ内に閉じ込めやすくなる。

また、パッケージの作り方として、

  • パッケージ内のクラスが増えてきたらサブパッケージ化する
  • クラスが少なくても長い名前のパッケージ名をつけたくなったら、パッケージを階層にして一つ一つのパッケージ名を短くする
  • 1つのパッケージ内に1つ、2つしかクラスがなくても全然いい

三層の関心事と業務ロジックの分離を徹底する

これまで扱ってきた通り、業務アプリケーションの中核は、業務データを使った

  • 判断(学生かどうか?など)
  • 加工(フルネームなど)
  • 計算(注文合計金額)

の業務ロジックである。
これらの業務データ、業務ロジックをひとまとめにしたようなオブジェクトを ドメインオブジェクト と呼ぶ。
そして、ドメインオブジェクトを整理して、パッケージにまとめ、独立性を高めていったものを ドメインモデル と呼ぶ。

して、冒頭にでた三層アーキテクチャの説明をアップデートするとこのようになる。

名前 説明
プレゼンテーション層 画面や外部接続インターフェース
アプリケーション層 業務機能のマクロな手順の記述
データソース層 データベースなどのデータソースとの入出力
ドメインモデル 業務データ、業務ロジックをひとまとめにし、整理して、パッケージにまとめ、独立性を高めていったものの集合

三層+ドメインモデルの構造では、業務ロジックを記述するのはドメインモデルだけになる。

ドメインモデルは画面やデータベースのテーブル設計などの都合からは切り離され、純粋に業務の関心ごとだけを扱う。
こうすることによって記述がシンプルになる、役割が明確される。

まとめ

  • データとロジックを分けると複雑化する
  • データとロジックを同じクラスにまとめる
  • ドメインモデルに業務ロジックをまとめる
  • パッケージにまとめて、スコープを最小限にする
  • ドメインモデルは画面やデータベースの都合から独立させる
  • 他の層は業務的な判断/加工/計算のロジックをドメインモデルに任せることでシンプルになる

文献

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

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