Yanonoblog!

こつこつと

ポリシーパターン

はじめに

本記事では「良いコード 悪いコードで学ぶ設計入門」で学んだ内容と別途気になって調べた内容や知識も含めアウトプットしています。

書籍に記載されている内容を順序立ててまとめるというよりは、整理しておきたい周辺の知識と深ぼった内容を雑多に書いています。

書籍ではJavaで説明されていたのですが本記事ではRubyに置き換えながら解説しています。

ネストで複雑化した分岐コード

多重にネストし複雑化した分岐の解消にも役立ちます。

ECサイトにおいて、優良顧客かどうかを判定するロジックです。

顧客の購入履歴を調べ、次の条件をすべて満たす場合にゴールド会員と判定します。

  • これまでの購入金額が10万円以上であること
  • 1か月あたりの購入頻度が10回以上であること
  • 返品率が0.1%以内であること
# ゴールド会員かどうかを判定するメソッド
# @return [Boolean] ゴールド会員である場合はtrue
# @param history [PurchaseHistory] 購入履歴
def is_gold_customer?(history)
  if history.total_amount >= 100000 # これまでの購入金額が10万円以上であること
    if history.purchase_frequency_per_month >= 10 # 1か月あたりの購入頻度が10回以上であること
      if history.return_rate <= 0.001 # 返品率が0.1%以内であること
        return true
      end
    end
  end
  return false
end

次の条件をすべて満たす場合にシルバー会員と判定します。

  • 1か月あたりの購入頻度が10回以上であること
  • 返品率が0.1%以内であること
# 会員ランクを判定するメソッド
# @return [Boolean] シルバー会員である場合はtrue
# @param history [PurchaseHistory] 購入履歴
def is_silver_customer?(history)
  if history.purchase_frequency_per_month >= 10
    if history.return_rate <= 0.001
      return true
    end
  end
  return false
end

会員ランクの判定条件が一部ゴールド会員と同じであり、将来的にブロンズなどの会員ランクが追加され、同様の判定条件が存在する場合、現在の実装では同じ判定ロジックが複数の場所に散らばってしまいます。

このような場合、保守性が低下するため判定ロジックの再利用性を高める方法が求められます。

再利用性を高めるためには、共通の判定ロジックを別の場所にまとめ、必要な箇所から呼び出せるようにする必要があります。

ポリシーパターンで条件を集約する

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

# 優良顧客のルールを表現するインターフェース
module ExcellentCustomerRule
  # 条件を満たす場合はtrueを返す
  def ok?(history)
    # 実際の条件判定ロジックを実装する必要があります
  end
end
ルール

優良顧客のルールを表現するExcellentCustomerRuleモジュールをincludeしたクラスを定義します。

  • ゴールド会員の購入金額ルールを表すGoldCustomerPurchaseAmountRuleクラス
  • 購入頻度のルールを表すPurchaseFrequencyRuleクラス
  • 返品率のルールを表すReturnRateRuleクラスです。

これらのクラスはExcellentCustomerRuleインターフェースを実装しており、**ok**?メソッドを持っています。

# ゴールド会員の購入金額ルール
class GoldCustomerPurchaseAmountRule
  include ExcellentCustomerRule

  def ok?(history)
    history.total_amount >= 100_000
  end
end

# 購入頻度のルール
class PurchaseFrequencyRule
  include ExcellentCustomerRule

  def ok?(history)
    history.purchase_frequency_per_month >= 10
  end
end

# 返品率のルール
class ReturnRateRule
  include ExcellentCustomerRule

  def ok?(history)
    history.return_rate <= 0.001
  end
end

Policyクラス

RubyにおけるPolicyクラスは、特定のビジネスルールや方針を表現するためのクラスです。

通常、条件判定や許可/禁止の制御ロジックをカプセル化し、再利用可能で柔軟な方法でアプリケーションの振る舞いを設定します。

優良顧客の方針を表現するExcellentCustomerPolicyクラスを定義します。

このクラスは、複数の優良顧客ルールを保持し、そのルールをすべて満たすかどうかを判定します。

ExcellentCustomerPolicyクラスは、@rulesというインスタンス変数でルールの集合を管理しています。

# 優良顧客の方針を表現するクラス
class ExcellentCustomerPolicy
  def initialize
    @rules = Set.new # @rulesを空の**Set**オブジェクトで初期化
  end

  # ルールを追加する
  def add(rule)
    @rules.add(rule) # 引数として与えられたルールを**@rules**に追加
  end

  # 全てのルールを満たすか判定する
  def comply_with_all?(history)
    @rules.all? { |rule| rule.ok?(history) } # 各ルールに対して呼び出し全てのルールが満たされているか
 end
end
作成したPolicyの使い方

ExcellentCustomerPolicyを作成し、ゴールド会員の条件判定

gold_customer_policy = ExcellentCustomerPolicy.new
gold_customer_policy.add(GoldCustomerPurchaseAmountRule.new)
gold_customer_policy.add(PurchaseFrequencyRule.new)
gold_customer_policy.add(ReturnRateRule.new)

# purchaseHistoryを条件判定にかける
if gold_customer_policy.comply_with_all?(purchase_history)
  # ゴールド会員の処理
  puts "この顧客はゴールド会員です。"
else
  # ゴールド会員ではない顧客の処理
  puts "この顧客はゴールド会員ではありません。"
end

上記ノ通りif文が一つだけとなりロジックが単純化しました。

ただ、この書き方でどこかのクラスにベタ書きしてしまうと、ゴールド会員以外の無関係なロジックを挿し込まれる可能性があるためまだ不安定な構造です。

会員ごとのポリシーを作成しロジックを集約する

ゴールド会員の条件をまとめたクラス

ExcellentCustomerPolicyインスタンスを作成し、そのインスタンスGoldCustomerPurchaseAmountRulePurchaseFrequencyRuleReturnRateRuleインスタンスを追加しています。

これにより、ゴールド会員の判定条件が設定されます。

# ゴールド会員の方針を表現するクラス
class GoldCustomerPolicy
  def initialize
    @policy = ExcellentCustomerPolicy.new
    @policy.add(GoldCustomerPurchaseAmountRule.new)
    @policy.add(PurchaseFrequencyRule.new)
    @policy.add(ReturnRateRule.new)
  end

  # 条件判定を行うメソッド
  def comply_with_all?(history)
    @policy.comply_with_all?(history)
  end
end
シルバー会員の条件をまとめたクラス

SilverCustomerPolicyの初期化メソッドでは、ExcellentCustomerPolicyインスタンスを作成し、そのインスタンスPurchaseFrequencyRuleReturnRateRuleインスタンスを追加しています。

これにより、シルバー会員の判定条件が設定されます。

# シルバー会員の方針を表現するクラス
class SilverCustomerPolicy
  def initialize
    @policy = ExcellentCustomerPolicy.new
    @policy.add(PurchaseFrequencyRule.new)
    @policy.add(ReturnRateRule.new)
  end

  # 条件判定を行うメソッド
  def comply_with_all?(history)
    @policy.comply_with_all?(history)
  end
end

今後それぞれの会員の条件に変更があれば、このGoldCustomerPolicyやSilverCustomerPolicyだけ変更すれば良くなります。

参考

続く…

コメント

本記事の内容は以上になります!

書籍の続きのアウトプットも随時更新したいと思います。


プログラミングスクールのご紹介 (卒業生より)

お世話になったプログラミングスクールであるRUNTEQです♪

https://runteq.jp/r/ohtFwbjW

こちらのリンクを経由すると1万円引きになります。

RUNTEQを通じて開発学習の末、受託開発企業をご紹介いただき、現在も双方とご縁があります。

もし、興味がありましたらお気軽にコメントか、TwitterのDMでお声掛けください。

https://twitter.com/outputky