業務でPythonを使ってウェブアプリケーションを実装する際、レイヤー毎に関心の分離を行いながら開発するために、Clean Architectureを導入することになりました。
チームメンバーへのナレッジ共有を兼ねて、漸進的型付けとDependency Injectionを用いながら、テスタビリティの伴ったアプリケーションを開発するためのプラクティスをまとめました。
Clean Architecture
今回はPythonを用いたサンプルを目的としているため、Clean Architectureの解説は簡易に済ませます。
(The Clean Architectureより引用)
Clean Architectureはロバート・C・マーティンによって2012年に提唱されたアーキテクチャで関心の分離を達成するための設計手法です。同じ狙いを持ったアーキテクチャとして、ヘキサゴナルアーキテクチャ(2005年に提唱)、オニオンアーキテクチャ(2008年に提唱)などがあります。
(Clean Architectureだけに特徴的ではありませんが)ビジネスロジックの明確化、UIやDB・フレームワークからの独立、テスタビリティの向上などが主なメリットです。
なお原文に書かれている通り、同心円のサンプルに通りのレイヤーに切る必要はなく、必要に応じてレイヤーを増減させることもできますが、今回は教科書どおりにレイヤーを切ることにしました。
typingを活用した漸進的型付け
昨今では動的型付け言語でも漸進的型付け(Gradual Typing)が定着してきましたが、Pythonもtyping
を使うことで開発時に型の恩恵を受けることができます。
TypeScriptのようにビルド時に型エラーが検出されませんが、PyCharm(IntelliJ IDEA)のようなIDEを使えば型の恩恵を受けながら開発できるので、静的型付けと遜色ない開発体験が実現できます。
Clean Architectureは依存性逆転の法則(Dependency Inversion Principle)を取り入れており、インターフェースを通じた操作を実現するためにも、typing
の利用は欠かせないでしょう。
抽象クラスを活用した依存性注入
Pythonにはインターフェースはないものの、抽象クラスがあります。抽象クラスには実装をもたせられますが、依存性逆転の法則で重要なのは抽象に依存することなので、抽象クラスでもその要件を達成することができます。
実際にUsecase層が呼び出すRepository層を、抽象クラスを通じて依存性逆転を行ってみましょう。
class SampleRepository(metaclass=ABCMeta):
@abstractmethod
async def get(self, resource_id: str) -> dict:
raise NotImplementedError
class SampleRepositoryImpl(SampleRepository):
async def get(self, resource_id: str) -> dict:
return {"id": id}
class SampleUsecase:
sample_repository: SampleRepository
def __init__(self, repository: SampleRepository):
self.sample_repository = repository
def get(self, resource_id: str) -> dict:
return asyncio.run(self.sample_repository.get(resource_id))
SampleUsecase(repository=SampleRepositoryImpl()).get()
この時Usecase層が知っているRepositoryは抽象クラスであり、その実装を知りません。また抽象クラスに実装を持たせることはできますが、そうすれば依存性逆転の法則に反してしまうので、@abstractmethod
を付与することで、具象クラスに実装を持たせるようにしています。
またClean ArchitectureのキモとなるDependency Injectionですが、DIコンテナを使わなくても、いわゆるバニラDIで依存性を注入することができます。(もちろんDIコンテナが不要というわけではありません)
依存性を注入する際にはtypingによる型チェックが強力で、IDEのサポートを受ければ動的型付け言語による不都合はほとんど感じないはずです。
モックを活用した単体テスト
Clean Architectureの利点の一つにテスタビリティがあります。Dependency Injectionをしていることも相まって、パーシャルモックが容易に行えます。またPython標準のunittest
モジュールの強力な機能もぜひ活用しましょう。
class SampleRepositoryMock(SampleRepository):
async def get(self, resource_id: str) -> dict:
raise NotImplementedError
class TestSampleUsecase(TestCase):
def test_get(self):
get_mock = AsyncMock(return_value={"id": "0002"})
repository = SampleRepositoryMock()
repository.get = get_mock
usecase = SampleUsecase(repository=repository)
self.assertEqual(usecase.get("0002"), {"id": "0002"})
get_mock.assert_called_with("0002")
パーシャルモックとしてMagicMockが提供されており、これを使えばメソッドの差し替えが簡単に行えるだけでなく、呼び出されたことを確認するverifyも実行できます。
さらにawaitableな返り値としてAsyncMock
も用意されており、これを利用すればネイティブコルーチンを返すための暫定的なasync def
や、低レベルなFuture
型の変数を生成する必要がありません。外部への通信を行う場合、昨今のPythonではasync/await
を使ったネイティブコルーチンが使われることが多いですが、これも標準モジュールのみで対応可能です。
そしてテスト時にはDependency Injectionを活用して、Usecase層にテスト用となるダミーのRepositoryを差し込み、テストするレイヤーのみの動作検証を行うようにしています。
ちなみにダミークラスを用意せず、実際に使われるクラスインスタンスを注入した上で、パーシャルモックを使って差し替えを行っても同様のことが実現できます。これについてはどちらが正解ということはなく、テスト対象のレイヤーのみにフォーカスしたいのであれば、ダミークラスを注入する方が望ましく、レイヤーを跨いでテストするのであればダミーは必要ありません。
なぜ前者の場合ダミーであるのが望ましいのかというと、モックを忘れてた場合にテストが通るかは依存するレイヤーに左右され、モックを使わなければ例外が発生するダミークラスの方がそれを確実に検出することができるためです。
Entity層でdataclassを利用する
Clean ArchitectureではビジネスロジックをEntity層に持たせます。EntityというとDDDが思い出されますが、厳密にDDDにおけるEntityと同じではなく、またValue Objectとも同じではありません。
原文によると「Entityはビジネスルールをカプセル化し、データ構造と関数による集合である」とされています。
Entities encapsulate Enterprise wide business rules. An entity can be an object with methods, or it can be a set of data structures and functions. It doesn’t matter so long as the entities could be used by many different applications in the enterprise.
(The Clean Architectureより引用)
厳格にentityにどのような性質を持たせるのか規定はありませんが、個人的にはPython 3.7から導入されたdataclassとの相性が良いと考えています。
@dataclass
class Article(frozen=True, eq=True)
id: str
body: str
article1 = Article(id="1", body="test")
article2 = Article(id="1", body="test")
# 値による同一性検証でTrueになる(デフォルトでeq=Trueが設定済み)
article1 == article2
# frozenで不変オブジェクトになっているのでエラーになる
article1.id = "2"
dataclass
自身が持つ機能はそれだけで便利ですが、とりわけ不変条件を持たせるためのfrozen
、値による同一性検証としてのeq
といったプロパティは、Entity層に期待される動作です。(もちろん不変性や値比較可能であることが必ず求められているわけではありません)
またEntity層に限らずとも、単体テストでオブジェクトの同一性検証をする際、__eq__
メソッドを逐次定義するのは手間ですし、dataclass
を利用するメリットは大きいでしょう。
サンプルをGithubに公開しました
記事内に全てのサンプルコードを掲載すると冗長になってしまうので、実際に動作するシンプルなウェブアプリケーションのソースをGithubに公開しました。
WebフレームワークとしてFlaskを利用していますが、Clean Architectureの思想に則り、ライブラリに依存しているのはRest層のみで、それ以外はほぼバニラなPythonコードになっています。
単体テストのコードも付属しているので、もし興味があればチェックしてみて下さい。
最後に
Pythonは機械学習による人気向上に伴って、近年では最も人気のある言語の一つであり、それに伴いウェブ領域での活用も増えているように見受けられます。
Gradual typingやdataclassの登場によって元来、動的型付け言語では開発体験が損なわれていた部分が十分にカバーできており、また従来の生産性の高さも加わって、ウェブアプリケーションとしても魅力的な選択肢になっています。
私自身、Pythonをプロダクションレベルで書いて数ヶ月というレベルですが、他言語で培った経験を活かせるので、これからPythonをやってみたいという方の参考になれば幸いです。