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」へ
「公開済みの記事を取得する」といった、繰り返し使われるクエリは、カスタムQuerySetとManagerに定義します。
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: これらをつなぎ合わせ、リクエストに応じて適切なレスポンスを返す「指揮者」に徹する。
 
ロジックを適切な場所に配置する習慣を身につけることで、コードの再利用性、テスト容易性、そして保守性を劇的に向上させることができます。
