LoginSignup
5
8

More than 3 years have passed since last update.

Python3.7でCrean Architectureを実現する

Last updated at Posted at 2019-09-15

先日あるプロジェクトで、python 3.7を利用したクリーンアーキテクチャの設計を行いました。

その時に得た知見を紹介したいと思います。

基本的に、この記事ではDDDに関しての説明はこの連載を参考していきます。

Domainモデル

ValueObject

参考記事によるとValueObjectでは、次の6つの要素を持つものである、と定義されています。

No 値オブジェクトの特徴 説明
1 計測/定量化/説明 ドメイン内の何かを計測したり定量化したり説明したりする
2 不変性 状態を不変に保つことができる
3 概念的な統一体 関連する属性を不可欠な単位として組み合わせることで、概念的な統一体を形成する
4 交換可能性 計測値や説明が変わったときには、全体を完全に置き換えられる
5 等価性 値が等しいかどうかを、他と比較できる
6 副作用のない振る舞い 協力関係にあるその他の概念に「副作用のない振る舞い」を提供する

python 3.7からはValue Objectを作成する際にdataclassを利用するのが最もシンプルに記載できます。

from dataclasses import dataclass

@dataclass(frozen=True)
class MonetaryValue():
    amount: Decimal
    currency: str

dataclassを利用することで、先程の特性の2. 編成、4. 交換可能性、5. 透過性、6. 副作用のない振る舞いを手に入れることができます。 1. の計測/定量化/説明及び3. の概念的な統一体は設計に依存する箇所であるため、これでValueObjectを記述することができました。

注意事項

  1. 個人情報に該当する箇所はreprで出力されないようにマスクする

    例えば次のようなクラスがあった場合に、何かの拍子にパスワードがログに書かれて、更に何かの拍子に流出する、という事がないように、ハッシュ化前のパスワード等の重要なものはログに出力されないようにしましょう。

    from dataclasses import dataclass, field
    
    @dataclass(frozen=True)
    class UserPassword():
        name: str
        password: str = field(repr=False)
    
    user = UserPassword(name="foo", password="bar")
    print(user)
    
    >>> UserPassword(name='foo')
    
  2. 例えばテナントIDのような、単一の値を持つValueObjectを作成する場合は、継承と委譲と2種類の実装があります。

    継承の場合:

    class TenantId(str):
        @classmethod
        def __new__(cls, value):
            # do some validation
            return cls(value)
    

    委譲の場合:

    class TenantId():
        def __init__(self, value):
            super().__init__()
            this.__value = value
    
        # 他に必要なメソッドをvalueからdelegateする(色々)
        def __eq__(self, value):
            ...
    

    委譲のほうが設計としては良いのですが、記述は継承のほうがシンプルです

Entity

Entityも基本的には同じで設計可能ですが、dataclassでは特定のフィールドだけ書き換え不可能、という書き方ができません。
そのため、idの不変性を持つためのEntity classを作成し、それを継承してEntityを作るようにします。

from dataclasses import dataclass, field

@dataclass
class Entity():
    id: str = field(compare=True)

    def __setattr__(self, name, value):
        if (name == 'id' and hasattr(self, name) ):
            raise SystemError()

        super().__setattr__(name, value)


@dataclass
class User(Entity):
    name: str = field(compare=False)


jon = User(id='foo', name='jon')
pochi = User(id='foo', name='pochi')

jon == pochi

>>> True

これにより、idに初期値を代入後再代入することが不可能となるため、idの不変性を得ることができました。

と、楽したいためにここまでdataclassを利用して来ましたが、特にエンティティのValidationがうまくできないため、dataclassを利用しない実装も検討の余地がありそうです。

Dependency Injection

pythonは静的型付け言語ではないため、DIコンテナの利用は必須では無いです。しかし、クラスのインスタンス化の手間を考えると、DIコンテナを利用して依存性の解決を行う方が良いと思います。

Dependency Injectionのおすすめライブラリはinjectorです。シンプルな記述で必要最低限の機能を持つため、簡単にDIコンテナによる依存性注入を実現できます。

from injector import inject, singleton, Injector, Module
from abc import ABC, abstractmethod

class IARepo(ABC):
    @abstractmethod
    def get_user(self):
        raise NotImprementedError('not impremented')

@singleton
class ARepo(IARepo):
    def get_user(self):
        return User(id='aaa', name='bbb')

@singleton
class Usecase():
    @inject
    def __init__(self, repo: IARepo):
        super().__init__()
        self.__repo = repo

    def get_user(self):
        return self.__repo.get_user()

class InjectConfig(Module):
    def configure(self, binder):
        binder.bind(IARepo, to=ARepo, scope=singleton)

inj = Injector(InjectConfig())

usecase = inj.get(Usecase)
usecase.get_user()

DIを利用した単体テスト

また、DIコンテナを利用することで、例えばユースケース等、十分なテストを行いたいモジュールの単体テストも簡単にできるようになります。

from unittest.mock import create_autospec

def test_run():
    repo_mock = create_autospec(IARepo)
    usecase = Usecase(repo=repo_mock)

    user = User(id='baz', name='hoge')
    repo_mock.get_user.return_value = user

    result = usecase.get_user()
    assert result == user

test_run()
5
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
8