クラスベースビューとデコレータとkwargs

クラスベースビューとデコレータを駆使するとDRYに書けていいですねーという話。

たとえば

こんなURL設計のページを実装するとする。

/shop/<shop_id>/
/shop/<shop_id>/goodsA/<goodsA_id>/
/shop/<shop_id>/goodsA/
/shop/<shop_id>/goodsA/<goodsA_id>/buy
/shop/<shop_id>/goodsA/<goodsA_id>/confirm
/shop/<shop_id>/goodsA/<goodsA_id>/buy_finish
/shop/<shop_id>/goodsB/
/shop/<shop_id>/goodsB/<goodsB_id>/
/shop/<shop_id>/goodsB/<goodsB_id>/buy
/shop/<shop_id>/goodsB/<goodsB_id>/confirm
/shop/<shop_id>/goodsB/<goodsB_id>/buy_finish
.
.
.

べた書きするとひじょうにめんどそうな感じですが、クラスベースビューを使ってshop, goodsA, goodsBの3つのアプリに分け・・・

class ShopViews(object):
    
    @property
    def urls(self):
        from django.conf.urls.defaults import url, patterns
        return patterns("",
            url("^shop/(?P<shop_id>\d+)/$", self.index, name=u"index"),
            url("^shop/(?P<shop_id>\d+)/goodsA/", include(goodsAView().urls),
            url("^shop/(?P<shop_id>\d+)/goodsB", include(goodsBView().urls),
        )

などして、適宜goodsAView, goodsBViewでbuy, confirmなどのビューを実装してやればビューの分割が綺麗にできてうれしさ満点です。*1

しかし

この場合goodsAのビューメソッドがどうなるかというと

class goodsA(object):
    
    @property
    def urls(self):
        from django.conf.urls.defaults import url, patterns
        return patterns("",
            url("^$", self.index, name=u"index"),
            url("^(?P<goodsA_id>\d+)/", self.detail, name=u"detail"),
            url("^(?P<goodsA_id>\d+)/buy", self.buy, name=u"buy"),
            以下多数
        )
     
    def index(self, shop_id):
        return get_object_or_404(Shop, pk=shop_id)

    def detail(self, shop_id, goodsA_id):
        shop = get_objects_or_404(Shop, pk=shop_id)
        goods = get_objects_or_404(GoodsA, pk=goodsA_id)
        return (shop, goods)

    以下延々と

こんな感じですね。

なにがめんどいかと言えば

  1. shop_idからShopオブジェクトを取得するところが分離できてないので同じ処理をたくさん書かなきゃいけない
  2. goods_idも同様
  3. ビューメソッドにshop_idとかいっぱい出てくるのでダサい
  4. でもURLパターンでshop_id取っちゃってるからビューメソッドの引数からなくすとエラー


やだー

どうする

このままではダサいうえに変更に弱くなるのでなんとかしましょう。

まずこんなデコレータを書きます。

(shop/views.pyとか)

def shop_view(view_func):
    def _view(request, *args, **kwargs):
        _kwargs = copy(kwargs)
        shop_id = _kwargs.pop("shop_id")
        shop = get_object_or_404(Shop, shop_id)
        request._shop = shop
        return view_func(request, *args, **kwargs)
    return _view

(goodsA/views.pyとか)

def goods_view(view_func)
    def _view(request, *args, **kwargs):
        _kwargs = copy(kwargs)
        goods_id = _kwargs.pop("goodsA_id")
        goods = get_object_or_404(GoodsA, goods_id)
        request._goods = goods
        return view_func(request, *args, **kwargs)
    return _view

あとはgoodsAのビュークラスに仕込むだけ。

class goodsAView(object):
    
    ...

    @shop_view
    def index(self, request):
       return request._shop

    @goods_view
    @shop_view
    def buy(self, request):
       return (request._shop, request._goods)

ちょうすっきりしました。デコレータはよいものですね。

要点1

shop = get_object_or_404(Shop, shop_id)
request._shop = shop

Shopオブジェクトを取得し、なければ404を返すというのをデコレータ上で処理させます。

ビュー周りではデコレータをかけるだけなので見通しがよく、つまらんコードをコピペする必要もなくなります。

要点2

goods_id = _kwargs.pop("goodsA_id")

kwargsからpopしています。

urlから取られた引数のdict(kwargs)がそのままviewに渡されるので受け取る側と引数がマッチしていないとエラーが出ますが*2、こいつをビューに渡る前にとっぱらってやるわけです。

要点3

_kwargs = copy(kwargs)

kwargsままでなく、コピーしています。

元のkwargsを弄ってしまうと、shop_viewより外側で適用されてかつview実行の後処理を行なうデコレータがあった場合にkwargs変更の影響が及んでしまい、危険です。




おしまい。

*1:例なので説明しやすさ重視で変なURLの切り方ですが、ほんとならshop/のとこは/urls.pyに書いて、そこにShopViews().urlsをincludeするのが綺麗だと思います。

*2:勿論ビュー側仮引数にデフォルト値を指定すれば回避できますが、shop_idの例でデフォルト値指定するのはバグの元になりそうだし、結局shop_idという仮引数は書かなくちゃならないので意味ナシ。