目次
データとロジックを別のクラスに分けることがわかりにくさを生む
業務アプリケーションのコードの見通しが悪くなる原因
業務アプリケーションでは、業務ロジックを整理するために、画面インターフェース/業務ロジック/データベース入出力の3つの関心事を分離するための 三層アーキテクチャ が一般的です。
データクラスと機能クラスを分ける手続き型の設計では、業務ロジックが入り組んでくると、次の問題が顕著になります。
- 同じ業務ロジックがあちこちに重複して書かれる
- どこに業務ロジックが書いてあるか見通しが悪くなる
データクラスを使うと同じロジックがあちこちに重複する
プレゼンテーション層/アプリケーション層/データソース層は、同じデータクラスを参照できると、同じロジックが異なるクラスに重複して記述されがちです。
三層のどこにでもロジックが書け、しかもあちこちに業務ロジックが重複すると、変更が大変になります。
ロジックを持たないデータクラスの例
class DataWithoutLogic {
String value;
String getValue() {
return value;
}
void setValue(String value) {
this.value = value;
}
}
このデータクラスは、オブジェクト指向のクラスの使い方としては、間違っています。
このクラスは データを格納しているだけです。データを使った判断/加工/計算のロジックを持っていません。 このようなロジックを持たないクラスは、本来のクラスではありません。
コードの重複が起きる理由
ロジックを持たないデータクラスを使ってデータの受け渡しをする
↓
コードの重複が起きる
↓
データクラスを参照できる場所であれば、どのクラスにでも、ロジックを書けてしまう
データクラスを使うと業務ロジックの見通しが悪くなる
データクラスを使ってしまうと、アプリケーション層に業務ロジックを集めても、どこに何が書いてあるかの見通しが悪くなりがちです。
原因には2つのパターンがあります。
- アプリケーション層の構造が画面の構造に引きずられる
- アプリケーション層の構造がデータベースの都合に影響される
画面と機能クラスを1対1で関連づけると、複数の機能クラスに同じ業務ロジックが重複しやすくなります。
また、変更が適切に行われていることを確認するには、広い範囲のテストが必要です。
★共通機能ライブラリが失敗する理由
共通ライブラリクラスとして用意する方法(UtilクラスとかCommonクラス)があります。
しかし、共通ライブラリ方式では、業務ロジックの共通化をそれほどうまくいきません。
うまくいかないパターンは2つあります。
汎用的な共通関数
- 汎用的に使えるよう関数の引数にフラグやオプション引数が増える
- 引数が増えれば増えるほど、メソッドを使う側は、自分には関係のない引数まで理解することが必要
- 適切な使い方をするのに労力が必要になる
用途ごとに細分化した共通関数
- 用途別に細かく分けると共通ライブラリのメソッド数が膨れ上がる
- 似たようなメソッドの中から、自分のニーズにぴったりあったメソッドを探す必要がある
- 微妙な違いを理解してメソッドを使い分けるのが大変
業務ロジックをわかりやすく整理する基本のアプローチ
基本方針は次の2つです。
- データとロジックを一体にして業務ロジックを整理する
- 三層のそれぞれの関心事と業務ロジックの分離を徹底する
データとロジックを一体にして業務ロジックを整理する
業務ロジックを重複させないためにはどう設計すればよいか
オブジェクト指向では、データとロジックを1つのクラスにまとめます。
大切なことは、使う側のクラスのコードがシンプルになるように設計することです。
メソッドをロジックの置き場所にする
データを保持するだけのクラス
class Person {
private String firstName;
private String lastName;
...
String getFirstName() {
return firstName; // インスタンス変数をそのまま返す
}
String getLastName() {
return lastName; // インスタンス変数をそのまま返す
}
}
この例のgetFirstName()メソッドとgetLastName()メソッドはインスタンス変数を返すだけです。何も役に立つことをしていません。
メソッドにロジックを持たせる
class PersonName {
private String firstName;
private String lastName;
String fullName() {
return String.format("%s %s", firstName, lastName);
}
}
業務ロジックをデータを持つクラスに移動する
データを返すだけのgetterメソッドを見つけたら、そのメソッドに何らかの判断/加工/計算をさせることを考えます。
データを持つクラスに判断/加工/計算をまかせることができるため、利用する側のクラスのコードはシンプルになり読みやすくなります。
使う側のクラスに業務ロジックを書き始めたら設計を見直す
データを持つクラスに、そのデータを使った判断/加工/計算のロジックを書くのがオブジェクト指向のクラス設計の基本です。
そのことを前提に、問題はとりあえず動くようになったあと です。
データとロジックを別のクラスに書いてあった場合、
- 動いたから良しとしてそのまま放置するのか
- データを持つ側にロジックを移動する設計改善を続けるか
この違いがソフトウェアの変更容易性を左右します。
オブジェクト指向の設計は、改善の繰り返しです。
メソッドを短く書くとロジックの移動がやりやすくなる
長いメソッドを短いメソッドに分けると、本来ならばそのクラスにふさわしくないコードのかたまりを発見しやすくなります。
メソッドは必ずインスタンス変数を使う
渡された引数だけを使い、インスタンス変数を使わないメソッド
BigDecimal total(BigDecimal unitPrice, BigDecimal quantity) {
BigDecimal total = unitPrice.multiply(quantity);
return total.setScale(0, ROUND_HALF_UP);
}
インスタンス変数を使わないメソッドは、どこに何が書いてあるかをわかりにくくします。
クラスが肥大化したら小さく分ける
インスタンス変数が多いクラスに関連する業務ロジックを集めると、そのクラスがしだいに大きくなります。
同じインスタンス変数を使うメソッドを1つのグループとしてまとめます。
メソッドがすべてのインスタンス変数を使っていない
class Customer {
String firstName;
String lastName;
String postalCode;
String city;
String address;
String telephone;
String mailAddress;
boolean telephoneNotPreferred;
String fullName() {
return String.format("%s %s", firstName, lastName);
}
...
}
メソッドがすべてのインスタンス変数を使っている
class PersonName {
private String firstName;
private String lastName;
String fullName() {
return String.format("%s %s", firstName, lastName);
}
}
fullName()メソッドは、そのクラスのすべてのインスタンス変数を使うようになります。
関連性の強いデータとロジックだけを集めたクラスを 凝集度が高い と言います。
凝集とは「切っても切れない」関係です。
パッケージを使ってクラスを整理する
クラスの数が増えてきたときの整理の手段がパッケージです。
- 関連性の強いクラスは同じパッケージに集める
- クラスやメソッドのスコープ(参照範囲)は、可能な限りパッケージスコープにする
- パッケージのクラス数が増えたら、さらにサブパッケージに分ける
- また、クラス数が少なくても、長いパッケージ名をつけたくなったら、パッケージを階層にして、一つひとつのパッケージ名を短くすることを検討する
- そのために階層が深くなったり、1つのパッケージに1つか2つのクラスになってしまうこともある、それでもよい
- 名前を手がかりに、どこに何が書いてあるかを推測するときに、パッケージ名が単純で、1つのパッケージのクラスが少ないほうが推測しやすい
- パッケージの設計も継続的に改善する
三層の関心事と業務ロジックの分離を徹底する
業務ロジックを小さなオブジェクトに分けて記述する
関連する業務データと業務ロジックを1つにまとめたこのようなオブジェクトをドメインオブジェクトと呼びます。
「ドメイン」とは、対象領域とか問題領域という意味です。
ドメインオブジェクトは、業務で扱うデータをインスタンス変数として持ち、その業務データを使った判断/加工/計算の業務ロジックを持つオブジェクトです。
データとその判断/加工/計算のロジックは、比較的、小さな単位に分けて整理します。
注文であれば、「商品」「数量」「金額」「納期」「届け先」「請求先」という単位に分けながらドメインオブジェクトを作っていきます。そしてそういう小さなドメインオブジェクトを組み合わせて「注文」オブジェクトを組み立てます。
ドメインモデル
業務アプリケーションの対象領域(ドメイン)をオブジェクトのモデルとして整理したものをドメインモデルと呼びます。
- 業務で扱うデータと関連する業務ロジックを集めて整理したもの
- ドメインモデルを見れば、業務全体がどういう関心事から成り立っているかを理解できる
三層+ドメインモデルで関心事をわかりやすく分離する
三層+ドメインモデルの構造では、業務ロジックを記述するのはドメインモデルだけ です。業務的な判断/加工/計算のロジックは、すべて、ドメインモデルを構成するドメインオブジェクトに任せます。
勉強したことをどう活かすか?
- ロジックを持たないデータクラスを使わない
- 共通機能ライブラリがそれほどうまくいかないことを理解する
(汎用的な共通関数、用途ごとに細分化した共通関数) - メソッドはロジックの置き場所にする(データを保持するだけのクラスはダメ)
- 業務ロジックをデータを持つクラスに移動する
- データを返すだけのgetterメソッドを見つけたら、そのメソッドに何らかの判断/加工/計算をさせることを考える
- とりあえず動くようになったあとは、データを持つ側にロジックを移動する設計改善をする
- 業務ロジックをデータを持つクラスに移動する
- メソッドを短く書く
- ロジックの移動がやりやすくなるため
- メソッドは必ずインスタンス変数を使う
- クラスはメソッドがすべてのインスタンス変数を使うようにする(凝集度を高くする)
- パッケージを使ってクラスを整理する
- パッケージの設計も継続的に改善する