Open6

Pythonについて

naoki matsuzakinaoki matsuzaki

例えば、EC サイトのユーザー会員として

  • 通常会員
  • プレミアム会員
  • 株主優待会員

がいたとします。
これらは、User クラスをベースにし、それぞれで会員が購入した時ポイント取得率がが異なります。

あくまでユーザーという括りでは同じですので、クラスを継承して作成するのが良いと思うが、クラスより抽象基底クラスを利用するのが良い。

理由としては、下記。

a. インターフェースの明確化: ABC は、サブクラスに対して実装が必要なメソッドやプロパティを指定することができます。これにより、ABC を継承したすべてのサブクラスは、特定のメソッドやプロパティを実装しなければならないことが保証される。これにより、インターフェースの明確化とコードの一貫性が向上する。

b. ポリモーフィズムの促進: ABC を使用することで、異なるクラスが同じインターフェースを実装することが保証される。これにより、ポリモーフィックなコードを書くことが容易になる。例えば、ABC を継承する複数のクラスを受け取る関数を作成することができ、それらのクラスが同じインターフェースを持つことが保証されるため、同じ関数を利用して多様なクラスを扱うことが可能。

c. 型の確認: ABC は、特定のクラスが特定の抽象基底クラスを継承しているかどうかを簡単に確認する機能を提供。これにより、タイプチェックを行って意図しないクラスの渡されることを防ぐことが可能。

象基底クラスは、特に Python のような動的型付け言語で、型の安全性やポリモーフィズムの実現に重要な役割を果たす。

Python で interface を実装するためには、抽象基底クラスを作る。
Python では抽象基底クラスを実装するための標準ライブラリとして、abc が提供されている。
https://docs.python.org/ja/3/library/abc.html

下記のようにすることでインターフェースを実装できる。

from abc import ABCMeta, abstractmethod


class User(metaclass=ABCMeta):
    @abstractmethod
    def get_point(self, rate: int) -> int:
        raise NotImplementedError

    @abstractmethod
    def get_user(self, user_id):
        raise NotImplementedError

このインターフェースでは、@abstractmethodとして、get_point と get_user メソッドがある。
この二つのメソッドを User クラス(インターフェース)を継承したクラスには実装しなければならない。

例えばこのように未実装の場合

class StandardUser(User):
    def __init__(self, name) -> None:
        self.name = name

    def user(self):
        return self.name

if __name__ == "__main__":
    standard_user = StandardUser("tanaka")
    standard_user.user

実行すると下記エラーとなる。

Traceback (most recent call last):
  File "/abstract.py", line 23, in <module>
    standard_user = StandardUser("tanaka")
TypeError: Can't instantiate abstract class StandardUser with abstract methods get_point, get_user

User クラスを利用するクラスには必ず実装しなければならない。
これも一つの品質の良いコードを書くためのメリットともなる。

下記がインターフェースを利用したコードです。

from abc import ABCMeta, abstractmethod


class User(metaclass=ABCMeta):
    @abstractmethod
    def get_point(self, price: int) -> int:
        raise NotImplementedError

    @abstractmethod
    def get_user(self):
        raise NotImplementedError


class StandardUser(User):
    def __init__(self, name) -> None:
        self.name = name
        self.rate = 0.5

    def get_point(self, price: int) -> int:
        print(price * self.rate)
        return price * self.rate

    def get_user(self):
        print(self.name)
        return self.name

    # 通常会員だけが使えるメソッドを定義可能(ここでは税込価格を取得)
    def get_product_price(self, price):
        print(price + self.get_point(price))
        return price + self.get_point(price)


class PremiumUser(User):
    def __init__(self, name) -> None:
        self.name = name
        self.rate = 0.65

    def get_point(self, price: int) -> int:
        print(price * self.rate)
        return price * self.rate

    def get_user(self):
        print(self.name)
        return self.name


class ShareholderUser(User):
    def __init__(self, name) -> None:
        self.name = name
        self.rate = 0.8

    def get_point(self, price: int) -> int:
        print(price * self.rate)
        return price * self.rate

    def get_user(self):
        print(self.name)
        return self.name


if __name__ == "__main__":
    standard_user = StandardUser("tanaka")
    standard_user.get_point(5000)
    standard_user.get_user()

    # 下記は税込価格を取得
    standard_user.get_product_price(5000)

    standard_user = PremiumUser("sato")
    standard_user.get_point(5000)
    standard_user.get_user()

    standard_user = ShareholderUser("ito")
    standard_user.get_point(5000)
    standard_user.get_user()

naoki matsuzakinaoki matsuzaki

先ほどのインターフェース設計にファクトリーパターンを適用させる。

ファクトリーパターンとはデザインパターンの一種で Python 以外にも使われる。

クラスのインスタンス化のロジックを特定の「ファクトリ」クラスまたはメソッドに委譲することで、コードの柔軟性と再利用性を向上させるためのパターン。

特に、クライアント(オブジェクトを要求する部分)が必要とするオブジェクトの具体的なクラスを知ることなく、さまざまな種類のオブジェクトを作成する際に役立ちます。

先ほどのインターフェース設計では利用側でクラスを指定してインスタンス化し、それを利用してた。

    standard_user = StandardUser("tanaka")
    standard_user.get_point(5000)
    standard_user.get_user()

StandardUser を作ることが利用側でわかっているため、そのクラスをインスタンス化していますが、ファクトリーパターンでは、そのインスタンス化を委譲させることになる。

このメリットとしては下記。

  • クライアントは具体的なクラスを知らず、抽象的なインターフェースまたは抽象クラスに対してプログラミングする。これにより、クライアントのコードは具体的なクラスから切り離され、疎結合になる
  • ファクトリメソッドやファクトリクラスがオブジェクトの生成を担当。これにより、オブジェクトの生成ロジックを一箇所にまとめることができ、コードの見通しを良くすることが可能
  • 新しい種類のオブジェクトを追加する場合でも、クライアントコードを変更する必要がないため、拡張性が向上する

ファクトリーパターンはインターフェースに基づいてオブジェクトを作成するため、両者は一緒に使用される。

例えば下記のようにファクトリクラスを用意する

class UserFactory(object):
    def __init__(self):
        self._users = {
            "StandardUser": StandardUser,
            "PremiumUser": PremiumUser,
            "ShareholderUser": ShareholderUser,
        }

    def create_user(self, user_type, name):
        if user_type not in self._users:
            raise ValueError(f'Unknown user "{user_type}"')
        return self._users[user_type](name)

利用側は下記で、インスタンス作成をファクトリクラスに移譲する

    factory = UserFactory()
    user = factory.create_user("StandardUser", "tanaka")
    user.get_user()

つまり、利用側でインスタンス化するのではなく、インスタンス化を委譲させることで、コードから切り離しができるのはとても大きい。

naoki matsuzakinaoki matsuzaki

もう一歩踏み込んで、クラス作成をインターフェースに移譲することも可能

ファクトリクラスを用意するのではなく、インターフェース側でクラスからのインスタンス作成メソッドを static で置いておく。

そして、 _types に全てのクラスを登録しておき、それを使ってインスタンス化生成コマンドを呼ぶ

from abc import ABCMeta, abstractmethod


class User(metaclass=ABCMeta):
    _types = {}

    @staticmethod
    def register(user_type, cls):
        User._types[user_type] = cls

    @staticmethod
    def create(user_type, name):
        if user_type not in User._types:
            raise ValueError(f'Unknown user "{user_type}"')
        return User._types[user_type](name)

    @abstractmethod
    def get_point(self, price: int) -> int:
        raise NotImplementedError

    @abstractmethod
    def get_user(self):
        raise NotImplementedError


class StandardUser(User):
    def __init__(self, name) -> None:
        self.name = name
        self.rate = 0.5

    def get_point(self, price: int) -> int:
        print(price * self.rate)
        return price * self.rate

    def get_user(self):
        print(self.name)
        return self.name

    # 通常会員だけが使えるメソッドを定義可能
    def get_product_price(self, price):
        print(price + self.get_point(price))
        return price + self.get_point(price)


class PremiumUser(User):
    def __init__(self, name) -> None:
        self.name = name
        self.rate = 0.65

    def get_point(self, price: int) -> int:
        print(price * self.rate)
        return price * self.rate

    def get_user(self):
        print(self.name)
        return self.name


class ShareholderUser(User):
    def __init__(self, name) -> None:
        self.name = name
        self.rate = 0.8

    def get_point(self, price: int) -> int:
        print(price * self.rate)
        return price * self.rate

    def get_user(self):
        print(self.name)
        return self.name


User.register("StandardUser", StandardUser)
User.register("PremiumUser", PremiumUser)
User.register("ShareholderUser", ShareholderUser)


if __name__ == "__main__":
    user = User.create("StandardUser", "tanaka")
    user.get_user()

naoki matsuzakinaoki matsuzaki

ポリシーパターンについて

ポリシーパターンは、行動またはアルゴリズムをカプセル化し、それをオブジェクトとして扱うデザインパターン。

このパターンを使用すると、特定のタスクを実行するための戦略または方針(ポリシー)を、コードの実行時に動的に変更したり交換できる。

ポリシーパターンは主に柔軟性と再利用性を提供することで、コードのメンテナンスを容易にします。

from abc import ABCMeta, abstractmethod


# 戦略を定義するための抽象基底クラス
class AbstractPolicy(metaclass=ABCMeta):
    @abstractmethod
    def action(self):
        pass


# 具体的な戦略1
class ConcretePolicy1(AbstractPolicy):
    def action(self):
        return "Performing action in policy1"


# 具体的な戦略2
class ConcretePolicy2(AbstractPolicy):
    def action(self):
        return "Performing action in policy2"


# コンテキストクラス
class Context:
    def __init__(self, policy):
        self._policy = policy

    def set_policy(self, policy):
        self._policy = policy

    def do_action(self):
        return self._policy.action()


# クライアントコード
if __name__ == "__main__":
    policy1 = ConcretePolicy1()
    context = Context(policy1)
    print(context.do_action())  # Outputs: Performing action in policy1

    policy2 = ConcretePolicy2()
    context.set_policy(policy2)
    print(context.do_action())  # Outputs: Performing action in policy2

これだけ見てもファクトリパターンとポリシーパターンの違いがよくわからない。。

ファクトリーパターンは、作るオブジェクトを切り替えれるようにしている。(インスタンス化は委譲されている)

一方、ポリシーパターンは、作るオブジェクトは一つであるが、その振る舞いを切り替えれるようにしている。(アルゴリズムや振る舞いをカプセル化)
こうすることで具体的な実装を切り替えれることが可能となる。

振る舞いをカプセル化するか、オブジェクトをカプセル化するかの違い

AnimalFactoryが異なる種類のAnimalオブジェクトを作成し、それぞれのAnimalオブジェクトは異なるSoundBehaviour(音の振る舞い)を持つことを示す。

from abc import ABC, abstractmethod

# Strategy interface
class SoundBehaviour(ABC):
    @abstractmethod
    def sound(self):
        pass

class BarkBehaviour(SoundBehaviour):
    def sound(self):
        return "Woof!"

class MeowBehaviour(SoundBehaviour):
    def sound(self):
        return "Meow!"

# Product interface
class Animal(ABC):
    def __init__(self, sound_behaviour):
        self.sound_behaviour = sound_behaviour

    def make_sound(self):
        return self.sound_behaviour.sound()

# Concrete product classes
class Dog(Animal):
    def __init__(self):
        super().__init__(BarkBehaviour())

class Cat(Animal):
    def __init__(self):
        super().__init__(MeowBehaviour())

# Factory class
class AnimalFactory:
    def create_animal(self, type):
        if type == "Dog":
            return Dog()
        elif type == "Cat":
            return Cat()
        else:
            raise ValueError("Invalid type")

# Client code
factory = AnimalFactory()
animal = factory.create_animal("Dog")
print(animal.make_sound())  # Outputs: Woof!

naoki matsuzakinaoki matsuzaki

下記については別途勉強する

  • Singletonパターン
  • Observerパターン
  • Decoratorパターン

Singletonパターン: このパターンは、あるクラスに対してそのインスタンスが1つしか存在しないことを保証。
これは、グローバルな状態(例えば、設定情報など)を管理するのに便利です。

Factoryパターン: ファクトリーパターンはオブジェクトの作成を専門化したクラスやメソッドに委譲。これにより、コードは具体的なクラスではなく抽象的なインターフェースに依存するようになり、より柔軟で再利用可能な設計になる。

Strategyパターン(ポリシーパターン): ストラテジーパターンはオブジェクトの振る舞いをカプセル化し、それを実行時に交換することが可能にする。これにより、コードの柔軟性が高まる。

Observerパターン: このパターンは、あるオブジェクトの状態が変更されたときに他のオブジェクトに通知するために使用される。これにより、オブジェクト間の依存関係を最小限に抑えつつ、状態の変更を効率的に伝播することが可能になる。

Decoratorパターン: デコレーターパターンは、既存のオブジェクトに動的に新しい機能を追加するためのパターン。Pythonのデコレータはこのパターンを直接サポートしており、関数やメソッドに対して追加の振る舞いを追加することが可能。

Observerパターンでは、setterについても確認する
https://qiita.com/s_ryota/items/2ff8473778c0c5f85a3e