Skip to content

設計ガイドライン

masuda220 edited this page Mar 14, 2019 · 32 revisions

ビジネスロジック(ビジネスルール)を記述するためのクラス設計のガイドラインです。

ビジネスルールに基づく計算ロジックと判定ロジックをプログラミング言語で記述したものがビジネスロジックです。

このガイドラインは、ビジネスロジックを型指向でプログラミングするための考え方とやり方を説明します。

型による設計

型によるモジュール化

  • 型とは値の種類である
    • ビジネスロジックの計算・判定の対象になる「値の種類」を特定する
    • 値の種類ごとに、データとロジックを一つのモジュール(クラス)にまとめる
    • 値の種類ごとにロジックを一つのモジュールにまとめることで、同じロジックが複数のモジュールに重複することを防ぐ
  • 型は、次の3つで定義する
    • 値の種類の「名前」
    • 値に対して有効な計算
    • 値の有効な範囲
  • クラスを使って、型を記述する
    • クラス名で値の種類を表現する
    • メソッドで、値に対して有効な計算を表現する
    • 値のMAX, MINがある場合、定数として公開する
  • クラスのインスタンス(オブジェクト)は、「一つの値」を表現する
    • an object is a value
    • an object represents whole value

型の候補

  • ビジネスルールの計算式や判定式に登場する値の種類が型の候補になる
    • 例:単価 × 数量 = 金額
    • 例:期限日、日数、期間
    • 例:区分値(条件分岐に使ったり、判定式の結果の表現に使う値の列挙)
    • 例:上記の集合(一覧、グループ、履歴、...)
  • 型の候補には2種類ある
    • 計算に使う値(単価、数量)
    • 計算結果(金額)
  • 実際に計算ロジックを書きながら、それぞれの型の必要性/有用性を検討する

契約による設計として型を使う

  • バートランド・メイヤーが提唱した「契約による設計」スタイルを採用する。
    • 約束事を明示して、それを守ることを前提に、防御的なコードは書かない
    • 約束違反の可能性を想定した「防御的プログラミング」スタイルとは真逆のアプローチ
  • 契約その1:nullの扱い
    • null を渡さない → 引数を受け取った側で、引数のnull検査は不要
    • null を返さない → 返り値を受け取った側で、返り値のnull検査は不要
    • インスタンス変数がnullであっても良いが、外部には見せない(内部に隠蔽する)
  • 契約その2:可能な限り immutable にする
    • 生成したオブジェクトの状態は変えない
    • 状態を変えるのではなく、新しい値を持った別のオブジェクトを生成する
    • setterを書かないということ
  • 契約その3:事前条件は、引数の型で表明する
    • クラスを使う側が渡す引数に、値の有効範囲などの条件がある場合、その範囲を持つ型で制限する
  • 契約その4:事後条件は、メソッドの返す型で表明する
    • メソッドの返す値の種類や範囲を、メソッドの返す型で表明する

補足

事前条件・事後条件の表明を、assert文ではなく、型で記述するということ

ドメインに閉じた型とドメインに閉じていない型

「ドメインに閉じた型」と「ドメインに閉じていない型」を区別して考える

ドメインに閉じた型

  • メソッドの返す値の型、引数として渡す値の型が、ドメインに特化した独自の型だけを使っている
  • ビジネスの関心事だけを表現できている(ドメインに閉じている)
  • ビジネスルールを表現する型として望ましい設計

ドメインに閉じていない型

  • メソッドの返す値や引数で渡す値の型に、Java言語のプリミティブ(int, String, booleanなど)やその配列が混在する型である
  • ビジネスの関心事とプログラミングの関心事とが混ざっている型である
  • プリミティブは、可能な値の範囲が広すぎたり、可能な演算が汎用的すぎて、ビジネスルールの表現として不適切/不完全なことが多い
  • 初期の設計ではドメインに閉じていない型が多くなるが、ドメインに閉じる方向で改良できないか検討する
  • ドメインに閉じていないプリミティブを使うメソッドも、可能な限り、public にせず公開範囲を制限する

自分の型に閉じる

  • メソッドの返す値と引数に渡す値の型を、以下に限定すると「自分の型に閉じる」ことになる
    • 自分自身の型
    • その配列
  • 自分の型に閉じる設計は、その型を理解するために必要な知識が少なくてすむため、わかりやすく扱いやすい設計になる
    • 例:BigDecimal の算術演算メソッドは、BigDecimalに閉じている
  • プリミティブは、理解する負担が軽いので、自分の型に閉じるのとほぼ同等のわかりやすさを実現できる。
    • int, String, booleanなどプリミティブな型
    • プリミティブな型の配列
  • ただし、プリミティブは、ビジネスルールの表現として不適切/不完全なことが多いので、可能であれば使わない

引数に渡す値の型

  • 「ドメインに閉じた型」では、プリミティブを渡さない
  • 「ドメインに閉じていない型」では、必要に応じてプリミティブを渡すことがある
    • プリミティブの値の範囲は、ビジネスの関心事とは一致しないことを理解した上で使うこと

メソッドが返す値の型

  • 「ドメインに閉じた型」では、プリミティブ型を返さない
  • 「ドメインに閉じない型」も、できるだけプリミティブ型を返さないことを検討する
    • クライアント側(その型を使う側)で、返ってきたプリミティブ型を使ったロジックを記述しなくてすむようにする
    • プリミティブな型を返す場合、値の範囲が、ビジネスの意味的に問題がないか、慎重に検討する

boolean型

  • できるだけ引数の型としても、メソッドの返す型としても使わない
  • 列挙型を使う
    • boolean型のtrue/falseは、汎用的な意味で、ドメインに特化した概念を表現していない
    • open/close, start/stop のように、より、ビジネスロジックの意味を明示する列挙型を提供する

String型

  • 「ドメインに閉じた型」でも、値の文字列表現を返すメソッドは、String型を使う
  • クライアント側(型を使う側)で、返ってきた文字列の判定ロジックや加工ロジックが必要になる場合、以下を検討する
    • String型を返す側で、その値の判定や加工ロジックを持つ別のメソッドを用意する
    • String型を返す代わりに、判定や加工ロジックを持つ、別の型を返す

クラスの継承 ( extends )

  • 使わない(コンポジションを考える)

型の継承 ( implements )

  • 型のファミリーの階層が、意味がある場合に使う
  • 概念的にファミリーであるという単なるマーカーとしては使わない

パッケージ

  • ビジネスルール(の扱う値の種類)のグルーピングのために作成する
  • 名前が2単語になる場合は、原則、サブパッケージを作って、1単語のパッケージ名にする

スコープ

  • private は不要(public か、そうでなければデフォルト=記述しない)
  • 理想的には、パッケージでpublicなクラスはひとつ。それ以外は、すべてパッケージプライベート(デフォルト)
  • 同じパッケージ内では、他のクラスのフィールドを直接アクセスしてもよい(getterは不要)

final

  • final 宣言は書かない(すべてimmutable になる「メソッド」構成にする)

共通メソッドのオーバーライド

toString(), equals(), hashCode()

toString()

  • 必ずオーバーライドする
  • その値の正式の文字列表記を定義する(parse可能であることが望ましい)
    • 参考: LocalDate#toString()/#parse()

equals()

  • できるだけ書かない(コレクションで使う場合のみ書く)
  • null チェックしない、型チェックしない(契約による設計)

hashCode()

  • equals() をオーバーライドした場合、必ずオーバーライドする
  • 原則、フィールドの値のhashCode() を再利用する

共通メソッドオーバーライドのコード例

public class SomeType{

    int value;

    @Override
    public boolean equals(Object other) {
        return value == ((SomeType)other).value;
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }

    @Override
    public String toString() {
        return String.valueOf(value);
    }
}

例外

  • 引数の値が不正などの契約違反については、必要であれば例外を明示的に送出する
  • 標準例外を使用する ( 参考:Effective Java 第2版 項目60)
  • 基本は java.lang で定義済の例外を使用する
  • クラスが java.mathjava.time のクラスを使う場合は、それぞれのパッケージで定義済の例外も使用してよい

命名

  • 原則、一単語。多くて2単語
  • 常にフルスペル
  • enum の要素は、日本語名を原則とする
  • クラス名は英語が原則
  • メソッド名、パラメータ名、変数名は、英語を原則とするが、日本語に挑戦しても良い

習慣的な名前

  • 値オブジェクトで、インスタンス変数が一つの場合、インスタンス変数名は、value にする 例 int value;
  • オブジェクトのプリミティブな数値を返すメソッド名は、lang.Number クラスに準じる 例 intValue(), longValue()
  • オブジェクトの値を部分的に変更したオブジェクトを返すメソッド名は withValue() 形式 参考 time.LocalDate.withYear()
  • 説明的な文字表現を提供する場合 toString() とは別に show()describe() を使う

ドキュメンテーション

  • クラス名、パッケージ名は、javadoc 形式で「日本語」を明示する
  • それ以外は、コメントを書かない。コードで説明する。
  • コードだけで説明しきれない内容は、クラスのjavadocに記載する

メソッドのJavaDocコメント

  • 書かないようにする
  • メソッドの意味が伝わりにくい場合は、メソッド名、引数の型名/変数名、返す型名の改善を考える
  • コメントがあると、メソッド名や引数名に少々の違和感があっても、見逃しやすいため、あえて、コメントをつけないことで、名前の違和感の発見・議論・改善の機会を増やす

マーカーコメント

  • 作業中の内容は、積極的にマーカーコメントを残す
  • 問題が残っている場合 // FIXME
  • 未実装の場合、ジャストアイデアの場合 // TODO

テスト

  • 各テストクラスのメソッドに、@DisplayNameで、名前をつける
  • テストのassertが一つであれば、assertレベルの説明は不要(@DisplayNameに移動する)