コードに意味を与える技術

仕様パターンが語るビジネスルールの意図 - 変更容易性を高める条件表現の技術

Tags: デザインパターン, 仕様パターン, ビジネスロジック, 可読性, 保守性, Python

はじめに:ビジネスルールのコード化における課題

ソフトウェア開発において、ビジネスルールはアプリケーションの根幹をなす要素です。しかし、これらのルールをコードに落とし込む際に、その意図が不明瞭になりがちな課題があります。

例えば、「特定の条件を満たすユーザーに対して割引を適用する」といったビジネスルールがあったとします。これをコードで表現する際に、複数の条件が複雑に組み合わさることで、以下のような問題が発生しやすくなります。

これらの問題は、コードが「何をしたいか(意図)」ではなく、「どうやって判定するか(実装詳細)」に寄りすぎてしまうために起こります。本記事では、この課題を解決し、ビジネスルールの意図をコードでより明確に表現するための「仕様パターン(Specification Pattern)」について解説します。

仕様パターンとは何か

仕様パターンは、エリック・エヴァンスの「ドメイン駆動設計」などで紹介されているパターンの一つです。特定のオブジェクトがある条件(仕様)を満たすかどうかを判定するロジックを、その判定を行うオブジェクトやロジックから分離し、独立したオブジェクト(仕様オブジェクト)として表現します。

このパターンの主な目的は以下の通りです。

  1. 意図の明確化: ビジネスルールとしての「仕様」そのものを名前を持つオブジェクトとして定義することで、コードがその仕様を満たすかどうかを判定していることが明確になります。
  2. 再利用性の向上: 一度定義した仕様は、複数の場所で再利用できます。
  3. 組み合わせ可能性: 仕様は論理演算(AND, OR, NOT)によって組み合わせることができ、複雑な仕様をより小さな基本仕様の組み合わせとして表現できます。
  4. テスト容易性: 各仕様オブジェクトは単体でテストが容易です。

つまり、仕様パターンはビジネスルールの「何を(What)」という意図を、コード上で独立した構成要素として表現するための強力な手段なのです。

仕様パターンでビジネスルールの意図を伝える

仕様パターンを適用する基本的な流れは以下のようになります。

  1. 「仕様」を表すインターフェース(または抽象基底クラス)を定義します。これには通常、対象オブジェクトを受け取り真偽値を返すメソッド(例: is_satisfied_by(candidate))が含まれます。
  2. 各ビジネスルール(またはその構成要素)に対応する仕様クラスを作成し、上記のインターフェースを実装します。クラス名そのものが仕様の意図を表現します。
  3. 必要に応じて、複数の仕様を組み合わせるための論理AND、OR、NOTを実装した仕様クラスを作成します。
  4. 判定を行いたい場所では、これらの仕様オブジェクトをインスタンス化し、is_satisfied_byメソッドを呼び出して判定を行います。

具体的なコード例を見てみましょう。ここではPythonを使用します。

Before: 条件分岐が複雑なコード

例えば、あるユーザーが「プロモーションAの対象者」であるかを判定するロジックを考えます。その条件は「アクティブなユーザー」かつ「プレミアム会員である」か、または「初回購入者である」とします。

from datetime import datetime, timedelta

class User:
    def __init__(self, name, is_active, is_premium, first_purchase_date, registration_date, last_login):
        self.name = name
        self.is_active = is_active
        self.is_premium = is_premium
        self.first_purchase_date = first_purchase_date # datetime or None
        self.registration_date = registration_date # datetime
        self.last_login = last_login # datetime

    def is_first_purchaser(self):
        # 仮に登録から7日以内の購入があれば初回購入とみなす
        if self.first_purchase_date is None:
            return False
        return self.first_purchase_date <= self.registration_date + timedelta(days=7)

def is_eligible_for_promotion_a(user: User) -> bool:
    # 条件: (アクティブ かつ プレミアム会員) または (初回購入者)
    if (user.is_active and user.is_premium) or user.is_first_purchaser():
        return True
    else:
        return False

# 使用例
user1 = User("Alice", True, True, datetime.now(), datetime.now(), datetime.now())
user2 = User("Bob", True, False, None, datetime.now(), datetime.now())
user3 = User("Charlie", False, True, datetime.now() - timedelta(days=10), datetime.now() - timedelta(days=20), datetime.now())

print(f"Alice eligibility: {is_eligible_for_promotion_a(user1)}") # True
print(f"Bob eligibility: {is_eligible_for_promotion_a(user2)}")   # False
print(f"Charlie eligibility: {is_eligible_for_promotion_a(user3)}") # True (初回購入者条件)

このコードでは、is_eligible_for_promotion_a 関数内に条件が直接記述されています。単純な条件ですが、これがさらに複雑化したり、他の場所で似た条件が必要になったりすると、可読性や保守性がすぐに低下します。条件の「意図」がコードの構造から直接読み取りにくい状態です。

After: 仕様パターンを適用したコード

仕様パターンを適用して、上記のロジックを改善してみましょう。まず、仕様を表すインターフェースと論理結合のための抽象クラスを定義します。

import abc
from datetime import datetime, timedelta

# 仕様インターフェース
class Specification(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def is_satisfied_by(self, candidate) -> bool:
        pass

    # 仕様の組み合わせ (AND, OR, NOT) を提供する
    def and_(self, other):
        return AndSpecification(self, other)

    def or_(self, other):
        return OrSpecification(self, other)

    def not_(self):
        return NotSpecification(self)

# 論理AND仕様
class AndSpecification(Specification):
    def __init__(self, spec1: Specification, spec2: Specification):
        self._spec1 = spec1
        self._spec2 = spec2

    def is_satisfied_by(self, candidate) -> bool:
        # spec1 かつ spec2 を満たす
        return self._spec1.is_satisfied_by(candidate) and self._spec2.is_satisfied_by(candidate)

# 論理OR仕様
class OrSpecification(Specification):
    def __init__(self, spec1: Specification, spec2: Specification):
        self._spec1 = spec1
        self._spec2 = spec2

    def is_satisfied_by(self, candidate) -> bool:
        # spec1 または spec2 を満たす
        return self._spec1.is_satisfied_by(candidate) or self._spec2.is_satisfied_by(candidate)

# 論理NOT仕様
class NotSpecification(Specification):
    def __init__(self, spec: Specification):
        self._spec = spec

    def is_satisfied_by(self, candidate) -> bool:
        # spec を満たさない
        return not self._spec.is_satisfied_by(candidate)

# ユーザーに関する具体的な仕様クラス
class IsActiveUser(Specification):
    def is_satisfied_by(self, user: User) -> bool:
        return user.is_active

class IsPremiumUser(Specification):
    def is_satisfied_by(self, user: User) -> bool:
        return user.is_premium

class IsFirstPurchaser(Specification):
    def is_satisfied_by(self, user: User) -> bool:
        # Userクラスのメソッドを再利用しても良いが、仕様として定義することも可能
        # ここではUserクラスのメソッドを利用する例
        return user.is_first_purchaser()

# プロモーションAの対象者仕様を定義
# 条件: (アクティブ かつ プレミアム会員) または (初回購入者)
is_active = IsActiveUser()
is_premium = IsPremiumUser()
is_first_purchaser = IsFirstPurchaser()

# 仕様を組み合わせる
is_eligible_for_promotion_a_spec = (is_active.and_(is_premium)).or_(is_first_purchaser)

# 使用例(Beforeと同じUserクラスとインスタンスを使用)
# ... (Userクラスとuser1, user2, user3の定義は省略) ...

print(f"Alice eligibility: {is_eligible_for_promotion_a_spec.is_satisfied_by(user1)}") # True
print(f"Bob eligibility: {is_eligible_for_promotion_a_spec.is_satisfied_by(user2)}")   # False
print(f"Charlie eligibility: {is_eligible_for_promotion_a_spec.is_satisfied_by(user3)}") # True

このAfterコードでは、各条件(アクティブである、プレミアム会員である、初回購入者である)がそれぞれ IsActiveUserIsPremiumUserIsFirstPurchaser という名前を持つ仕様クラスとして定義されています。そして、プロモーションAの対象者というビジネスルールは、これらの基本仕様を and_or_ といったメソッドで組み合わせた is_eligible_for_promotion_a_spec オブジェクトとして表現されています。

コードを読む人は、is_eligible_for_promotion_a_spec が「アクティブかつプレミアム会員である仕様」と「初回購入者である仕様」をORで組み合わせたものであることを、コードの構造と命名から直感的に理解できます。これにより、複雑な条件式の意図が明確にコードで語られるようになります。

仕様パターンがコードの意図伝達に与える効果

仕様パターンの適用は、以下のようにコードの意図を伝える上で大きな効果をもたらします。

陥りやすいアンチパターンと注意点

仕様パターンは強力ですが、誤った使い方をするとかえってコードを分かりにくくする可能性もあります。

まとめ

仕様パターンは、複雑になりがちなビジネスルールをコードで明確に表現し、その意図を正確に伝えるための有効な手段です。条件判定ロジックを名前を持つ独立したオブジェクトとして定義し、それらを組み合わせることで、可読性、保守性、再利用性、テスト容易性の高いコードを実現できます。

単に条件式を記述するだけでなく、「この条件を満たすのは、こういう意図を持つ対象である」という視点でコードを設計することで、後続の開発者やコードレビューを行うチームメンバーに、より深いレベルでコードの「なぜ」や「何を」を伝えることができるようになります。

あなたのプロジェクトで複雑な条件分岐に直面した際は、仕様パターンの適用を検討し、ビジネスルールの意図をコード自身に語らせてみてはいかがでしょうか。