Why I don't like class-based generic views

… because I have to replace this:

def category(request, slug, language): translation.activate(language or 'ru') category = get_object_or_404(models.Category, slug=slug) return object_list(request, template_name = 'marcus/category.html', queryset = models.Article.public.language(language).filter(categories=category), paginate_by = settings.MARCUS_PAGINATE_BY, extra_context = { 'category': models.Translation(category, language), 'language': language, }, )

with this:

class Category(generic.ListView): template_name = 'marcus/category.html' paginate_by = settings.MARCUS_PAGINATE_BY def get_queryset(self): self.category = get_object_or_404(models.Category, slug=self.args[0]) return models.Article.public.language(self.args[1]).filter(categories=self.category) def get_context_data(self, **kwargs): translation.activate(self.args[1] or 'ru') context = super(Category, self).get_context_data(**kwargs) context.update({ 'category': models.Translation(self.category, self.args[1]), 'language': self.args[1], }) return context

Now, these snippets might not look strikingly different from the first sight so let me break them down for you and explain my point:

When your code is a function it is this function that actually works and calls other functions. When your code lives in overridden methods, something else works elsewhere and calls your code. This is called "inversion of control" and it makes code harder to read because you no longer see why and in which order things happen. This is not necessarily a bad thing (the whole framework works by inverting control, after all) but in this particular case it did make me think harder. In the original code I call translation.activate before anything else to make sure that any code that might depend on current language will have it available. Here I don't really know in which order get_queryset and get_context_data are called and the only reliable way to activate translation early is to override another method — dispatch — that calls both of them. But it makes code even hairier.

You're bound to have some boilerplate code when subclassing. Here I have two method signatures that have nothing to do with my specific domain, I just have to look them up in the docs and repeat to the letter. And I also have to call super() , not the prettiest thing in Python and it makes me to repeat the class name.

Since my code is now in two places the only way to have a variable known to both of them is to put it in the class instance — the self.category in this case. It doesn't really belong there, it was just a local temporary variable but after it became a class attribute it looks just as important and global as any other "real" attribute.

I lose descriptive names of arguments that I had in a function. Now they are available as opaque self.args[..] values. True, I could assign them to local variables but doing it every time is just silly…

And of course, the code just got obviously longer.

By the way, this is not the worst refactoring that I had to deal with during my yesterday's crusade on deprecation warnings. But I decided to be merciful on you :-).

Philosophy

I often hear that class based generic views are better because they are more flexible and extensible. What people miss is that extensibility doesn't come at all from the fact that they are implemented as classes. Extensibility exists only in those places that framework explicitly defines as extensibility points. Everything that you can do by overriding a method or setting an attribute you can do with a function passing values and callables as arguments. These two paradigms are thus equivalent in terms of functionality.

Still, it probably makes perfectly good sense for Django itself to have generic views as classes because users demand insane level of extensibility for them. We'll never know if they would look prettier were they implemented as functions because nobody volunteered to do that. Probably readability of classes does indeed scale better along the axis of use-case complexity.