🐡

[コード設計]条件分岐2/ポリシーパターン

2023/01/10に公開

初めに

コード設計について「良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方」を手に取り、学んでいます。本記事はこちらを参考に書いています。
今回は前回に引き続き、条件分岐について取り上げます。


本題

条件の重複(アンチパターン)

条件の重複はコードを書いているとよくあることではないでしょうか。本書ではECサイトアプリを想定し、優良会員の判定ロジックが一部重複されている例が取り上げられていました。

Historyクラスを用意し、そのインスタンス変数によって優良会員を判定します。
今回Historyクラスは、全ての条件が通るデフォルト値を設定しています。

class History():
    """購入履歴"""
    def __init__(self, total_amount=100500, purchase_frequency_per_month=15, return_rate=0.00001):
        """
        Args:
            total_amount: 購入金額の合計
            purchase_frequency_per_month: 一月の購入頻度
            return_rate: 返品率
        """
        self.total_amount = total_amount
        self.purchase_frequency_per_month = purchase_frequency_per_month
        self.return_rate = return_rate


以下が優良会員のゴールド会員と、シルバー会員を判定するメソッドになりますが、購入頻度と返品率のロジックは重複していることがわかります。

"""アンチパターン 条件分岐の重複"""
def is_gold_customer(history: History):
    """ゴールド会員

    Args:
        history: 購入履歴
    Returns:
        ゴールド会員条件を満たす場合True
    """
    if (history.total_amount >= 100000): # 購入金額
        if (history.purchase_frequency_per_month >= 10): # 購入頻度
            if (history.return_rate <= 0.001): # 返品率
                return True
    
    return False

def is_silver_customer(history: History):
    """シルバー会員

    Args:
        history: 購入履歴
    Returns:
        シルバー会員条件を満たす場合True
    """
    if (history.purchase_frequency_per_month >= 10): # 購入頻度
        if (history.return_rate <= 0.001): # 返品率
            return True
    
    return False



現時点では会員の種類は2つのため、さほど問題は感じないかもしれませんが、今後仕様変更となり、他の会員ランクが追加され、同じ条件が使用されていると同じ判定ロジックがあちこちでも使用され、冗長なコードになります。


ポリシーパターン(対策)

判定ロジックを再利用し、条件の重複を防ぐ方法としてポリシーパターンが挙げられます。流れとしては以下の通りです。順を追って実装していきます。

  1. 条件を表現する抽象クラスを作成
  2. 抽象クラスを継承して、条件クラスを作成
  3. 条件を集約、判定するPolicyクラスを作成
  4. Policyクラスを継承する、優良会員クラスを作成


条件を表現する抽象クラスを作成

まず条件を定義する抽象クラスをinterface, Pythonではabc.Metaで定義します。

import abc

class ExcellentCustomerRule(metaclass=abc.ABCMeta):
    """ルールの抽象クラス"""
    @abc.abstractmethod
    def ok(self, history):
        """
        Args:
            history: 購入履歴
        Returns:
            条件を満たす時 True
        """ 

abstractmethodデコレータを使用することで、継承するクラスはokメソッドを必ず定義するようにします。


抽象クラスを継承して、条件クラスを作成

各条件クラスを作成していきます。okメソッド内に、判定ロジックを置きます。

class GoldCustomerPurchaseAmountRule(ExcellentCustomerRule):
    """ゴールド会員の購入金額ルール"""
    def ok(self, history):
        return history.total_amount >= 10000
        

class PurchaseFrequencyRule(ExcellentCustomerRule):
    """購入頻度のルール"""
    def ok(self, history):
        return history.purchase_frequency_per_month >= 10


class ReturnRateRule(ExcellentCustomerRule):
    """返品率のルール"""
    def ok(self, history):
        return history.return_rate <= 0.001


条件を集約するPolicyクラスを作成

class ExcellentCustomerPolicy():
    def __init__(self):
        self.rules = list()

    def add(self, rule: ExcellentCustomerRule):
        """条件をrulesに追加する
        Args:
            rule: ルール
        """
        self.rules.append(rule)

    def comply_with_All(self, history):
        """ruleから各条件を判定する
        Args:
            history: 購入履歴
        Returns
            ルールを全て満たす場合True
        """
        for rule in self.rules:
            if not rule.ok(history):
                return False
        return True

Policyクラスはrulesというlist型をインスタンス変数に持ちます。addメソッドでは、条件クラスのインスタンスをrulesに追加します。comply_with_Allメソッドは、rulesから一つずつokメソッドを呼び出して条件を判定します。


Policyクラスを継承する、優良会員クラスを作成

上記で設計したPolicyクラスを継承し、各優良会員クラスを作成します。インスタンス化する際に、addメソッドを呼び出し、rulesの値を設定します

class GoldCustomerPolicy(ExcellentCustomerPolicy):
        """ゴールド会員の条件"""
    def __init__(self):
        super().__init__()
        self.add(GoldCustomerPurchaseAmountRule())
        self.add(PurchaseFrequencyRule())
        self.add(ReturnRateRule())


class SilverCustomerPolicy(ExcellentCustomerPolicy):
         """シルバー会員の条件"""
    def __init__(self):
        super().__init__()
        self.add(PurchaseFrequencyRule())
        self.add(ReturnRateRule())


それでは実際に使用してみます。

history = History()
gold_customer_policy = GoldCustomerPolicy()
print(gold_customer_policy.comply_with_All(history)) 
#=> True

history = History(purchase_frequency_per_month=5) # 購入頻度を5回にし条件を満たさない
silver_customer_policy = SilverCustomerPolicy()
print(silver_customer_policy.comply_with_All(history))
#=> False

無事に条件を再利用し、重複を防ぎ判定することができました。これにより、新しいランクの優良会員が追加されたとしても、同条件の場合は条件クラスをaddメソッドに渡すだけで済むので、コードが膨れ上がることもなく、仕様変更に強い設計と言えるのではないのでしょうか。


参考

Discussion