Class-based views — зачем и как использовать

    В Django версии 1.3 были представлены class-based views — способ описания view в виде классов. Документация, однако, касается только generic views, не описывая общую технику написания «вьюх» в виде классов. Начав использовать generic views и затем пытаясь изменить их поведение, я постепенно дошел до того момента, когда мне пришлось смотреть исходный код, чтобы понять, как что-то сделать. Поэтому я решил написать этот текст. Цель его — объяснить, как использовать class-based view и чем это может быть полезно.

    Мотивация


    Идея проста — есть некоторый часто используемый функционал (например, отображение объекта/списка объектов из БД, передача контекста в шаблон, и т.п.), который используется очень часто. Чтобы не приходилось писать однообразный код, подобные view описаны прямо в коде фреймворка. Для того, чтобы использовать их, нужно сообщить только специфические параметры, вроде типа объекта или имени шаблона, в который передавать контекст.

    Например, у нас есть класс Publisher, и нам хочется отобразить на странице список всех объектов этого класса. В Django 1.2 мы используем generic view так:

    # urls.py
    from django.views.generic import list_detail
    from books.models import Publisher
    
    urlpatterns = patterns('',
        (r'^publishers/$', list_detail.object_list, {
             "queryset" : Publisher.objects.all(),
        })
    )
    

    Мы просто передаем в словарь параметров интересующий нас queryset, все остальное уже реализовано за нас.

    Однако если в процессе разработки обнаруживается необходимость поменять поведение «вьюхи», изначально реализованной как generic (что бывает довольно часто), возникают проблемы:
    1. В generic view не предусмотрены параметры, меняющие интересующее поведение.
    2. Код, формирующий параметры для generic view, сравним по объему с кодом отдельно написанной «вьюхи», и лежит в URLCONF, где ему явно не место.

    Хочется иметь технологию, позволяющую использовать готовый фунционал Django для написания view, при этом имея возможность изменять его поведение в любой части.

    Generic views на классах


    Для того, чтобы в generic views можно было менять все, что угодно, разработчики решили представлять их в виде классов, в которых основные параметры представлены полями, а фунционал наглядно разбит на функции — отдельно диспетчеризация, отдельно формирование контекста, отдельно рендеринг шаблона (подробнее об этом см. ниже). От такого класса можно наследовать свои view, и перекрывать в них переменные и функции вместо передачи их в параметрах. Так выгладит реализация примера с Publisher на классах:

    # views.py
    from django.views.generic import DetailView
    from books.models import Publisher
    
    class PublisherDetailView(DetailView):
        model = Publisher
    


    В urls.py же вместо названия «вьюхи» теперь передается метод as_view() описанного класса:

    urlpatterns = patterns('',
        (r'^publishers/$', PublisherDetailView.as_view()),
    )
    

    В качестве параметров этой фунции могут быть переданы переменные типа model или template_name, аналогично тому, как это делалось с function-based views.

    Теперь, если нам нужно будет, например, поменять контекст, передаваемый в шаблон, можно просто переопределить в этом классе фунцию get_context_data(), которая занимается формированием контекста. И код будет жить там, где ему полагается — во views.py. Осталось только разобраться с тем, какие функции в каких generic view есть, и что они делают.

    Примеси


    Ответы на вопросы о том, как вы хотите обработать параметры входящего запроса, как будете выбирать данные из БД и как будете формировать ответ часто не зависят друг от друга. Иногда, вам хочется выдать в качестве ответа HTML страницу, иногда — JSON-объект. Если вы хотите написать общий код для обработки запроса и работы с БД, который будет использоваться с обоих случаях, нужно как-то выделить группу всех методов, отвечающих за формирование ответа из контекста. Такую проблему можно решить с помощью паттерна примесей (mixins).

    Каждая class-based generic view:
    1. Наследуется от базового класса View;
    2. Наследуется от одной или нескольких примесей.

    Итак, для расширения фунционала generic view нужно:
    1. Посмотреть в документации, какие примеси она содержит;
    2. Найти в этих примесях ту фунцию, в которой реализовано интересующее вас поведение;
    3. Переопределить эту фунцию.

    Все «вьюхи» как классы


    Использование class-based generic views мотивирует писать все «вьюхи» в виде классов, все из тех же соображений снижения повторяемости и структуризации. Если из function-based generic views вам часто не подходила ни одна — по причинам, описанным выше, то из class-based generic views вам всегда будет полезен View.

    View

    Все generic views наследуются от этого класса. В нем реализована функция dispatch(), принимающая request и возвращающая HttpReponse, собственно то, что и делала всегда view, будучи фунцией. Стоит заметить, что функция as_view() как раз возвращает указатель на dispatch(), что делает ясной аналогию с function-based views — в своем минималистичном варианте класс View — класс, содержащий всего одну функцию, которую и вызывает контроллер из URLCONF.

    Класс View умеет вызывать свои фунции get(), post() и т.п. в зависимости от типа запроса, передавая request туда.

    Пример


    Пример простой реализации «вьюхи» в виде класса можно посмотреть, например, здесь. Ниже приведен сложный пример — моя переделка примера из документации — гораздо более полно иллюстрирующий удобство написания «вьюх» в виде классов.

    Задача

    Проект требует, чтобы любые данные, запрашиваемые клиентом, могли быть возвращены как в виде обычной HTML-страницы, так и в виде JSON-объекта — в зависимости от GET-параметра запроса.

    Решение

    Нужно написать общий класс «вьюхи», умеющий представлять ответ в двух видах, и от него наследовать все последующие «вьюхи» в проекте.

    В django реализован TemplateResponseMixin, функция render_to_response() которого умеет рендерить указанный в template_name шаблон с переданным ей в качестве параметра контекстом.

    Напишем свою примесь, которая будет вместо HTML возвращать JSON:
    class JsonResponseMixin(object):
        def render_to_reponse(self, context):
            return http.HttpResponse(self.convert_context_to_json(context),
                                     content_type='application/json')
    
        def convert_context_to_json(self, context, extract_from_queryset=None):
            pass
    


    Функцию конвертации контекста в JSON нужно писать уже понимая, что этот контекст из себя представляет, поэтому она не реализована.

    Теперь напишем класс, который будет уметь возвращать и HTML, и JSON:
    class MixedView(View, JsonResponseMixin, TemplateResponseMixin):
        def get_context(self, request):
            pass
        
        def get(self, request, *args, **kwargs):
            context = self.get_context(request)
            if request.GET.get('format', 'html') == 'json' or self.template_name is None:
                return JsonResponseMixin.render_to_reponse(self, context)
            else:
                return TemplateResponseMixin.render_to_response(self, context)     
    


    Функция get переопределяет ту, что объявлена во View. В зависимости от GET-параметра, контекст отдается на рендеринг в одну из двух примесей, и возвращает JSON или HTML. В первом случае понадобится функция, определяющая перевод данного контекста в json. Во втором случае понадобится название шаблона, в котором этот контекст рендерить. Любая «вьюха»-наследник этой, должна иметь имплементации двух методов: get_context(request) — формирующего контекст, и convert_context_to_json(context) — переводящего его в JSON, и определенный template_name.

    Реализация конечного view могла быть, например, такой:
    class PublisherView(MixedView):
        def get_context(self, request):
            context = dict()
            context['publishers'] = Publisher.objects.all()
            return context
    
        template_name = 'publisher_list.html'
    
        def convert_context_to_json(self, context):
            json_context = dict()
            json_context['publisher_names'] = [p.name for p in context['publishers']]
            return json.dumps(json_context, encoding='utf-8', ensure_ascii=False)
    


    Заключение


    В этой статье я постарался прояснить то, чего мне нехватало в документации Django — идеи, стоящие за class-based views, и способ их использования. Надеюсь, практика такого повсеместного использования generic views и их последующего расширения в тех случаях, когда это необходимо, покажется кому-то полезной.

    Спасибо за внимание, буду рад любой критике.
    Share post

    Comments 22

      +3
      Удобно, когда нужно описать джва десятка почти одинаковых views. Еще я полюбил разделение HTTP GET / POST / DELETE в разные методы (еще до включения этой штуки в транк у меня был похожий фрагмент кода, реализующий именно это).

      С другой стороны, использовать class-based views повсеместно, на мой взгляд, оверкилл. Функции проще и прозрачнее классов, их легче читать и отлаживать.
        0
        Ну а зачем, «плодить» похожие функции, если в них меняется только одно действие, обработка формы к примеру.
        Да и отладить проще маленький метод, нежели вчитываться в большую функцию.
          0
          Если похожие, то да.
          0
          С помощью class-based вы можете разбить выполнение вьюхи на логические блоки общие для всего проекта, разложив их по методам класса. Это позволяет очень удобно расширять функциональность в дочерних классах, используя super() и прочие прелести. Мixins опять же.

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

            0
            А как именно вы рассовали get/post в разные методы?
            А то вот я привык уже к такому по gae, а как соорудить в джанге что-то не соображу.
          0
          get_context разве не должна возвращать context?
            0
            он возвращает словарь, из которого формируется context.
              +1
              Я имел в виду переменную context, которая не возвращалась в методе. Уже исправили.
                0
                Да, я прочел комментарий после того, как исправил сам. Спасибо.
            +9
            1. Если метод нужно реализовывать в классе-наследнике, то нужно не `pass`, а `raise NotImplementedError`
            2. Если вам нужно что-то сделать с queryset'ом, то это лучше делать в менеджере модели, а не во вьюхе.
            3. Сериализация (в том числе и в JSON) уже есть в Джанге.

            А еще смотрите, если вы решите использовать ваше `MixedView` для другой модели, то, вам придется написать:

            class ArticleView(MixedView):
                def get_context(self, request): #!
                    context = dict() #!
                    context['articles'] =Article.objects.all()
                    return context #!
            
                template_name = 'articles_list.html'
            
                def convert_context_to_json(self, context): #!
                    json_context = dict() #!
                    json_context['article_names'] = [p.name for p in context['articles']]
                    return json.dumps(json_context, encoding='utf-8', ensure_ascii=False) #!
            


            Восклицательными знаками я отметил строчки, которые придется копировать без изменений. Их 6 из 10! Просто anti-DRY. Можно, в качестве альтернативы, добавить еще один слой наследования и в конец запутать код.

            Мне кажется, что CBV в 95% случаев не нужны. Хитросплетения методов и цепочек наследования намного сложнее поддерживать, чем функции. Они не гибкие (если вы используете ListView, а однажды вам понадобится вывести не один а два списка чего-то, что тогда?). Если у вас реально сложная иерархия вьюх в проекте, то может быть CBV и подойдут, но много ли таких проектов.
              0
              Под «что-то делать с queryset-ом» вы имели ввиду [p.name for p in context['items']]?
              Как это грамотно прикрутить к менеджеру модели?
                +2
                Примерно так:

                from django.core import serializers
                
                class MyModelManager(models.Manager):
                    def to_json(self):
                        return serializers.serialize('json', self.all(), fields=('name',), ensure_ascii=False)
                
                class MyModel(models.Model):
                    # всякие поля тут
                    # ...
                    objects = MyModelManager()
                

                Ну и затем вызывать MyModel.objects.to_json().
                  0
                  Вот про нативную сериализацию я не знал, спасибо за подсказку. В таком случае можно написать общий сериализатор под все контексты, и опустить convert_context_to_json() вообще.
                0
                За 1 и 3 большое спасибо, а 2 не понял. Какие именно действия с QuerySet'ом тут нужно перенести в менеджер? Это ведь нужно делать, если предполагается еще где-то использовать такой запрос?

                > Если у вас реально сложная иерархия вьюх в проекте, то может быть CBV и подойдут, но много ли таких проектов.

                Дело быть не только в сложной иерархии вьюх в одном проекте, но и в их использовании в других проектах. Хочется, чтобы код, написанный для одного проекта, был бы полезен в других. Это такой межпроектный DRY. Понятно, что создавая лишний уровень абстракции использованием наследования мы усложняем отладку и общее понимание кода, однако мы же выигрываем в астрагировании — программист пишущий MixedView, например, не должен будет задумываться о том, что и как сериализуется.

                Мне нравится идея выделения отдельной части представления, ответственной за конечное преобразование данных в ответ. Потому что компоновка данных из базы, их предобработка по запросу — это отдельная задача, а преобразование запроса для просмотра в том или ином виде — совсем другая. И, как Вы подсказали, задача сериализации подготовленных для ответа данных в JSON, например, решается одинаково, независимо от вида этих данных.
                  0
                  По поводу 2, тут, скорее, дело вкуса (поэтому «лучше», а не «нужно» :-)), но во view этому коду точно не место. Можно в менеджер, можно в отдельный сериализатор. Даже если его предполагается использовать один раз, есть и другие причины. Например, чтобы протестировать, как работает сериализация, вам нужно «прогнать» view полностью, с контекстом, реквестом и прочим. Если вынести его куда-нибудь, тестирование становится приятным. Документировать код проще, глядя на менеджер, я сразу вижу, что он сериализуется в json, тут логика не так ясна. Сильное связывание опять же, получается, что я не могу сериализовать объект без атрибута name.

                  Плюс менеджеры не менее «реюзабельны», чем CBV, тот самый межпроектный DRY.

                  > Мне нравится идея выделения отдельной части представления, ответственной за конечное преобразование данных в ответ.
                  В вашем случае это HttpResponse(data, content_type='application/json'). Если очень необходимо это выделить, то почему бы не так?
                0
                context=super(MyClassView, self).get_context_data() вместо context=dict() не лучше ли?
                  0
                  тьфу, без _data
                    0
                    Это если у класса родителя есть такой метод, и он действительно возвращает dict-like объект. В данном случае это не так.

                    Но это хорошо показывает сложность поддержки CBV.
                      0
                      Ну во всех «новых генериках», которые пришли на смену func-based-ам оно есть: в ListView, DetailView и прочих. Имхо, это стандартная практика, которую предлагает django-team.
                        0
                        В статье речь не о генериках, а о CBV в целом. У базового класса View этого метода нет.
                    +2
                    CBV хороши для приложений, которые делаются для повторного использования. Сколько раз сталкивался: есть некое приложение, в нем вьюха, и хочется немного изменить ее поведение. И вот тут вьюхи на функциях предстают как черный ящик — на входе параметры, на выходе — HttpResponse, с которым уже мало что можно сделать. Хорошо, если автор предусмотрел какие-то параметры, с помощью которых можно кастомизировать поведению вьюхи. Иначе остается только переписывать весь ее код ради небольших изменений

                    Only users with full accounts can post comments. Log in, please.