[Python] Django x 戦術的DDD の是非を考える
ひょんなことから「Django DDD」と検索してみた。ヒットしたいくつかのWebページをざっと読んだ感想として "Django で戦術的 DDD の実践" としてアーキテクチャパターンを採用するプラクティスに強い違和感を覚えた。
※本記事では「軽量DDD」もほぼ同義と見なす
SNS 上の言説と照らし合わせつつ、自分自身の理解を助けるための思考過程を記述する
[全て記述したあとの追記]
※この記事における "Django" を "MVC パターン" と読み替えても通じる文章が多いと思う。
一般的な戦術的 DDD の設計パターンはある種の「デザインパターン」であり、それぞれの背景にある課題やメリットを考えることが重要である。
課題へのアプローチは言語/フレームワークごとに異なり、同じ課題の解決手段を「ただ違う違う名前で呼んでいるだけ」という場合も多いと思う。
(戦術的)DDD の設計パターンやその目的とエッセンスを理解することは重要と思いつつ、実際の開発シーンへの適用はきちんと逐次考えるようにしていきたい。
Disclaim
本記事の主張の対比として示すリンクや言説を非難したり、その著者を個人攻撃する意図はありません。
あくまで "Django x DDD" に関する当方の考えを述べるものです。
前置き1/2:
自分は Laravel に詳しくないのだが、Django と同じ MVC パターンである Laravel は戦術的DDD に不向きらしい
前置き2/2:
kumagi 氏が言及している mond の回答。
レイヤードという観点で言うとRuby on Railsは天才的で、プロジェクトとして初期化した時点で走るコードの形でModel/View/Controllerが別のディレクトリに分けられてレイヤー構造が出来ています。
これは「Webアプリを作りたいならこういうアーキテクチャを取れ」というのをコードのレベルから押し付けており、逆にWebアプリ以外には使えません。
せっかく天才が傑作なアーキテクチャを提案してくれたのがRailsなのにそれにDDDを導入するという倒錯が世の中にあるそうで実に残念です。
...そこに劣った方の概念であるDDDを導入するのは、ガソリン車からエンジンとハンドルを取っ払って馬に引かせるような錯誤であると認知されて欲しいものです。
以上の前置きから「"Django に戦術的DDD を導入" に対する違和感」はあながち見当外れではなさそうに思える。
1. Repository パターン
一般的な定義を以下から引用する
インフラストラクチャの永続レイヤーの設計 - .NET | Microsoft Learn
リポジトリ パターンは、システムのドメイン モデルの外部で永続化の問題を維持することを目的としたドメイン駆動設計パターンです
1 つ以上の永続化抽象化 (インターフェイス) がドメイン モデルで定義されており、これらの抽象化には、アプリケーションの他の場所で定義されている永続化固有のアダプターの形式で実装があります。
感想: QuertSet でいいじゃん。
クエリを作成する | Django documentation | Django
つまり Django はフレームワークレベルで Repository パターンを採用しており、利用者にレールを敷いている。
このレールを無視して repositorioes/
という階層を設けるのはフレームワークの思想に沿っていないし、コンテキストが複雑になったりやコミュニケーションコストが大きくなる可能性がある。
また、デフォルトでも使える save()
や delete()
など基本的なCRUDを実装するのは明らかに車輪の再開発である。
Django を使う時点で最初からインフラ(DB)へのアクセスは抽象化されている。
例えばドメインモデル Blog
とそれに対応するデータベーステーブル blogs
があるとき、以下のように DB へのアクセスが抽象化される。
# blog/models.py
class Blog(models.Model):
name = models.CharField(max_length=100)
tagline = models.TextField()
>>> from blog.models import Blog
>>> b = Blog(name="Beatles Blog", tagline="All the latest Beatles news.")
>>> b.save()
save()
はレコードの作成と更新をサポートする。
また、他にも CRUD を行うのに十分なメソッドがデフォルトで用意されているし、必要に応じてカスタマイズ可能である。
QuerySet API リファレンス | Django documentation | Django
また、ドメイン駆動設計パターンを Python で紹介している以下の記事からリポジトリパターンの実装を引用する。
※この記事は Django を前提とした内容ではないので DDD の設計/実装パターンの紹介として有用なものである
【DDD】ドメイン駆動設計を自分なりにまとめてみる
class UserService:
def __init__(self, user_repository: IUserRepository):
self.user_repository = user_repository
def exists(self, user: User) -> bool:
found = self.user_repository.find_by_name(user.user_name)
return found is not None
実装例で定義、呼び出されているメソッドは Django QuerySet には元々用意されている。
上記コードの exists()
と find_by_name()
を Django ベースで記述すると以下のようになる。
# 1. exists()
User.objects.filter(name="skokado").exists()
# => True or False
# 2. find_by_name()
from django.core.exceptions import ObjectDoesNotExist
try:
user = User.objects.get(name="skokado")
except ObjectDoesNotExist:
user = None
...
感想: やっぱり QuerySet でいいじゃん。
実装例の引用元記事でつぶやいている
※ 個人的に存在確認の処理はリポジトリに実装してもいいと思うが。。。
も満たせているし、存在確認(= 一意性の担保)はデータモデル/DB の関心事だと思うのでやっぱり Django QuerySet だと感じる。
2. エンティティ
感想: モデル層でいいじゃん(= Django においては DB の1レコード)。
エンティティ(Entity)とは一般に、id によって一意性を識別する単位である。
Django では Model クラスのオブジェクトと読み替えれば定義に従う。
なぜなら「DB で主キーが割り当てられた(= 一意性が担保されている)1レコード」だからである。
from blog.models import Blog
blog = Blog.object.get(name="John's blog")
print(blog.id) # => 1
another_blog = Blog.object.get("Bob's blog")
print(another_blog.id) # => 2
blog == another_blog
# => False (当たり前)
Django における戦術的 DDD の実践として "Model 層とは別の Entity 層" を用意することには反対である。
Model 層でドメインモデルを表すクラスが定義されるはずで、関心事がかなり重複したクラスになる可能性が高いだろう(違いといえば id の有無くらい?)。
「Django + DDD」でググってヒットしたいくつかの実践例では上記のように "Model 層とは別の Entity 層" が用意されるものがあった。
blog/
├── entity.py # <== I hate this
└── models.py
3. 値オブジェクト
感想: Mode 層(≒ Entity)に対応する不変オブジェクトを常に用意することには反対。
※もちろん要件により必要なケースがあることは否定しない。
値オブジェクトは Entity(本記事では Model)に対する概念で、id ではなく値によって等価性を評価する概念。
しかし、これは「値オブジェクト = クラス定義」ということを意味しない。
例えば、「プリミティブな値を小さなクラスに切り出して例えばコンストラクタでバリデーションを実施する」という目的なら Django には不要と考える。
Django なら値のバリデーションは validator というコンポーネントに記述する方がプラクティスに沿っている。
- バリデータ (Validator) | Django documentation | Django
- フォームとフィールドのバリデーション | Django documentation | Django
また上記に関連する値オブジェクトの解説やありがちな誤用、注意点は kumagi 氏の説明が分かりやすいのでリンクを載せておく。
4. Presentation, Controller
Django デフォルトの命名に合わせて views
で良い。
※または RESTful なバックエンド専用APIの場合なら django-ninja に沿って routers
とか
Ruby on Rails のように Django プロジェクトを初期作成するときにボイラープレートが適用される。
デフォルトで models.py
views.py
が含まれる。これを指している。
$ django-admin startproject blogs
$ tree blogs/
blogs/
├── __init__.py
├── apps.py
├── migrations/
├── models.py
├── test.py
└── views.py # <==
まとめ、雑感
今回調べた過程で戦術的 DDD として謳われているデザインパターンはかなり広く使われていて、共通言語やメンタルモデルが形成されていることが理解できた。
一方でやはり全エンジニアが共通認識を持つことは難しく、言語とフレームワークによって適用できる度合いも変わりうる というのが自分の結論となった(もちろんそれが自然だと思う)。
戦術的 DDD はその名の通りDDD の方法論で、フレームワークとの兼ね合いを十分に考慮する必要があると思う。
また現実的には組織/チームの技術選定、フレームワークは(戦略的 DDD の根幹である)ビジネス/ドメインサービスとは別に選定されることが多いのではないかと思う。
e.g. Python なら Django/FastAPI、Go なら xxx、etc
特に「フレームワークのレール」に従わないアーキテクチャパターンは独自設計であり認知負荷とのトレードオフになりえる。
戦術的 DDD の一般的なデザインパターンを常に全て導入するのはやはり無駄にアーキテクチャの認知負荷を高める可能性が生まれるように思える。
そもそもレイヤー(≒ ディレクトリ階層)やクラスはやみくもに増やすべきではない。
このあたりは kumagi 氏の mond の回答で以下の部分がかなり共感できた。
クラスやインタフェースは少なければ少ないほど認知負荷が減るし変更に強い
少ないインタフェースでより多くの事ができるのが良い設計。その向こうでどんな複雑な事をしても認知負荷を増やさないので。
一方で複数のドメインモデルが関わってくる複雑なユースケースは「ドメインサービス」や「アプリケーションサービス」の考え方はサービスの複雑性に立ち向かう武器になりえると思う。
その他: "短い文章だけで意図を汲み取り切れてないかもしれないが、概ねの意図は自分の主張と同じなのではないか?"
というツイートのピックアップ