【読書録】「現場で役立つシステム設計の原則」第二章:場合分けのロジックを整理する

現場で役立つシステム設計の原則第二章 エンジニアリング
スポンサーリンク

目次

第一章:小さくまとめてわかりやすくする
第二章:場合分けのロジックを整理する ※本記事
第三章:業務ロジックをわかりやすく整理する

はじめに

この記事は増田 亨さんの著書「現場で役立つシステム設計の原則」を読んでの感想と備忘録です
自分用のメモのため、書籍の内容を網羅したものではありません

書籍のタイトルの通り、まさに現場ですぐにでも役立つ内容でしたので是非読んでみてください
特に新卒エンジニアに早いうちに読んで欲しい先輩達のクソコードに毒される前に

現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法 | 増田 亨 |本 | 通販 | Amazon
Amazonで増田 亨の現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法。アマゾンならポイント還元本が多数。増田 亨作品ほか、お急ぎ便対象商品は当日お届けも可能。また現場で役立つシステム設計の原則 ~変更を楽で安全にするオブジェクト指向の実践技法もアマゾン配送商品なら通常配送無料。

第二章の感想

区分と種別に関する処理をクラスに分解する、ということを通して以下を理解した

  • 実は場合分けのロジックは分岐それぞれお互いの分岐後の処理を知らなくてもいい
  • 他のクラスが知らなくてもいいことはクラスに閉じ込めた方がプログラムを変更しやすい
  • クラスを使う側は「使うクラスの使い方(インタフェース)」だけ知っていれば十分で実装まで知る必要はない
  • Javaなら列挙形(enum)を使うと多態の一覧性を担保してシンプルに記述できる

業務の関心事を分析し、分離して整理するべしという考え方は区分と種別に関する処理だけではなく、
変更しやすいプログラムを作る基本的な考え方のように思える

キーワード

  • 早期リターン
  • ガード節
  • インターフェース
  • 多態
  • 疎結合
  • 区分オブジェクト

第二章:場合分けのロジックを整理する

区分や種別がコードを複雑にする

場合分けのロジックがソフトウェアを複雑にする
例えば「顧客区分」「料金種別」など
区分や分類ごとに特別な業務ルールが適用され、if文やswitch文で実装されるので複雑になりがち
更に、複数の区分や種別を組み合わせると更に複雑になる
例: 送料を「顧客区分」「製品タイプ」「地域区分」から判断する

(感想)
愚直にif文やswitch文で実装するとコードが読みづらくなるのはとてもわかる
更に、区分や種別を追加する時に既存の分岐を全て読まないと理解できなくなりがちで地獄

判断や処理のロジックをメソッドに独立させる

区分ごとのコードの整理もオブジェクト指向の考えで設計する

  • コードのかたまりはメソッドに抽出する
  • 関連するデータとロジックを1つのクラスにまとめる
// 悪い例。判定や処理のロジックをそのままif文に書く

if (customerType.equals("child")) {
  fee = baseFee * 0.5; // 子供料金
}
// 良い例。メソッドに抽出する

if (isChild()) {
  fee = childFee();
}

// 抽出した判定ロジック
private Boolean isChild() {
  return customerType.equals("child");
}

// 抽出した処理ロジック
private int childFee() {
  return baseFee * 0.5;
}

(感想)
これだけではパッと見効果が高いようには見えないけど、名前をつけただけでそれぞれが何者かわかりやすくはなる

else句をなくすと条件分岐が単純になる

else句はプログラムの構造を複雑にするので出来るだけ使わないようにする
ローカル変数への代入を使わずすぐ値を返す(早期リターン)方がシンプルになる

// 悪い例。 else連打 + ローカル変数への代入

Yen fee() {
  Yen result;
  if (isChild()) {
    result = childFee();
  } else if (isSenior()) {
    result = seniorFee();
  } else {
    result = adultFee();
  }

  return result;
}
// 良い例。 elseなし + 早期リターン

Yen fee() {
  if (isChild()) return childFee();
  if (isSenior()) return seniorFee();
  return adultFee();
}

(感想)
こういう分岐は本当によく使うし、早期リターンを知ってから実際に現場でよく使っている

ローカル変数への代入がバグを生みやすい、というのはちょうど最近出会ったばかりなのでよくわかる

// ローカル変数への代入で発生していた、最近出会ったバグの例

Yen fee() {
  Yen result;
  if (isChild()) {
    result = childFee();
  } else if (isSenior()) {
    result = seniorFee();
  }
  result = adultFee(); // ここで代入しているのでif文の分岐が無意味になっている...

  return result;
}

ところで、「全てのif文の条件に当てはまらなかった場合は大人料金」というのはコードから意図が汲み取れない気がするし、大人料金だけ他の料金と違う書き方なので一見特別な料金なのかな?と思ってしまうので以下のようにするのが良さそう

Yen fee() throws Exception {
  if (isChild()) return childFee();
  if (isSenior()) return seniorFee();
  if (isAdult()) return adultFee();

  throw new Exception("不正な年齢区分です");
}

復文は単文に分ける

else ifは入れ子になった復文構造になっている

// 実際はこれと同義

if (hoge) {
  ...
} else {
  if (fuga) {
    ...
  }
}

復文は意図をわかりづらくするので単文で開くのが良い

また、単文を並べる方式であれば区分の追加も楽
他のif文と疎結合なので並び替えも可

Yen fee() {
  if (isBaby()) return babyFee(); // 幼児区分を追加
  if (isChild()) return childFee();
  if (isSenior()) return seniorFee();
  return adultFee();
}

(感想)
よく言われる「ネストを減らしましょう」かな

区分ごとのロジックを別クラスに分ける

更に独立性を高めるためには区分ごとにメソッドではなくクラスに分ける

class AdultFee {
  Yen fee() {
    return new Yen(100);
  }

  String label() {
    return "大人";
  }
}

class ChilidFee {
  Yen fee() {
    return new Yen(50);
  }

  String label() {
    return "子供";
  }
}

(感想)
次節で出てくるけどインターフェースを作って実装することは必須
特定の区分にだけ複雑なロジックが出てくる、ということも多々あるので他の区分の関心と切り離せるのは可読性が爆増しそう

区分ごとにクラスを同じ「型」として扱う

区分ごとにクラスを分けるとロジックは理解しやすくなるが、使う側はそれぞれのクラスを使い分けないといけなくなる
なのでインターフェースを使って異なるクラスのオブジェクトを同じ型として使えるようにする(多態)

// インターフェースを宣言
interface Fee {
  Yen yen();
  String label();
}

class AdultFee implements Fee {
  ... // 実装
}

class ChildFee implements Fee {
  ... // 実装
}

// クラスを使う側のコード
class Charge {
  Fee fee;

  Charge(Fee fee) {
    this.fee = fee; // AdultFee, ChildFeeどちらでも可
  }
}

使う側(Chargeクラス)は使われる側(料金区分)にどのようなものが存在するのかを知る必要がない(疎結合)
なので、料金区分を料金区分を追加する場合でも使う側(Chargeクラス)を変更しなくて良い
つまり、多態を使ったオブジェクト指向らしい書き方で変更の影響をクラス内に閉じ込められるため安全

(感想)
まだ実際に試していないけど、変更の影響が大きいことがソフトウェアの修正の1番の妨げなのでこのパターンはかなり使えそうだと思う

区分ごとのクラスのインスタンスを生成する

if文を使わずに区分ごとのクラスのインスタンスを作ることもできる

class FeeFactory{
  static Map<String,Fee> types;

  static {
    types = new HashMap<String, Fee>();
    types.put("adult",new AdultFee());
    types.put("child",new ChildFee());
  }

  static Fee feeByName(String name){
    return types.get(name);
  }
}

// クラスを使う側のコード
Fee fee = FeeFactory.feeByName("adult");

(感想)

いわゆるFactoryパターンをif文を使わずに実現する例?

Javaの列挙型を使えばもっとかんたん

多態を使うと区分の一覧がわかりにくいという問題がある
javaの場合は列挙形(enum)を使うとクラスの一覧を明示的に記述できる

// 区分をenumで定義
enum FeeType {
  adult,
  child,
  senior
}

// 区分を使うクラス
class Guest {
  FeeType type;

  boolean isAdult() {
    return type.equals(FeeType.adult)
  }
}

javaのenumは単純な定数ではなく、クラスなので変数やロジックを書くことができる
列挙形をつかて区分ごとのロジックを整理する方法を区分オブジェクトという

// 区分のロジックもenumで定義
enum FeeType {
  adult( new AdultFee() ),
  child( new ChildFee() ),
  senior( new SeniorFee() );

  private Fee fee;

  private FeeType(Fee fee) {
    this.fee = fee;
  }

  Yen yen() {
    return fee.yen();
  }
}

// 区分を使うメソッド
Yen feeFor(String feeTypeName) {
  FeeType feeType = FeeType.valueOf(feeTypeName);
                                    // 例えばadult
  return feeType.yen();
}

(感想)
区分オブジェクトという言葉は初めて聞いた
区分の追加、変更は業務上そこそこ起こりうることなので使えそう

区分ごとの業務ロジックを区分オブジェクトで分析し整理する

業務ロジックの複雑さはほとんど区分や区分の組み合わせに関連する
区分オブジェクトの設計通して区分に関わる業務ロジックを把握し整理できる
第一章の「値オブジェクト」「コレクションオブジェクト」と同じく、「区分オブジェクト」も業務の関心事と直接的に対応する
オブジェクト指向では「業務の関心事や業務ロジックを分析し整理する活動」と「クラスを設計する活動」は基本的に同じ

(感想)
「業務の関心事や業務ロジックを分析し整理する活動」と「クラスを設計する活動」は基本的に同じ!
だからこそ面白いと感じるのだなぁ
修正が容易なソフトウェア、業務に役立つソフトウェア、一挙両得でお得

状態の遷移ルールをわかりやすく記述する

業務アプリケーションでは状態遷移の管理も重要な関心事の一つ
ある状態から次に遷移できる状態には制限がある
例: 審査中->承認済->実施中 はOK、審査中->実施中 はNG

状態遷移も宣言的に記述できる
例えば以下の状態遷移表の場合

from/to審査中承認済差戻中実施中中断中終了
審査中XOOXXX
承認済XXXOXO
差戻中OXXXXO
実施中XXXXOO
中断中XXXOXO
終了XXXXXX
// 状態をenumで宣言
enum State {
  審査中,
  承認済,
  差戻中,
  実施中,
  中断中,
  終了
}

// 状態遷移できるかを判定するクラス
class StateTransitions {
  Map<State,Set<State>> allowed;

  {
        allowed = new HashMap<>();
        // 許可する状態遷移をMapに入れる
        allowed.put("審査中",Enum.of(承認済,差戻中))
        allowed.put("差戻中",Enum.of(審査中,終了))
        allowed.put("承認済",Enum.of(実施中,終了))
        allowed.put("実施中",Enum.of(中断中,終了))
        allowed.put("中断中",Enum.of(実施中,終了))
    }

    boolean canTransit(State from, State to){
        Set<State> allowedStates = allowed.get(from);
        return allowedStates.contains(to); // 状態遷移が許可されているか判定
    }
}

このやり方は状態遷移だけではなく、あるイベントがその状態で起きて良いのかの判定にも応用できる

コメント

タイトルとURLをコピーしました