… 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:

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.

What I'm really trying to show is that in case of the majority of user code there's no point at all in having views defined as classes. Your code is never going to be as generic as Django's (unless you're building a framework) and functions just make better sense. I, for one, simply implemented a custom short version of object_list and everything just worked.

Comments: 18

  1. Vee

    +1.

    Ну, и,

    "We'll never know if they would be prettier..."

    "... functions just make more sense. I, for one, simply ... and everything just worked."

    Oh, и в школе учили что начинать sentence с "Probably" или "And" неположено.

    :D

  2. Malcolm Tredinnick

    Definitely agree. A long-time complaint of mine about trying to write domain specific languages is that now the developer has to learn a second language and unless it's a HUGE win to do so or the language is absolutely tiny, the extra abstraction is making things harder. Class-based views are a DSL, in effect: they are trying to remove the need to write a view by saying you "only" have to fit the bits in your code that are different to the framework pattern, so the language is a pattern language. But people's constant wish for Yet Another Lever and Shiny Button(tm) has led to something that isn't a huge difference over just writing a view in the first place. Developers already have a nicely configurable interface for writing views: it's called Python.

  3. Sergey Tkachuk

    I totally agree. Class-based generic views is the ugliest thing in django.

  4. Андрей Богомолов

    А разве обычные вьюхи (не generic) стали deprecated? Я всегда почему то думал, что можно внутри своей вьюхи вызвать ListView.as_view и передать ей все те параметры как и раньше. Правда там получается какая то морока с extra_context (я так понял, что его теперь нельзя передать не наследуя), но зато если нужно расширить функциональность generic view и при этом оставить ее все же generic то cbv получаются более гибкими.

  5. Dean

    Definitely agree... I'm relatively new to Django and generic views made no sense to me coming from other frameworks (Grails and Rails). I'm glad to see I'm not the only one...

  6. glader.livejournal.com

    Есть подозрение, что как Джанго странно смотрится на микропроектрах, так и CBV странно смотрятся на микровьюхах. И большой профит будет для проектов с большими развесистыми вьюхами, выполняющими много вычислений. Их легче будет наследовать, изменяя только часть функционала.

  7. John DeRosa

    +1! I wrote about this (but not as eloquently as you did) in my own blog a few months back. (http://seeknuance.com/2011/05/28/i-dont-like-django-class-based-generic-views/) I agree with the intent, but between the extra coding you need to make them work, and the documentation's paucity, they're not fun at all. I still don't use them.

  8. andrew@amccloud.com

    From Reddit:

    I'd have to disagree with this article. It misses a major point of class-based (generic) views. Your language and category filtering code must be duplicated across all your views. This argument may work for one view but as soon as you have a project or app with 10+ views you wouldn't want this boilerplate mess in every view.

    This code needs to be separated (mixins are a good choice for this) so that it can be mixed and matched in multiple views at a whim.

    https://gist.github.com/1415cf6300f07b9bc568 (untested but i'm using something very similar)

    As you can see the final view boils down to 3 lines of code, the rest of your views will also end up a very low LOC. Your code may be shorter in one view but as your codebase scales to multiple views properly utilized class-based views will not only reduce your line count, it will reduce the chances of error.

  9. Volodymyr Sergeyev

    Classes has one thing functions missing - inheritance. So you can reuse class Category() later (maybe with overiden methods/data).

    Imho, as always.

  10. Ivan Sagalaev

    I was expecting these :-)

    andrew@amccloud.com:

    Your language and category filtering code must be duplicated across all your views. This argument may work for one view but as soon as you have a project or app with 10+ views you wouldn't want this boilerplate mess in every view.

    Volodymyr Sergeyev:

    Classes has one thing functions missing - inheritance. So you can reuse class Category() later (maybe with overiden methods/data).

    I already mentioned in the post, in passing, that in terms of code reuse functional and class paradigms are equivalent. I'm not going to write some sort of a formal proof here but I'll try my best to explain it…

    A function, at its essence, is the abstraction of logic that can be reused elsewhere — nothing else. A method of a class is simply a function that has an implicit first argument (not that implicit in Python). This makes class methods a specialized kind of functions meaning that functional abstraction is actually more flexible than class-based. An instance of a class is much the same as a function with variables bound in a closure.

    Inheritance is a fancy mechanism of method lookup that makes it possible to override only some of your methods. It implements the idea that descendant objects share a common semantics that is expressed with common method names. In a functional world you just have differently named functions that you can call freely from each other. Again, inheritance establishes specialized rules of naming and calling functions that makes class hierarchies less flexible than functional call graphs.

    Andrew's example shows this very well. Each part of the code that can be reused is represented by a mixin. And the final view just uses them all. All this can be easily expressed with functions:

    def translate(language):
        # ...
    
    def get_category(slug):
        # ...
    
    def filter_category(*args):
        # ...
    
    def view(request, language, slug):
        translate(language)
        return object_list(filter_category(get_category(slug)), PAGINATE_BY)
    

    Having all those words inside the view — "translate", "filter_category", "get_category" — is the equivalent of listing mixin names in bases. But without having to think about the MRO :-).

    If necessary this functional code can be made more generic. For example, translate can be a decorator and would dig for language parameter by itself. I don't have it in my code because translation.activate(language or 'ru') is not really worth shortening to @translate. And object_list can be a function with PAGINATE_BY as the default value for an argument. This is how I have it in my code right now because there are about five views using it. The outcome is the same as in a class code: PAGINATE_BY is present in the code only once, I don't repeat it.

    My point is that class-based code adds boilerplate that can only be tolerated if you have many places to use it while functions are good for both ends of the simple—complex spectrum. The only real purpose of classes, as Malcolm wrote earlier, is to build DSLs. It's just so happened that this particular DSL of Django's generic views didn't work for my app (though it did work for one of my other apps).

    Further (lazy) reading: SICP, Paul Graham on Object Orientation.

    P.S. BTW, I blame universities for continuing to plant the misconception of classes being more powerful than functions. It's notoriously hard to overcome.

  11. Ivan Sagalaev

    А разве обычные вьюхи (не generic) стали deprecated?

    Нет, конечно. Я и написал в конце, что свои вьюхи делать классами смысла нет.

    Правда там получается какая то морока с extracontext (я так понял, что его теперь нельзя передать не наследуя)

    Да, это ещё одно неудобство, оно тоже сыграло свою роль :-)

    но зато если нужно расширить функциональность generic view и при этом оставить ее все же generic то cbv получаются более гибкими.

    Не более гибкими. Всё тоже самое всегда делали, оборачивая generic вьюхи в свои функции.

  12. andrew@amccloud.com

    Иван Сагалаев:

    Yes MRO can bite you in the ass sometimes. Yes you can chain multiple functions with a slew of arguments to override behavior. Yes "generic" class-based views add some sort of boilerplate. Could you provide an instance where the boilerplate gets in your way and causes you to write more code or gets in your way? I've never had this issue and i've done some pretty radical things with class-based views like rendering to an api that interacts with phones. https://github.com/amccloud/django-tropo-webapi/blob/master/example/opengate/views.py

  13. @coffeesnake

    Yeah, there's indeed some preference to OOP approach over the functional one in Python, not sure why. For instance in Scala functional approach is considered much more clean and robust way than writing imperative OO-code and those who prefer class abstractions over functional ones are usually considered as newbies in Scala community :) That's huge contrast comparing to Python which is really strange cuz Python features first-class functions and certain syntax sugar like decorators.

    Looks like the only reason is Python's poor syntax for lambdas. People would probably use much more of FP in Python if they'd have some nicer way to define multiline anonymous functions inline. I mean the language is powerful enough but its syntax really encourages imperative style/OO-approach.

  14. Андрей Богомолов

    Не более гибкими. Всё тоже самое всегда делали, оборачивая generic вьюхи в свои функции.

    Ну например вот такой случай: необходимо вычислить пару значений для переменных контекста на основе полученного объекта используя object_detail вьюху. Без использования cbv и переопределения метода get_context_data (где нужный объект уже получен из переданного queryset на основе pk или slug) я не представляю как это написать не нарушая DRY.

  15. Ivan Sagalaev

    необходимо вычислить пару значений для переменных контекста на основе полученного объекта используя object_detail вьюху. Без использования cbv и переопределения метода get_context_data (где нужный объект уже получен из переданного queryset на основе pk или slug) я не представляю как это написать не нарушая DRY.

    Я и не спорю, что предыдущая реализация generic вьюх была ограниченной. Моя позиция в том, что для придания им гибкости не нужен был синтаксис классов. Конкретно описанную задачу можно было решить разными способами:

    • Научить вьюху получать не только queryset и id, из которого она выбирает объект, но и непосредственно объект. Тогда обёртка может выбрать объект сама. Это либо прямой вызов Model.objects.get(pk=1), либо можно было бы иметь утилитарную функцию get_object(queryset, id=None, slug=Non), хотя это уже overkill.

    • Воспользоваться TemplateResponse (появившимся тогда же, когда и CBV). Он впрямую предназначен для того, чтобы менять контекст, после работы вьюхи:

      def wrapper_view(...):
          response = object_detail(...)
          obj = response.context['object']
          # ...
          response.context[...] = ...
          return response
      
  16. Andrey Popp

    @coffeesnake

    Yeah, there's indeed some preference to OOP approach over the functional one in Python, not sure why. For instance in Scala functional approach is considered much more clean and robust way than writing imperative OO-code and those who prefer class abstractions over functional ones are usually considered as newbies in Scala community :)

    You seems to oppose FP to OOP, but I think this is wrong. FP is about being referentially transparent and OOP is about encapsulation. Please note I'm not saying FP langs don't have encapsulation, they are, but usually via other techniques, not OOP.

    @Иван Сагалаев

    You're definitely right, classes are essentially sets of related functions, nothing more. Anything you can do with classes you can also achieve by using functions in Python. But classes still exist in the language for the reason — they capture useful pattern named encapsulation.

    Consider a function which is parametrized over some function which do filtering on database results:

    def func(filter_func):
        pass
    

    Now something has changed and you want also to parametrize your function over some another function which do grouping of database results:

    def func(filter_func, group_func):
        pass
    

    Now things get interesting — you need to change every func call to provide additional argument... and that's every time you add more parameters to your func. Another solution is to pass all functions as single argument — dictionary:

    def func(funcs):
        filter_func = funcs["filter_func"]
        group_func = funcs["group_func"]        
        ...
    

    But that's exactly the same as define you func being method in class and have parametrized over other methods:

    class Func(object):
        def __call__(self):
            filter_func = self.filter
            group_func = self.group
            ...
    

    This also helps in usual case of func's params being closely related to each other — you define them together in single class definition and what's more important you can't mix params irrelevant to each other in client code.

  17. Кирилл Маврешко

    Написал свои размышления на эту тему http://kimavr.name/blog/2011/9/2/7/ Получилось что-то вроде "почему я люблю class-based generic views" :-)

  18. jetbird

    I am learning generic views and it has been a painful experience so far. The official documentation about them is bad.

Add comment