Class-Based ViewからはじめるDjango入門

この記事は 2012 Pythonアドベントカレンダー (Webフレームワーク) 16日目 のエントリになります。
今回は DjangoのClass-Based Viewの紹介です。

Function-basedからClass-Basedへ

皆さん!View書いてますか、View。
Model-View-ControllerのViewじゃなくてModel-Template-ViewのViewです。


仕様変更なんて日常茶飯事なこの業界で、変更に強いウェブアプリを書くには
いかにViewを書かないかが週末の休日を守る最後の砦だと思う次第なわけですよ。


シンプルなViewと正しいモデル設計があれば仕様変更で
マミられたあなたの休日も半分くらいは残るはず!


とは言っても、画面とモデルの接点にいるViewはだいたい同じだけど
細かい大量の違いがあるためになかなか共通化が難しいでしょう。


画面は素人に見える分どうでもいい変更が大量にいれられてしまいますし
そこを守るよりはモデル側の設計を守る為に仕様変更と戦う事に注力したい所。


関数は引数で結果を変えるのは得意ですが、関数自体の処理を
ちょっとだけだけ変えたものをたくさん作るのはあまり得意ではありません。
そういえばそういうのが得意なやつがいました。


クラスです。


おぶじぇくとしこうってやつです。


いままでの関数ベースViewってのはDjangoチュートリアルでも作った以下のようなやつです。

# views.py
def index(request):
    latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
    return render_to_response('polls/index.html',
                              {'latest_poll_list': latest_poll_list})
# urls.py
urlpatterns = patterns('',
    url(r'^polls/$', 'polls.views.index'),
)

普通ですね。ただのPollのリストをテンプレートに渡すだけのView関数です。


これがクラスベースになると

# views.py
class IndexView(TemplateView):
    template_name = 'polls/index.html'

    def get_context_data(self, **kwargs):
        latest_poll_list = Poll.objects.all().order_by('-pub_date')[:5]
        return {'latest_poll_list': latest_poll_list}
# urls.py
from polls.views import IndexView
urlpatterns = patterns('',
    url(r'^polls/$', IndexView.as_view()),
)

みたいに書きます。
これだけだと urls.pyでViewを直接importするのが面倒なくらいですかねー


便利なdjangoさんは色々汎用的な処理を用意していてくれて、テンプレートだけ表示するならそもそもViewを書かなくてもがあるのも今まで通りです。

urlpatterns = patterns('django.views.generic.simple',
    (r'^foo/$',  'direct_to_template', {'template': 'foo_index.html'}),
)

クラスベースではこうなります。

from django.views.generic import TemplateView
urlpatterns = patterns('',
    (r'^foo/$',  TemplateView.as_view(template_name="foo_index.html")),
)

まぁimportする分コードは増えた感があります。


他にもListを表示したりCRUDインターフェイスを用意するためのものなど一通りありますが
個別の細かいクラスベース汎用ビューの説明は ドキュメント のほうを参照してください。
リスト表示したり、詳細表示したり、リダイレクトしたりする汎用的なやつが色々あります。

で、クラスベースだと何がうれしいのか

やっと本題です。

Class-Based Viewはその名の通りクラスなので振る舞いを非常に容易に変更できます。
単に継承して上書きすればいいだけです。


Class-Based Viewは結局の所、純粋なPythonのクラスなので、テンプレートメソッドパターン wikipedia:Template_Method_パターン を中心としてオブジェクト指向のテクニックがそのまま使えます。(modelだとdjangoの models.Modelとの相性や、southとかsouthとかsouthとかsouthとかのせいで色々出来ない事があります)


そのため、個々の処理をMixinとして切り出しておき、最終クラスはそれらを組み立てるだけで完成させらるわけです。

class HogeView(TemplateView, FooMixin, BarMixin, FugaMixin):
    template_name = 'hoge/fuga.html'

これのすばらしい所は、最終的なクラス自体は別ですから、HogeViewだけでFooMixinに変更が必要なった場合はHogeViewで該当メソッド再定義して上書きすれば他のFooMxinを使っているクラスには一切修正をいれずに修正可能な所です。機能は共通化されつつも、部分変更は個別のクラスに閉じ込めておける。度重なる変更の末に共通処理がif文の嵐という未来とはこれでおさらば!


例えばテンプレートのレンダリングで、結果のオブジェクトが単数と複数の場合で使うテンプレートを変えたい時なんかは

import re
class MultiSingleTemplateView(TemplateView):
    is_single = False
    single_suffix = '_single'
    multi_suffix = '_multi'
    def get_template_names(self):
        suffix = self.single_suffix if self.is_single else self.multi_suffix
        return [re.sub(r'.html$', '{}.html'.format(suffix), self.template_name)]

みたいなのを書いて

class ResultView(MultiSingleTemplateView)
    template_name = 'poll/result.html'
    is_single = True
    
    def get_context_data(self, **kwargs):
        poll = Poll.get(kwargs.get('poll_id'))
        
        if len(poll.authors) > 1:
            self.is_single = False
        
        return {
            'poll': poll
        }

とするだけで単数の場合は poll/result_single.html が使われ
複数では poll/result_multi.html が使われます。

継承しての拡張なので

class ResultView(MultiSingleTemplateView)
    template_name = 'poll/result.html'
    
    def get_context_data(self, **kwargs):
        self.poll = Poll.get(kwargs.get('poll_id'))
        return {
            'poll': self.poll
        }
        
    @property
    def is_single(self)
        return len(self.poll.authors) == 1

みたいにプロパティにしても当然動きます。
(この例はかなり強引ですが・・・)


また、別のViewではresult_single.html, result_multi.htmlではなく、複数の時だけ result_n.html にしたい時は

class ComposeResultView(MultiSingleTemplateView)
    template_name = 'compoase/result.html'
    single_suffix = ''
    multi_suffix = '_n'

でOKなわけです。

今回はTemplateViewを拡張しましたが、当然ほかのdjango標準のgeneric viewも同じように拡張できます。

そんなこんなで、よりシンプルなViewを書いて変更に強いWebアプリケーションにするためにClass-Based Viewはかなり強力なツールではないかなと思う次第でした。



明日の 2012 Pythonアドベントカレンダー は @hdknr が django-social-authでGithub API を使ってみる(仮) を書くそうです。
OAuthは自分で書くと結構面倒ですから楽しみ!