先日あるプロジェクトで、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を記述することができました。
注意事項
-
個人情報に該当する箇所は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')
-
例えばテナント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()