All Unkept

My approach to Class Based Views

Posted in: Python, Django  — 

I've written in the past about my dislike for Django's Class Based Views. Django's CBVs add a lot of complexity and verbosity, and simply get in the way of some moderately common patterns (e.g. when you have two forms in a single view). It seems I'm not alone as a Django core dev who thinks that way.

In this post, however, I'll write about a different approach that I took in one project, which can be summed up like this:

Write your own base class.

For really simple model views, Django's own CBVs can be a time saver. For anything more complex, you will run into difficulties, and will need some heavy documentation at the very least.

One solution is to use a simplified re-implementation of Class Based Views. My own approach is to go even further and start from nothing, writing your own base class, while borrowing the best ideas and incorporating only what you need.

Steal the good ideas

The as_view method provided by the Django's View class is a great idea — while it may not be obvious, it was hammered out after a lot of discussion as a way to help promote request isolation by creating a new instance of the class to handle every new request. So I'll happily steal that!

Reject the bad

Personally I dislike the dispatch method with its assumption that handling of GET and POST is going to be completely different, when often they can overlap a lot (especially for typical form handling). It has even introduced bugs for me where a view rejected POST requests, when what it needed to do was just ignore the POST data, which required extra code!

So I replaced that with a simple handle function that you have to implement to do any logic.

I also don't like the way that template names are automatically built from model names etc. — this is convention over configuration, and it makes life unnecessarily hard for a maintenance programmer who greps to find out where a template is used. If that kind of logic is used, you just Have To Know where to look to see if a template is used at all and how it is used. So that is going.

Flatten the stack

A relatively flat set of base classes is going to be far easier to manage than a large set of mixins and base classes. By using a flat stack, I can avoid writing crazy hacks to subvert what I have inherited.

Write the API you want

For instance, one of the things I really dislike about Django's CBVs is the extremely verbose way of adding new data to the context, which is something that ought to be really easy, but instead requires 4 lines:

class MyView(ParentView):
    def get_context_data(self, **kwargs):
        context = super(MyView, self).get_context_data(**kwargs)
        context['title'] = "My title"  # This is the only line I want to write!
        return context

In fact, it is often worse, because the data to add to the context may actually have been calculated in a different method, and stuck on self so that get_context_data could find it. And you also have the problem that it is easy to do it wrong e.g. if you forget the call to super things start breaking in non-obvious ways.

(In searching GitHub for examples, I actually found hundreds and hundreds of examples that look like this:

class HomeView(TemplateView):
    # ...

    def get_context_data(self):
        context = super(HomeView, self).get_context_data()
        return context

This doesn't make much sense, until I realised that people are using boilerplate generators/snippets to create new CBVs — such as this for emacs and this for vim, and this for Sublime Text. You know when you have created an unwieldy API when people need these kinds of shortcuts.)

So, the answer is:

Imagine the API you want, then implement it.

This is what I would like to write for static additions to the context:

class MyView(ParentView):
    context = {'title': "My title"}

and for dynamic:

class MyView(ParentView):
    def context(self):
        return {'things': Thing.objects.all()
                          if self.request.user.is_authenticated()
                          else Thing.objects.public()}

    # Or perhaps using a lambda:
    context = lambda self: ...

And I would like any context defined by ParentView to be automatically accumulated, even though I didn't explicitly call super. (After all, you almost always want to add to context data, and if necessary a subclass could remove specific inherited data by setting a key to None).

I'd also like for any method in my CBV to simply be able to add data to the context directly, perhaps by setting/updating an instance variable:

class MyView(ParentView):

    def do_the_thing(self):
        if some_condition():
            self.context['foo'] = 'bar'

Of course, it goes without saying that this shouldn't clobber anything at the class level and violate request isolation, and all of these methods should work together nicely in the way you would expect. And it should be impossible to accidentally mutate any class-defined context dictionary from within a method.

Now, sometimes after you've finished dreaming, you find your imagined API is too tricky to implement due to a language issue, and has to be modified. In this case, the behaviour is easily achievable, although it is a little bit magic, because normally defining a method in a subclass without using super means that the super class definition would be ignored, and for class attributes you can't use super at all.

So, my own preference is to make this more obvious by using the name magic_context for the first two (the class attribute and the method). That way I get the benefits of the magic, while not tripping up any maintainer — if something is called magic_foo, most people are going to want to know why it is magic and how it works.

The implementation uses a few tricks, the heart of which is using reversed(self.__class__.mro()) to get all the super-classes and their magic_context attributes, iteratively updating a dictionary with them.

Notice too how the TemplateView.handle method is extremely simple, and just calls out to another method to do all the work:

class TemplateView(View):
    # ...
    def handle(self, request):
        return self.render({})

This means that a subclass that defines handle to do the actual logic doesn't need to call super, but just calls the same method directly:

class MyView(TemplateView):
    template_name = "mytemplate.html"

    def handle(self, request):
        # logic here...
        return self.render({'some_more': 'context_data'})

In addition to these things, I have various hooks that I use to handle things like AJAX validation for form views, and RSS/Atom feeds for list views etc. Because I'm in control of the base classes, these things are simple to do.

Conclusion

I guess the core idea here is that you shouldn't be constrained by what Django has supplied. There is actually nothing about CBVs that is deeply integrated into Django, so your own implementation is just as valid as Django's, but you can make it work for you. I would encourage you to write the actual code you want to write, then make the base class that enables it to work.

The disadvantage, of course, is that maintenance programmers who have memorised the API of Django's CBVs won't benefit from that in the context of a project which uses another set of base classes. However, I think the advantages more than compensate for this.

Feel free to borrow any of the code or ideas if they are useful!

Comments §

blog comments powered by Disqus