Open9

[Python] Django x 戦術的DDD の是非を考える

s kokados kokado

ひょんなことから「Django DDD」と検索してみた。ヒットしたいくつかのWebページをざっと読んだ感想として "Django で戦術的 DDD の実践" としてアーキテクチャパターンを採用するプラクティスに強い違和感を覚えた。
※本記事では「軽量DDD」もほぼ同義と見なす

SNS 上の言説と照らし合わせつつ、自分自身の理解を助けるための思考過程を記述する

[全て記述したあとの追記]

※この記事における "Django" を "MVC パターン" と読み替えても通じる文章が多いと思う。

一般的な戦術的 DDD の設計パターンはある種の「デザインパターン」であり、それぞれの背景にある課題やメリットを考えることが重要である。
課題へのアプローチは言語/フレームワークごとに異なり、同じ課題の解決手段を「ただ違う違う名前で呼んでいるだけ」という場合も多いと思う。

(戦術的)DDD の設計パターンやその目的とエッセンスを理解することは重要と思いつつ、実際の開発シーンへの適用はきちんと逐次考えるようにしていきたい。

Disclaim

本記事の主張の対比として示すリンクや言説を非難したり、その著者を個人攻撃する意図はありません。
あくまで "Django x DDD" に関する当方の考えを述べるものです。

s kokados kokado

前置き2/2:
kumagi 氏が言及している mond の回答。

https://mond.how/topics/rvda8xe0kgrvza9/h80b3t6nvnlt79o

レイヤードという観点で言うとRuby on Railsは天才的で、プロジェクトとして初期化した時点で走るコードの形でModel/View/Controllerが別のディレクトリに分けられてレイヤー構造が出来ています。

これは「Webアプリを作りたいならこういうアーキテクチャを取れ」というのをコードのレベルから押し付けており、逆にWebアプリ以外には使えません。

せっかく天才が傑作なアーキテクチャを提案してくれたのがRailsなのにそれにDDDを導入するという倒錯が世の中にあるそうで実に残念です。

...そこに劣った方の概念であるDDDを導入するのは、ガソリン車からエンジンとハンドルを取っ払って馬に引かせるような錯誤であると認知されて欲しいものです。

以上の前置きから「"Django に戦術的DDD を導入" に対する違和感」はあながち見当外れではなさそうに思える。

s kokados kokado

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 だと感じる。

s kokados kokado

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
s kokados kokado

3. 値オブジェクト

感想: Mode 層(≒ Entity)に対応する不変オブジェクトを常に用意することには反対。
※もちろん要件により必要なケースがあることは否定しない。

値オブジェクトは Entity(本記事では Model)に対する概念で、id ではなく値によって等価性を評価する概念。
しかし、これは「値オブジェクト = クラス定義」ということを意味しない

例えば、「プリミティブな値を小さなクラスに切り出して例えばコンストラクタでバリデーションを実施する」という目的なら Django には不要と考える。
Django なら値のバリデーションは validator というコンポーネントに記述する方がプラクティスに沿っている。

また上記に関連する値オブジェクトの解説やありがちな誤用、注意点は kumagi 氏の説明が分かりやすいのでリンクを載せておく。

s kokados kokado

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   # <==
s kokados kokado

まとめ、雑感

今回調べた過程で戦術的 DDD として謳われているデザインパターンはかなり広く使われていて、共通言語やメンタルモデルが形成されていることが理解できた。
一方でやはり全エンジニアが共通認識を持つことは難しく、言語とフレームワークによって適用できる度合いも変わりうる というのが自分の結論となった(もちろんそれが自然だと思う)。

戦術的 DDD はその名の通りDDD の方法論で、フレームワークとの兼ね合いを十分に考慮する必要があると思う。
また現実的には組織/チームの技術選定、フレームワークは(戦略的 DDD の根幹である)ビジネス/ドメインサービスとは別に選定されることが多いのではないかと思う。
e.g. Python なら Django/FastAPI、Go なら xxx、etc

特に「フレームワークのレール」に従わないアーキテクチャパターンは独自設計であり認知負荷とのトレードオフになりえる。

戦術的 DDD の一般的なデザインパターンを常に全て導入するのはやはり無駄にアーキテクチャの認知負荷を高める可能性が生まれるように思える。
そもそもレイヤー(≒ ディレクトリ階層)やクラスはやみくもに増やすべきではない。
このあたりは kumagi 氏の mond の回答で以下の部分がかなり共感できた。

https://mond.how/topics/rvda8xe0kgrvza9

クラスやインタフェースは少なければ少ないほど認知負荷が減るし変更に強い

少ないインタフェースでより多くの事ができるのが良い設計。その向こうでどんな複雑な事をしても認知負荷を増やさないので。

一方で複数のドメインモデルが関わってくる複雑なユースケースは「ドメインサービス」や「アプリケーションサービス」の考え方はサービスの複雑性に立ち向かう武器になりえると思う。

s kokados kokado

その他: "短い文章だけで意図を汲み取り切れてないかもしれないが、概ねの意図は自分の主張と同じなのではないか?"
というツイートのピックアップ

https://x.com/hatchinee/status/1523501854352543744

https://x.com/hirodragon112/status/1092272551126921216

https://x.com/tzm_freedom/status/1677951562826784769

https://x.com/TWBMT/status/1565723681732841472

https://x.com/soragiwa_429/status/1533753642620428288