nikkie-ftnextの日記

イベントレポートや読書メモを発信

Djangoの汎用クラスベースビューの実装を、GoFデザインパターンのテンプレートメソッドの観点から味わう(ListViewを例に)

この記事は Django Advent Calendar 2023 4日目です。
1日目〜3日目など空きがありますので、Djangoに関係するアウトプットネタがある方はぜひどうぞ〜

はじめに

今できないことがあっても大丈夫。nikkieです。

変更しやすいコードに関心があり、ちょうぜつ本を読んでいます。
デザインパターンの章でテンプレートメソッドパターンを学びました。

テンプレートメソッドパターンの例として浮かんだのが、Djangoの汎用クラスベースビューでした。
テンプレートメソッドパターンの実装例として、Djangoの汎用クラスベースビューを見ていきます。

目次

Djangoのビュー

Djangoビュー · HonKit

ビュー はアプリのロジックを書いていくところです。

もう少し詳細化すると、ビューはリクエストオブジェクトを受け取り、ロジックで処理して、レスポンスオブジェクトを返します。

ビューの実装方法は関数による方法とクラスによる方法の2通りがあります。

関数によるビュー

関数による方法はリクエストオブジェクトを受け取り、レスポンスオブジェクトを返す関数を定義します。
Django Girls Tutorialなどのチュートリアルでよく見かける印象です1

Djangoフォーム · HonKitより

def post_new(request):
    if request.method == "POST":
        # POSTリクエストの場合の処理
    else:
        # GETリクエストを想定した処理

    # レスポンスオブジェクトを返すショートカット
    return render(request, 'blog/post_edit.html', {'form': form})

クラスによるビュー

クラスによる方法では、HTTPリクエストメソッドに応じたメソッドを実装します。

from django.views import View


class PostNewView(View):
    def get(self, request):
        # GETリクエストの処理
        # レスポンスオブジェクトを返す

    def post(self, request):
        # POSTリクエストの処理
        # レスポンスオブジェクトを返す

クラスベースビュー入門 | Django ドキュメント | Django より

特定の HTTP メソッド (GET 、 POST など) に関連するコードの集まりを、条件分岐を使ってかき分けるのではなく、それぞれに独立したメソッドを割り当てることができる。

例えば『実践 Django』や『現場で使える Django の教科書《基礎編》』で紹介されています。

汎用クラスベースビュー

クラスによるビューの実装では継承が使えます。
Djangoは定型的な処理を実装したビューのクラスを用意しており、それを継承することでわずかなコード量で定型処理を実装できます。

ドキュメントにあるListView(一覧処理のビュー)を使った例です:
https://docs.djangoproject.com/ja/4.2/topics/class-based-views/generic-display/#generic-views-of-objects

from django.views.generic import ListView
from books.models import Publisher


class PublisherListView(ListView):
    model = Publisher

たったこれだけ!
PublisherListViewはmodel属性しか指定していません。
これでPublisherを一覧するビューは機能します!

上の設定例では

  • テンプレートは books/publisher_list.html
  • テンプレート中でPublisherを指す変数は object_list

と設定したことになります。
ListViewを継承したクラスでそれぞれtemplate_namecontext_object_nameを指定してカスタマイズできます。

ListView

ドキュメントはこちら:
https://docs.djangoproject.com/ja/4.2/ref/class-based-views/generic-display/#listview
継承元やメソッドのフローチャートが掲載されています。

公式ドキュメントとは別に、Classy Class-Based Viewsというサイトもあります。
継承関係の図が見えるなど、公式ドキュメント+αが嬉しいですね

getメソッド

一覧するGETリクエストを受けたときのビューの処理を覗いてみましょう。
Classy Class-Based Viewsではgetメソッドの実装も確認できます

呼ばれているメソッドをざっくり挙げると

  1. get_queryset
  2. get_allow_empty
  3. get_context_data
  4. render_to_response

といった感じですね(条件分岐のif文は省略しました)

get_queryset

get_querysetではmodel属性を見て、全レコード取得する実装があります。

# https://github.com/django/django/blob/4.2.8/django/views/generic/list.py#L33-L34
elif self.model is not None:
    queryset = self.model._default_manager.all()

先のPublisherListViewが動いたのはこれによるのですね。
クエリセットを返しています。

get_allow_empty

get_allow_emptyは、一覧が空のときに404(Not Found)を返すかを設定するメソッドのようです。
https://github.com/django/django/blob/4.2.8/django/views/generic/list.py#L106-L111

get_context_data

get_context_dataはテンプレート用のコンテキスト(辞書)を作って返しています。
https://github.com/django/django/blob/4.2.8/django/views/generic/list.py#L122

キーは

  • paginator
  • page_obj
  • is_paginated
  • object_list

context_object_name属性を見て2、コンテキストに追加していますね。

# https://github.com/django/django/blob/4.2.8/django/views/generic/list.py#L144-L145
if context_object_name is not None:
    context[context_object_name] = queryset

コンテキストにはobject_listというキーの他にcontext_object_nameというキーもできて、この2つは同じオブジェクト(クエリセット)を指します。
テンプレートではどちらの変数を使っても同じになるわけですね

render_to_response

render_to_responseではレスポンスオブジェクトを返しています。
https://github.com/django/django/blob/4.2.8/django/views/generic/base.py#L190

ここでget_template_namesメソッドを呼び出しています。

return self.response_class(
    # https://github.com/django/django/blob/4.2.8/django/views/generic/base.py#L200
    template=self.get_template_names(),
)

この呼び出しの中でtemplate_name属性が参照されます。

テンプレートメソッドパターンの例として見たときの所感

テンプレートメソッドとしてうまいな〜と思います。

  • getメソッドを見ましたが、穴埋め問題になっていますよね(=差分プログラミング!)
    • 属性を穴埋め
    • getメソッドで呼ぶ各メソッドを穴埋め
  • 穴埋めしてビューの動きをカスタマイズできる!
  • ビューはリクエストオブジェクトを受け取り、レスポンスオブジェクトを返すと決まっている
    • 思うにこれはインターフェースのようなもの
  • ビューという抽象の中の定型処理だけを汎用クラスベースビューに切り出したのでは
    • つまり、テンプレートメソッドで全てのビューに対処しようとしていない
  • テンプレートメソッド(汎用クラスベースビューを継承)にしないで済む方法が常に提供されている
    • Viewクラスを継承して自分で書いていく
    • 関数による方法

Djangoは時の試練に耐えており、多くの人の目が入ったことでこんなふうに洗練しているのかなと思いました。

終わりに

GoFデザインパターン「テンプレートメソッド」の例として浮かんだDjangoの汎用クラスベースビューを見てきました。

  • 一覧処理を提供するListView、getメソッドはテンプレートメソッド
  • 属性の指定やメソッドのオーバーライドでカスタマイズできる
  • Classy Class-Based Viewsで実装を見るのが捗る!

課題全体をテンプレートメソッドパターンで解こうとしたというよりは、Webアプリの定型処理だけにテンプレートメソッドパターンを提供したということではと気付きがありました。
課題全体をテンプレートメソッドパターンで解こうとして手痛い失敗をした身としては非常に納得です。

参考文献

DjangoCongress 2023より

リファレンスへのリンクが大変充実しており

を知りました(多謝)


  1. Djangoドキュメントのpollアプリでも。はじめての Django アプリ作成、その 3 | Django ドキュメント | Django (クラスによるビューも紹介されます)
  2. get_context_object_nameメソッドでcontext_object_name属性を見ます