どうも。つじけ(tsujikenzo)です。このシリーズでは、2022年5月に発売されました書籍「良いコード/悪いコードで学ぶ設計入門」についてお送りします。今日は5回目で、いったん最終回です。
前回のおさらい
前回は、「インターフェイスとアプリケーション設計」をお送りしました。
今回は、「インターフェイスの応用と実践」をお届けします。
今日のアジェンダ
- 条件分岐の重複とネスト
- リスコフの置換原則
- フラグ引数と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);
}
}
この条件判定のロジックを、ソースコードのあちこちに記述されないように、「ゴールド会員ポリシー」としてクラス化したほうが良いです。
最終系はこちらです。
リスコフの置換原則
業務アプリケーションでよく行うのが、映画のチケット料金や、宿泊料金の条件分岐です。
具体的な料金を抽象化した、料金レートインターフェイスを定義することから始めます。
抽象メソッド名は、料金を算出するという意味で、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型で扱っていた金額も、値オブジェクト化して、見通しがよくなりました。
フラグ引数
メソッドの挙動を切り替える方法として、引数で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などを取り入れた最終系はこちらです。
JavaScriptでも書いてみましょう。
アプリケーション全体の見通しがよくなりました。
まとめ
以上で、「インターフェイスの応用と実践」をお送りしました。
ソースコード内で条件分岐を書きそうになったら、インターフェイスによる設計を試みましょう。
そして、ソースコードは、一度書いたら終わりではありません。
何度もリファクタリングを重ねて、良い状態を保てるようにしましょう。
引き続き、書籍の考察をしていきたいと思います。