【Python】Django設計のベストプラクティス:「Fat Models, Thin Views」(コントローラーには処理を書かない)

DjangoのようなMVT(Model-View-Template)アーキテクチャを採用するフレームワークを使い始めると、開発者はしばしば、あらゆるビジネスロジックを「ビュー(View)」関数(一般的に言う「コントローラー」)に詰め込んでしまうという過ちを犯しがちです。

この方法は、小規模な機能では手軽に感じるかもしれませんが、アプリケーションが成長するにつれて、コードは複雑怪奇になり、テストもメンテナンスも困難な「巨大なビュー(Fat View)」を生み出してしまいます。

今回は、この問題を解決するためのDjangoコミュニティにおける設計思想の金字塔、**「Fat Models, Thin Views(モデルは厚く、ビューは薄く)」**という原則について、具体的なコードのリファクタリングを通して解説します。

目次

問題点:あらゆるロジックが混在した「Fat View」

ブログの記事一覧を表示するビューを考えてみましょう。このビューは、以下の機能を持っています。

  • ブログの投稿者本人でなければアクセスできない
  • 公開済みの記事のみを表示する
  • タイトルによる検索機能がある
  • 各記事のおおよその読了時間を表示する

これらの機能をすべてビューに詰め込むと、次のようなコードになります。

悪い例 (views.py):

from django.shortcuts import get_object_or_404, render
from django.core.exceptions import PermissionDenied
from .models import Blog, Article

def article_list_view(request, blog_id):
    # 1. データ取得と権限チェック
    blog = get_object_or_404(Blog, id=blog_id)
    if blog.author != request.user:
        raise PermissionDenied("このブログの編集権限がありません。")

    # 2. クエリの組み立て
    articles = Article.objects.filter(blog=blog, is_published=True)

    # 3. 検索フォームの処理とバリデーション
    search_query = request.GET.get("query", "")
    if search_query:
        if len(search_query) < 2:
            # バリデーションエラー処理
            pass 
        articles = articles.filter(title__icontains=search_query)

    # 4. ビジネスロジック(計算)と表示用データの加工
    processed_articles = []
    for article in articles:
        word_count = len(article.body.split())
        # 1分あたり400文字読むと仮定
        reading_time = round(word_count / 400) or 1
        processed_articles.append({
            "title": article.title,
            "reading_time": f"{reading_time}分",
        })

    # 5. レンダリング
    return render(request, "articles/list.html", {"articles": processed_articles})

このビューは、権限チェック、DBクエリ、フォーム処理、ビジネスロジック、データ加工という、あまりに多くの責務を一人で抱え込んでいます。このような「Fat View」は、再利用性が低く、テストが非常に困難です。


解決策:ロジックを適切な層に分離する

「Fat Models, Thin Views」の原則に従い、これらのロジックを本来あるべき場所へ移動させていきましょう。

1. データベースロジックは「Model Manager」へ

「公開済みの記事を取得する」といった、繰り返し使われるクエリは、カスタムQuerySetManagerに定義します。

models.py

class ArticleQuerySet(models.QuerySet):
    def published(self):
        return self.filter(is_published=True)

class Article(models.Model):
    # ... (フィールド定義) ...
    is_published = models.BooleanField(default=False)
    objects = ArticleQuerySet.as_manager()

2. オブジェクト固有のロジックは「Modelプロパティ」へ

「記事の読了時間」のように、単一のオブジェクトに紐づくビジネスロジックは、モデルのメソッドやプロパティとして実装します。

models.py

class Article(models.Model):
    # ... (フィールド定義) ...
    body = models.TextField()

    @property
    def reading_time(self) -> int:
        """おおよその読了時間(分)を計算して返す。"""
        word_count = len(self.body.split())
        time = round(word_count / 400)
        return time or 1 # 最低でも1分と表示

3. 入力値の検証と処理は「Form」へ

検索クエリの受け取りやバリデーション、そしてそれに基づいたフィルタリング処理は、django.formsの責務です。

forms.py

from django import forms

class ArticleSearchForm(forms.Form):
    query = forms.CharField(min_length=2, required=False)

    def filter_queryset(self, queryset):
        if self.is_valid():
            query = self.cleaned_data.get("query")
            if query:
                return queryset.filter(title__icontains=query)
        return queryset

4. 権限チェックは「ヘルパー関数」や「Mixin」へ

ブログの所有者かを確認するような汎用的な権限チェックは、独立した関数に切り出します。

permissions.py

from django.core.exceptions import PermissionDenied

def validate_author_permission(user, blog):
    if blog.author != user:
        raise PermissionDenied("このブログの編集権限がありません。")

5. 表示に関するロジックは「Template」へ

データの最終的な表示形式(例: 〇〇分)は、テンプレート層で扱うのが適切です。

list.html

{% for article in articles %}
  <p>{{ article.title }} (読了時間: 約{{ article.reading_time }}分)</p>
{% endfor %}

結果:責務が分離された「Thin View」

すべてのロジックを分離した結果、私たちのビューは驚くほどスリムで読みやすくなります。ビューの役割は、各コンポーネントを呼び出す「指揮者」に徹することです。

リファクタリング後の views.py:

from django.shortcuts import get_object_or_404, render
from .models import Blog
from .forms import ArticleSearchForm
from .permissions import validate_author_permission

def article_list_view(request, blog_id):
    # データ取得と権限チェック
    blog = get_object_or_404(Blog, id=blog_id)
    validate_author_permission(request.user, blog)

    # Model Managerから基本となるクエリセットを取得
    articles = blog.article_set.published()

    # Formを使ってクエリの検証とフィルタリング
    form = ArticleSearchForm(request.GET)
    articles = form.filter_queryset(articles)

    # レンダリング
    context = {"blog": blog, "articles": articles, "form": form}
    return render(request, "articles/list.html", context)

この「Thin View」は、何をしているかが一目瞭然で、各部品が独立しているため、テストも非常に容易です。

まとめ

Djangoでスケーラブルなアプリケーションを構築する鍵は、「Fat Models, Thin Views」の原則を徹底することです。

  • Model: ビジネスロジックとデータベース関連の処理を担う。
  • Form: ユーザー入力の検証と、それに関連する処理を担う。
  • Template: データの表示方法を担う。
  • View: これらをつなぎ合わせ、リクエストに応じて適切なレスポンスを返す「指揮者」に徹する。

ロジックを適切な場所に配置する習慣を身につけることで、コードの再利用性、テスト容易性、そして保守性を劇的に向上させることができます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次