Django REST Framework + clean architecture 設計考察
はじめに
昨今の AI の盛隆によって、新規システム開発に Python で構築することを選択することも増えてきたのではないでしょうか。
新規 Web サービスを Python で作るとなると、フロントエンドは React や Vue.js といったフレームワーク、バックエンドは Django か Flask で REST API を提供するような形が主な選択肢になると思います。
シンプルな構成であれば Django REST Framework は簡単にかつシンプルに API サーバを作れますが、少し複雑になるときちんとアーキテクチャを考えないとすぐにカオスになります。
私の入った案件では、これがもうカオスでカオスで。。。
ざっと調べた限り、Django REST Framework + Clean Architecture の組み合わせで記事もないため、私自身の勉強、考えの纏めも兼ねて本記事で検討します。
というか辛いのでここで纏めた内容で導入したい。
良い設計があればコメントください。
今回は Project、 Application (後述)は除外して検討しています。
また、ルーティング、ViewSet、ModelManager などは検討外としています。
絵や文章だけではアレなので、サンプル実装をしてみました。
サンプルコードはここにあります。
Django のチュートリアルのモデルをベースに、Question の POST のみ実装してみてます。
(が、あんまり業務ロジック考えられなかったのでPOSTの処理を無駄に冗長になってるだけな実装になってます。。。)
Django REST Framework の構成と問題
Django そのものは、1 Project 内に複数の Application があり、その中に MVT(Model
, View
, Template
) があり、、、という考え方に沿ったフレームワークです。
Django REST Framework では、そこから Template
が無くなり、Serializer
というものが増えます。
以下に Django REST Framework のアーキテクチャと役割を図にしてみました。
1 つの model
(Database の 1table)に対する CRUD 処理であれば、基本的に view
- serializer
- model
が 1 対 1 対 1 となり、シンプルに作ることができます。
業務レベルの Web サービスであれば、REST API 実行時には、何かしらの業務知識、ルール、判断に沿ったロジック処理を行うため、単に 1table の CRUD 処理だけに留まる事は稀です。
しかしながら、アーキテクチャを深く考慮せずに実装を開始すると、Django REST Framework で提供しているアーキテクチャがカオスへと誘ってきます。。。
私としては、以下 2 点がカオスになりやすい大きな問題かと思います。
基本的に単一責任原則を崩しやすいフレームワークなのでは?と感じています。
問題 1. views
が複雑になりやすい(Fat Contoller になりやすい)
views
にロジックをべた書きする、いわゆる Fat Controller と呼ばれる問題です。
初心者やアーキテクチャ興味無し、な方が作ると発生しがちな問題です。
Django REST Framework では、シンプルなものであれば、以下のように CRUD 処理についてはほぼ実装することがありません。
from django.db import models
class Question(models.Model):
# ORMを提供
question_text = models.CharField(max_length=10)
pub_date = models.DateTimeField('date published')
from .models import Question
from rest_framework import serializers
class QuestionSerializer(serializers.ModelSerializer):
# data <-> model変換を提供
class Meta:
model = Question
fields = '__all__'
# data <-> model 変換する対象のmodelとフィールドを定義する
from rest_framework import viewsets
from .models import Question
from .serializers import QuestionSerializer
class QuestionViewSet(viewsets.ModelViewSet):
# REST APIのI/Oを提供
queryset = Question.objects.all()
serializer_class = QuestionSerializer
# 継承元のModelViewSetで、CRUD処理のAPIとなるfunctionを定義しているため
# 特にmethodを作成する必要がない
CRUD 処理が Django REST Framework 側で提供、隠ぺいされています。
そのため、例えば データ保存時(POST 時)に何らかのロジックを組み込まなければならなくなった場合、「どこに書く?」->「とりあえず views
(Controller) に追加してしまえ」という風に、安易に views.py に色々書いてしまう、という事も多いのではないでしょうか?
べた書きするのではなく、新しくクラスを作るなりしなければ、Fat Controller になる原因になります。
問題 2. 業務知識が serializers
やmodels
に散らばりやすい
Serializer が validation を提供しているため、値の上下限や、文字列のフォーマットなどの検証(業務知識、というレベルでもないですが)をどこに実装するか、という悩みが出てきます。
validation が提供されているから serializers
に実装する、という風にロジックを寄せてしまうと、seliarizers
に複数の責務を負わせてしまう形になります。
models
に実装すると、serializers
の validation は何なの?となってしまいます。
また、多人数で開発するのであれば、統一した考えで実装しなければロジックが散らばっていきます。。。
何をどこに実装すべきか、きちんと考えないとカオスになる原因になります。
個人的に考える問題の原因
なんだかんだ Serializer の提供する機能が多すぎ(優秀すぎ)、というのがカオスになりやすい原因な気がしています。
具体的には、以下が原因かな、と。
- (継承元の Serializer クラスによりますが)
serializer.save()
で対応するmodel
のレコードが保存される-
model
を使ってもserializer
を使っても保存ができるため、
-
- Serializer に validation 機能があり、かつ validation のカスタマイズができる
->serializer.validation()
に業務知識を入れたくなる(validation っていうくらいなので。)
前調査 1. 他の方の設計/実装
「django rest framework logic」でググって一番最初に出てきたこのページでは、serializers
にロジックを集める、という風に書かれています。
ですが、テストどうすんだ問題が。
こちらの方は、views
にゴリゴリ書く、という風に書かれていますが、業務ロジック散乱しそうな気が。。。
前調査 2. Django REST Framework と Clean Architecture の対応
以下に、私の解釈している Django REST Framework と Clean Architecture の対応を図にしてみました。
左がオリジナルの構成、右が Clean Architecture のいつもの図にマッピングしてみたものです。
左が基準、右がマッピングです。
こう見ると、xxx Business Rule に対応するものが無いですよね。。。
Django REST Framework + Clean Architecture 設計検討
設計
上記を鑑み、以下のように設計しました。
というか、こちらを多分に参考にさせて頂きました。
ざっくり、以下のような方針で設計しました。
当たり前ですがドメインロジックは、views
、serializers
、models
に書かない方針です。
逆に言うと、ドメインロジックを含まないものに関しては、Django REST Framework に則って使う想定です。
挙動
以下、REST API を叩いた場合の挙動を記載します(実装していないものもあります)。
-
Query(GET)実行時
- Django REST Framework が提供している機能をそのまま使う
- Django 以外のリソースからデータを引っ張ってくる場合は、
serializer
側にSerializerMethodField
などで定義する
-
Command 実行時:業務ロジックなし ver
-
serializer
のvalidate()
内で、Aggregate
やDomainObject
を用いて値のバリデーションを行う
-
-
Command(POST/PUT/PATCH/DELETE)実行時:業務ロジックあり ver
-
views
は、application_services
にserializer
からデータのdict
を引数として渡す -
application_service
は 引数のdict
を元に、Aggregate
を生成し、処理。- DB のデータから
Aggregate
を生成する場合は、IXXXReader
を用いる - データを保存する場合は、引数の
dict
を元に、Aggregate
生成、IXXXWriter
に渡して保存 -
views
には、response を意識したdict
を返す
- DB のデータから
-
application_services
から取得したdict
を元に、response 用のserializer
を使って返す
-
まとめと所感
調査や設計をしながら思ったことを以下にまとめます。
実装に関して
- Interface Adapters 側と XXX Bussiness Rules 側の I/F はシンプルに、かつロジック分割はできたと思う
- が、やはりファイルや module が増える
- パフォーマンスを気にする人には不評かも
-
serializer
のdata
やvalidated_data
(特にDateTimeField
)の癖が強い-
data
だとstr
、validated_data
だとdatetime
を返すとかハマる
-
-
DomainObject
など、もう少し実装しやすいものにするか、シンプルなものにしたい-
NamedTuple
を使うなり、property を dict 化するなり、より作りやすく使いやすくした方が良い。
-
- QueryやCommand、またCommandの内容によって、REST Frameworkのみの実装パターン、Clean Architectureを考慮した実装パターンがある
- パターンがあるとカオスになりやすいので、プロジェクト内で共通認識が必要
- PythonではInterfaceを定義することにあんまり旨味が無い
実運用に関して
API サーバを作る(というか URI 設計をする)ならば、以下のような方針で GraphQL と REST API を使い分けたほうが良いのでは、と思いました。
- Query の場合は GraphQL で提供
- UI ベースで考えると、REST API では必要なデータのみを提供する事が難しく、UI 毎に REST API を提供しようとすると API が増える。
- Query では validation が不要なため、柔軟に提供できる GraphQL の方が適しているのでは。
- Command の場合は REST API で提供
- コマンド毎に、業務ロジックが絡んでくることが多いと思われるため、柔軟な I/F を提供するのではなく、明示的に API とそのパラメータを決めていた方が良いのでは。
最後に
きっと今の現場では
「views
の中にロジック詰め込んだほうがシンプルで分かりやすいですよ(1 method 200 行越えのロジック実装済み)」
「無駄にクラス作るとパフォーマンスが...(なおパフォーマンス計測はしていない模様)」
あぁ...理解されないんだろうなァ...
参考文献
https://medium.com/@amarbasic4/django-rest-framework-where-to-put-business-logic-82e71c339022
https://isseisuzuki.com/it/django-rest-framework-concept/
https://qiita.com/MinoDriven/items/3c7db287e2c66f36589a
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
https://blog.tai2.net/the_clean_architecture.html