0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【技術書】現場で役立つシステム設計の原則 Chapter2 場合分けのロジックを整理する

Posted at

目次

プログラムを複雑にする「場合分け」のコード

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

プログラムのあちこちに同じようなif文/switch文が重複すると、区分の追加や区分ごとのルールの変更があったときにプログラムの変更をやりにくくします。

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

:x: 判断や処理のロジックをそのままif文の中に書く

java
if(customerType.equals("child")) {
    fee = baseFee * 0.5 ;
}

:o: メソッドに抽出して独立させる

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

// 判断ロジックの詳細
private Boolean isChild() {
    return customerType.equals("child");
}

// 計算ロジックの詳細
private int childFee() {
    return baseFee * 0.5 ;
}

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

else句は、プログラムの構造を複雑にします。else句をできるだけ書かないほうがプログラムは単純になります。

:x: else句を使った書き方

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

ローカル変数を使わずに判定後、ただちに結果を返す

java
Yen fee(){

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

ローカル変数がなくなったことで、コードがシンプルでわかりやすくなりました。
これを 早期リターン と言います。早期リターンを使った場合、else句を書く必要はありません。

:o: else句をなくした書き方

java
Yen fee(){

    if(isChild()) return childFee();
    if(isSenior()) return seniorFee();

    return adultFee();
}

else句を使わずに早期リターンするこの書き方を ガード節 と言います。

複文は単文に分ける

「幼児」区分を追加する場合、単文を並べた方式であれば、もう1つ単文を追加するだけで済みます。

java
Yen fee(){

    if(isBaby()) return babyFee();
    if(isChild()) return childFee();
    if(isSenior()) return seniorFee();

    return adultFee();
}

このように、else句を使わずに独立性の高い単文を並べる書き方は、if-then-else構文で書いた場合分けに比べ、プログラムをわかりやすくし、変更を楽で安全にします。

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

区分に関するさまざまなロジックを区分ごとのクラスに分けて記述すれば、区分ごとのロジックが整理され、どこに何が書かれているか明確になります。

:o: 区分ごとのロジックを別のクラスに分けて記述する

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

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

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

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

class SeniorFee {
    Yen fee() {
        return new Yen(80);
    }

    String label() {
        return "シニア";
    }
 }

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

Javaではインターフェース宣言を使えば、異なるクラスを同じ型として扱うことができます。

:o: AdultFeeクラスとChildFeeクラスをFee型として宣言する

java
interface Fee {
    Yen yen();
    String label();
}

class AdultFee implements Fee {
    Yen yen() {
        return new Yen(100);
    }

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

class ChildFee implements Fee {
    Yen yen() {
        return new Yen(50);
    }

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

Fee型を使う側のコード

java
class Charge {
    Fee fee;

    Charge(Fee fee) {
        this.fee = fee;
        // feeはAdultFee型またはChildFee型どちらでもよい
    }

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

Chargeクラスのオブジェクトを生成する時に、コンストラクタにFee型のどのクラスのオブジェクトを渡すかによって、Chargeクラスの振る舞いが変わります。

AdultFee型であれば大人料金を計算。ChildFee型であれば子供料金の計算。

例:子供連れの団体の合計料金を計算

java
class Reservation {
    List<Fee> fees; // 大人と子供の内訳は不明

    Reservation(List<Fee> fees) {
        this.fees = fees;
    }

    Reservation(addFee(Fee fee)) { // 大人と子供を意識しない
        List<Fee> result = new ArrayList<>(fees);
        return new Reservation(result.add(fee));
    }

    Yen feeTotal() {
        Yen total = new Yen(0); // 合計ゼロ円
        for( Fee each : fees ) {
            total = total.add( each.yen() );
        }
        return total;
    }
}

Reservationクラスは、大人と子供を意識しないでFee型のオブジェクトの料金を合計しているだけです。

このようにインターフェース宣言(Fee)と、区分ごとの専用クラス(AdultFee/ChildFee)を組み合わせて、区分ごとに異なるクラスのオブジェクトを「同じ型」として扱うしくみを 多態 と言います。

  • 使う側のクラスが、どのような区分があるのか「知らない」ことが重要
  • クラスとクラスの関係は、知っていることが多いほど密結合になる(ダメなこと)
  • 「知らない」ことが多いほど、クラス間の結びつきが弱くなる(良いこと)
  • 結合が弱いほど、独立性が高くなり、あるクラスの変更がほかのクラスに影響することが減る
  • 多態 :区分ごとのロジックをクラス単位に分離してコードを読みやすくするオブジェクト指向らしいしくみ

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

多態は、利用する側のコードをシンプルにします。

:o: if文を使わずに区分ごとのオブジェクトを生成するやり方の例

java
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);
    }
}

料金区分名を指定すれば、該当する料金区分のオブジェクトを取得できます。

java
FeeFactory.feeByName("adult")

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

多態には区分の一覧がわかりくいという問題があり、どういう区分体系であるか、一覧性に欠けます。
Javaでは区分ごとのクラス一覧を 列挙型(enum) で明示的に記述できます。

列挙型の使い方

java
// 料金区分の定義

enum FeeType {
    adult,
    child,
    senior
}

// 区分を使う側のクラス

class Guest {
    FeeType type ;

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

Javaでは列挙型もクラスです。区分ごとの値をインスタンス変数として保持したり、区分ごとのロジックをメソッドとして記述できたりします。

料金ごとのロジックをenumを使って表現する

java
enum FeeType {
    adult( new AdultFee() ),
    child( new ChildFee() ),
    senior( new SeniorFee() );

    private  Fee fee; 
    // Feeインターフェースを実装したどれかのクラスのオブジェクト

    private FeeType(Fee fee) {
        this.fee = fee; // 料金区分ごとのオブジェクトを設定する
    }

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

    String label() {
        return fee.label();
    }
}

料金区分から料金を計算するメソッドの例

java
Yen feeFor(String feeTypeName) {
    FeeType feeType = FeeType.valueOf(feeTypeName);
                                     // たとえば、"adult"
    return feeType.yen();
}

列挙型を使って、区分ごとのロジックをわかりやすく整理するこの方法を 区分オブジェクト と言います。

  • 区分定数を単なる定数ではなく、振る舞いを持ったオブジェクトとして表現
  • メソッドを指定して判断/加工/計算を依頼できる

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

業務アプリケーションでは、状態の遷移を管理することも重要な関心事のひとつです。

状態遷移の例

列挙型を使うと、このような状態遷移に関わる制約を、if文/switch文を使わずに表現できます。

状態遷移を列挙する

java
enum State {
    審査中,
    承認済,
    実施中,
    終了,
    差し戻し中,
    中断中
}

// 使用例
State[] states = State.values(); // 状態の一覧
Set nextStates = EnumSet.of(承認済,差し戻し中); // 状態のグルーピング

状態遷移を列挙型とコレクションを使ってコードで表現するやり方は次のとおりです。
(ここでは省きますが、一旦、状態遷移表を作成するとわかりやすくなります)

  • ある状態から遷移可能な状態(複数)をSetで宣言する
  • 遷移元の状態を「キー」に、遷移可能な状態のSetを値(バリュー)にしたMapを宣言する

ある状態から遷移できるかを判定する

java
class StateTransitions {
    Map<State, Set<State>> allowed;

    {
        allowed = new HashMap<>();

        allowed.put(審査中, EnumSet.of(承認済, 差し戻し中));
        allowed.put(差し戻し中, EnumSet.of(審査中, 終了));
        allowed.put(承認済, EnumSet.of(実施中, 終了));
        allowed.put(実施中, EnumSet.of(中断中, 終了));
        allowed.put(中断中, EnumSet.of(実施中, 終了));
    }

    boolean canTransit(State from, State to) {
        Set<State> allowedStates = allowed.get(from);
        return allowedStates.contains(to);
    }
}

状態の列挙型と、イベントの列挙型を組み合わせることで、次のようなルールを宣言的に記述できます。

  • あるイベントがその状態で起きてよいイベントか起きてはいけないイベントかの判定
  • ある状態で発生してもよいイベントの一覧の提示

Javaの列挙型は振る舞いを持てます。

勉強したことをどう活かすか?

  • 判断や処理のロジックをメソッドに独立させる
  • 早期リターン を使う
    • ローカル変数を使わずに判定後、ただちに結果を返す
    • 結果格納用の変数を使わない
  • ガード節 を使う
    • else句を使わずに早期リターンする
  • 福文は単文に分ける
  • 区分ごとのロジックは別クラスに分けて記述する
  • 区分ごとのクラスを同じ「型」として扱う
    • 例:AdultFeeクラスとChildFeeクラスをFee型として宣言する
    • 多態 を使う
      • 区分ごとに異なるクラスのオブジェクトを「同じ型」として扱うしくみ
  • if文を使わずに区分ごとのオブジェクトを生成する
  • Javaの列挙型(enum)クラスを使う
    • 区分オブジェクト:列挙型を使って、区分ごとのロジックを整理する方法
      • メソッドを指定して判断/加工/計算を依頼する
    • 業務アプリの状態の遷移ルールをわかりやすく記述する
0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?