[オブジェクト指向]良いコード/悪いコードで学ぶ設計入門 6章 インターフェイスの応用と実践

mino6_5プログラミング

どうも。つじけ(tsujikenzo)です。このシリーズでは、2022年5月に発売されました書籍「良いコード/悪いコードで学ぶ設計入門」についてお送りします。今日は5回目で、いったん最終回です。

前回のおさらい

前回は、「インターフェイスとアプリケーション設計」をお送りしました。

[オブジェクト指向]良いコード/悪いコードで学ぶ設計入門 6章 インターフェイスとアプリケーション設計
どうも。つじけ(tsujikenzo)です。このシリーズでは、2022年5月に発売されました書籍「良いコード/悪いコードで学ぶ設計入門」についてお送りします。今日は4回目です。前回のおさらい前回は、「インターフェイスの概念...

今回は、「インターフェイスの応用と実践」をお届けします。

今日のアジェンダ

  • 条件分岐の重複とネスト
  • リスコフの置換原則
  • フラグ引数とStrategyパターン

条件分岐の重複とネスト

「とある複数の条件を満たすとき」という条件分岐をおこなうばあいがあります。

この条件式の重複を、ソースコードでも重複せずに書けないものでしょうか。 

Policyパターン

Strategyパターンは、別名Policyパターンとも呼ばれます。

Policyパターンで記述すると、以下のことが可能になります。

  • 条件の部品化
  • 部品化した条件を組み替えてカスタマイズ可能にする

手順を確認してみましょう。

ルールクラス

まず、優良顧客のルールを表現するインターフェイスを定義します。

interface ExcellentCustomerRule {
  /**
   * @param history 購入履歴
   * @return 条件を満たす場合true
   */boolean ok(final PurchaseHistory history);
}

ゴールド会員になる条件は3つです。各クラスで、抽象メソッドを実装します。

/** 購入金額合計クラス */
class GoldCustomerPurchaseAmountRule implements ExcellentCustomerRule {
  public boolean ok(final PurchaseHistory history) {
    return 100000 <= history.totalAmount; 
    }
}

/** 購入回数合計クラス */
class PurchaseFrequencyRule implements ExcellentCustomerRule {
    public boolean ok(final PurchaseHistory history) {
      return 10 <= history.purchaseFrequencyPerMonth;
    }
}

/** 返品率クラス */
class ReturnRateRule implements ExcellentCustomerRule {
    public boolean ok(final PurchaseHistory history) {
      return history.returnRate <= 0.001;
  }
}

Policyクラス

次に、Policyクラスを定義します。

コンストラクタでHashSetのインスタンス(コレクションの1つです)を生成して、addメソッドでルール(ExcellentCustomerRule型)を追加します。

/** 優良顧客ポリシークラス */
class ExcellentCustomerPolicy {
  private final Set<ExcellentCustomerRule> rules;

  ExcellentCustomerPolicy() {
    rules = new HashSet();
  }

  /**
   * ルールを追加するメソッド
   * @param rule ルール
   */
  void add(final ExcellentCustomerRule rule) {  
    rules.add(rule);
  }
}

ルールをすべて満たすか判定するメソッドを定義します。

 /**
  * ルールをすべて満たすか判定するメソッド
  * @param history 購入履歴
  * @return ルールを全て満たす場合true
  */
boolean complyWithAll(final PurchaseHistory history) {
  for (ExcellentCustomerRule each : rules) {
    if (!each.ok(history))
    return false;
  }
 return true;
}

ルールクラスとポリシークラスを使うと、ロジックの条件判定をこのように書けます。

if文のネストが無くなって、すっきりしましたね。

/** 顧客のランク判定ロジッククラス */
class CustomerRankLogic {
  /**
   * @return ゴールド会員である場合true
   * @param history 購入履歴
   */
  boolean isGoldCustomer(PurchaseHistory history) {
    ExcellentCustomerPolicy goldCustomerPolicy = new ExcellentCustomerPolicy();
    goldCustomerPolicy.add(new GoldCustomerPurchaseAmountRule());
    goldCustomerPolicy.add(new PurchaseFrequencyRule());
    goldCustomerPolicy.add(new ReturnRateRule());

   return goldCustomerPolicy.complyWithAll(history);
  }
}

この条件判定のロジックを、ソースコードのあちこちに記述されないように、「ゴールド会員ポリシー」としてクラス化したほうが良いです。

最終系はこちらです。

minodriven-goodcodebadcode/Section6/Sample6_49.java at main · tsujike/minodriven-goodcodebadcode
ミノ駆動本のソースコードです。. Contribute to tsujike/minodriven-goodcodebadcode development by creating an account on GitHub.

リスコフの置換原則

業務アプリケーションでよく行うのが、映画のチケット料金や、宿泊料金の条件分岐です。

具体的な料金を抽象化した、料金レートインターフェイスを定義することから始めます。

抽象メソッド名は、料金を算出するという意味で、fee()メソッドにします。

interface HotelRates{
  intfee();  
}

次に、具象クラスで、fee()メソッドを実装します。

/** プレミアム料金クラス */
class PremiumRates implements HotelRates {
  public int fee() {
  return 12000;
  }
}

/** 通常料金クラス */
class RegularRates implements HotelRates {
  public int fee() {
    return7000;
  }
}

これで、Strategyパターンによる処理の切替が可能になります。

//中略/
** 料金判定クラス */
class CheckRate {
  int showRate(HotelRates hotelRates) {
    return hotelRates.fee();
  }
}

/** 実行用エントリポイント */
public classSample6_4_1 {

public static void main(String[] args) {
  HotelRates hotelRates = new PremiumRates();
  System.out.println(hotelRates.fee()); // 1200 // 料金判定クラスを使う

  CheckRate checkRate = new CheckRate();
    System.out.println(checkRate.showRate(hotelRates)); // 12000
  }
}

ここで、「ハイシーズンは割増料金」という機能を追加したいと思います。

せっかく、インターフェイス設計ができているのに、このように、インスタンスの型によって条件判定するのはよろしくありません。

/** 良くない機能追加・条件判定クラス */
class TypeCheckLogic {
  HotelRates hotelRates;

  TypeCheckLogic(HotelRates hotelRates) {
    this.hotelRates = hotelRates;
  }

  int setHighSeasonFee() {
    int seasonFee=0;
    if (hotelRates instanceof RegularRates) {
      seasonFee = hotelRates.fee() + 3000;
      return seasonFee;
    } else if (hotelRates instanceof PremiumRates) {
      seasonFee = hotelRates.fee() + 5000;
      return seasonFee;
    }
    return seasonFee;
  }
}

/** 実行用エントリポイント */
public class Sample6_56 {
  //中略
  //ハイシーズンの料金設定
  TypeCheckLogic typeCheckLogic = new TypeCheckLogic(hotelRates);
    System.out.println(typeCheckLogic.setHighSeasonFee()); //17000
}

なぜなら、instanceofメソッドで条件分岐した先で、hotelRatesインスタンスに金額を上乗せしています。

このhotelRatesインスタンスは、ほかの継承型へ置換できず、分岐した先で使い捨てるだけの状態になっています。

オブジェクト指向プログラミングには、「あるクラスXのインスタンスについて、かならず成り立つ条件があるなら、その条件はXのサブクラスYのインスタンスについてもかならず成り立たなければならない」 という原則があります。

これを、リスコフの置換原則と言います。 

ハイシーズン料金の切り分けを行うなら、インターフェイスでポリモーフィズムを行いましょう。

抽象メソッドを追加して、具象クラスで、具体的な処理を実装すればよいのです。

最終系はこのようなコードになります。int型で扱っていた金額も、値オブジェクト化して、見通しがよくなりました。

minodriven-goodcodebadcode/Section6/Sample6_57.java at main · tsujike/minodriven-goodcodebadcode
ミノ駆動本のソースコードです。. Contribute to tsujike/minodriven-goodcodebadcode development by creating an account on GitHub.

フラグ引数

メソッドの挙動を切り替える方法として、引数でboolean型や整数を渡す、フラグ引数という手法があります。

//メソッドの呼び出し
damage(true, damageAmount);

//メソッドの定義
void damage(boolean damageFlag, int damageAmount) {
 if (damageFlag == true) {
  // ヒットポイントダメージ
  member.hitPoint -= damageAmount;
 if (0 < member.hitPoint) return;

  member.hitPoint = 0;
  member.addState(StateType.dead);
 } 
  else {
  // 魔法力ダメージ
  member.magicPoint -= damageAmount;
  if (0 < member.magicPoint) return;
  member.magicPoint = 0;
  }
}

いずれも、よろしくない例です。

void execute(int processNumber) {
  if (processNumber == 0) {
  // アカウント登録処理
  }
  else if (processNumber == 1) {
  // 配送完了メール送信処理
  }
  else if (processNumber == 2) {
  // 注文処理
  }
  else if (processNumber == 3) {
  }
}

このようなばめんでは、まず、機能ごとにメソッドを分離して、メソッドにふさわしい名前を付けるといいでしょう。

Member member;
  void hitPointDamage(final int damageAmount) {
    member.hitPoint -= damageAmount;
    if (0 < member.hitPoint) return;

    member.hitPoint = 0;
    member.addState(StateType.dead);
  }

  void magic PointDamage(final int damageAmount) {
    member.magicPoint -= damageAmount;
    if (0 < member.magicPoint) return;

    member.magicPoint = 0;
  }

切り替え機構をStrategyパターンで実現する

これまでやってきたように、処理の切り替えをStrategyパターンで設計してみましょう。

まず、ダメージを表すインターフェイスを定義します。

interface Damage {
  void execute(final int damageAmount);
}

次に、各処理ごとに、具体的なメソッドを実装します。

/** HPダメージクラス */
class HitPointDamage implements Damage {
  Member member;

  // 中略
  public void execute(final int damageAmount) {
     member.hitPoint -= damageAmount;

    if (0 < member.hitPoint)
      return;

    member.hitPoint = 0;
    member.addState(StateType.dead);
  }
}

/** MPダメージクラス */
class Magic PointDamage implements Damage {
  Member member;

  // 中略
  public void execute(final int damageAmount) {
    member.magicPoint -= damageAmount;

    if (0 < member.magicPoint)
     return;

  member.magicPoint = 0;
  }
}

処理の切り替えに、Mapなどを取り入れた最終系はこちらです。

minodriven-goodcodebadcode/Section6/Sample6_65.java at main · tsujike/minodriven-goodcodebadcode
ミノ駆動本のソースコードです。. Contribute to tsujike/minodriven-goodcodebadcode development by creating an account on GitHub.

JavaScriptでも書いてみましょう。

minodriven-goodcodebadcode/Section6/myFunction6_7.js at main · tsujike/minodriven-goodcodebadcode
ミノ駆動本のソースコードです。. Contribute to tsujike/minodriven-goodcodebadcode development by creating an account on GitHub.

アプリケーション全体の見通しがよくなりました。

まとめ

以上で、「インターフェイスの応用と実践」をお送りしました。

ソースコード内で条件分岐を書きそうになったら、インターフェイスによる設計を試みましょう。 

そして、ソースコードは、一度書いたら終わりではありません。

何度もリファクタリングを重ねて、良い状態を保てるようにしましょう。

引き続き、書籍の考察をしていきたいと思います。

参考資料

このシリーズの目次

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