Django REST Framework + Clean Architecture 設計考察


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 処理についてはほぼ実装することがありません。

models.py
from django.db import models

class Question(models.Model):
    # ORMを提供
    question_text = models.CharField(max_length=10)
    pub_date = models.DateTimeField('date published')
serialzers.py
from .models import Question
from rest_framework import serializers

class QuestionSerializer(serializers.ModelSerializer):
    # data <-> model変換を提供
    class Meta:
        model = Question
        fields = '__all__'
        # data <-> model 変換する対象のmodelとフィールドを定義する
views.py
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. 業務知識が serializersmodels に散らばりやすい

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 設計検討

設計

上記を鑑み、以下のように設計しました。
というか、こちらを多分に参考にさせて頂きました。

ざっくり、以下のような方針で設計しました。
当たり前ですがドメインロジックは、viewsserializersmodelsに書かない方針です。
逆に言うと、ドメインロジックを含まないものに関しては、Django REST Framework に則って使う想定です。

挙動

以下、REST API を叩いた場合の挙動を記載します(実装していないものもあります)。

  • Query(GET)実行時

    • Django REST Framework が提供している機能をそのまま使う
    • Django 以外のリソースからデータを引っ張ってくる場合は、serializer側にSerializerMethodFieldなどで定義する
  • Command 実行時:業務ロジックなし ver

    • serializervalidate()内で、AggregateDomainObjectを用いて値のバリデーションを行う
  • Command(POST/PUT/PATCH/DELETE)実行時:業務ロジックあり ver

    1. viewsは、application_servicesserializerからデータのdictを引数として渡す
    2. application_service は 引数のdictを元に、Aggregateを生成し、処理。
      • DB のデータからAggregateを生成する場合は、IXXXReaderを用いる
      • データを保存する場合は、引数のdictを元に、Aggregate生成、IXXXWriterに渡して保存
      • viewsには、response を意識したdictを返す
    3. application_servicesから取得したdictを元に、response 用のserializerを使って返す

まとめと所感

調査や設計をしながら思ったことを以下にまとめます。

実装に関して

  • Interface Adapters 側と XXX Bussiness Rules 側の I/F はシンプルに、かつロジック分割はできたと思う
    • が、やはりファイルや module が増える
    • パフォーマンスを気にする人には不評かも
  • serializerdatavalidated_data(特にDateTimeField)の癖が強い
    • dataだとstrvalidated_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