Friday, July 8, 2011

Django Class-Based Views: A Design Perspective

The release of Django 1.3 introduced the concept of class-based generic views.  Prior to 1.3, generic views were written as functions.  What's the big fuss over class-based views?  What can they possibly offer that functional views can't?  Convenience, less code, and elegant design - a property that extends down through Django's core.   From a design perspective, class-based views in Django are a better way to implement common view patters with marginal effort than generic function views.  For me, having only used class-based views for a few months, I'm very impressed.  When I compare generic functions with generic classes, I'm amazed at the difference in quality.  This is one of those small enhancements that I think will change Python web development for the better.

Maybe a little history first, shall we?  First of all, what makes Django views so useful and easy to work with in the first place?  We can tell Django how to map HTTP requests to our view functions using URL configurations.  This is useful because we have URL definitions separate from the functions themselves - multiple URL patterns can be handled by a single view function.  The URL path segments and query parameters are passed to the function as positional arguments and keyword arguments respectfully.

Having a Python function handle each HTTP request sent to the application is intuitive, no two ways about it.  The job of the view function is to take the client's request, perform some business logic, and return a rendering context for the template.  For common scenarios, generic view functions can be passed to the URL configuration.  For example, listing specific objects or for a detailed view of a single object.  For this, generic view functions are perfect.  We don't need to write repetitive code that differs only slightly from view to view.

However, we can't use generic views for everything and this is where the Django developer needs to step in and write some custom stuff.  Stuff that'll refine a query or pass additional context to the template.

Prior to Django 1.3, where generic class views were introduced, generic functions were customized by passing an info dictionary to the URL configuration.  This isn't an ideal coupling since the view now depends on additional data being passed to the URL definition.  For instance, if I have a URL where I want to return a list of objects, I can use a generic view that does just that.  However, to customize the query, I need to pass additional information to the URL definition that tells the view how to alter the query.  So if I wanted to reuse this same view, with the same query modifications, I'd have to duplicate the additional generic view info.  Not that this is painfully tedious, just error-prone.

Class-based generic views aims to solve the trouble of binding view-specific data to the URL, and to that end, I think they've succeeded hugely.  The new class-based approach introduces the concept of inheritance.  This method of extending views is less susceptible to problems with configuring URLs which depend on additional information.  Plus, extending generic views is simply elegant from a design perspective.

For example, we can extend the ListView class provided by Django to render a list of objects from our model.  At the most basic level, all we really need to tell ListView about is the model to select from.  Say we have a Book class.  We can define ourselves a new BookList class view for returning lists for books.  We're overriding the default model value of ListView.

I find this superior to the decorated customization approach.  With decorators, we're wrapping the view function with additional logic.  For instance, depending on the request context, HTTP headers, GET values, the URL, and so forth, we'll execute the logic of the generic view differently.  Nothing is wrong with this except for the fact that it doesn't sit well with doing things generically for a database view.  If I'm writing a list or detail view for things stored in the database, I want as little customization as possible if I'm using generic views.  This is a key point - the views are generic because you're supposed to give them only as little information as possible.  Just the bare minimum required to produce the appropriate template context.

With the new class-based approach, it's easier to leave the defaults alone, only overriding what's necessary.  The model name, for instance.  Or if you need more control over the resulting query set, overriding the get_queryset() method.  It's best to leave the common stuff to the generic view framework.  This is it's selling point, after all.  Things like automatically finding the template based on the model name.  If you absolutely must change something about a view, there is an attribute or a method you can override.  If you find that you're extending the Django generic views and they're becoming large, chances are you shouldn't be using them.  That is, if a method you've overridden on a Django generic class view looks nothing like a basic customization, you should think hard about why you're using a generic view in the first place.  Maybe you're better off writing a regular, non-generic view.  There is nothing wrong with this approach - that's why standard, function-based views exist - for writing business logic.

So from a design perspective, what do class-based generic views bring to the table?  A cleaner, more elegant mechanism for displaying database data.  In addition to decoupling the customizations that go along with generic views from the URL configurations, class-based generic views embrace inheritance as a means to use default behaviour and override only when necessary.  Often the generic behaviour is enough, just tell the view about the basic necessities, and you're off to the races.  Django offers a plentiful selection of built-in class-based views to choose from, each of which can be extended cleanly.