Представления на основе классов или функции в Django?

В этой статье мы рассмотрим различия между представлениями Django на основе классов (CBV) и представлениями на основе функций (FBV). Мы сравним и противопоставим их, а также рассмотрим плюсы и минусы каждого подхода (наряду со встроенными в Django общими представлениями на основе классов). К концу у вас должно быть хорошее понимание того, когда использовать один подход вместо другого.

Введение

Одним из основных применений Django (и, по сути, любого другого веб-фреймворка) является предоставление HTTP-ответов в ответ на HTTP-запросы. Django позволяет нам делать это с помощью так называемых представлений. Представление - это просто вызываемый объект, который принимает запрос и возвращает ответ.

Django изначально поддерживал только представления на основе функций (FBV), но их было трудно расширять, они не использовали преимущества объектно-ориентированного программирования (ООП) и не были DRY. Именно поэтому разработчики Django решили добавить поддержку представлений на основе классов (CBVs). CBV используют принципы ООП, что позволяет нам использовать наследование, повторно использовать код и в целом писать более качественный и чистый код.

Необходимо помнить, что CBV не предназначены для замены FBV. Все, чего можно достичь с FBV, можно достичь и с CBV. У каждого из них есть свои плюсы и минусы.

Наконец, Django предлагает готовые или общие CBV, которые обеспечивают решение общих проблем. Они имеют удобные для программистов названия и предлагают решения таких проблем, как отображение данных, редактирование данных и работа с данными на основе даты. Они могут использоваться самостоятельно или наследоваться в пользовательских представлениях.

Давайте рассмотрим различные типы представлений и узнаем, когда какой из них уместно использовать.

Представления на основе функций (FBV)

По своей сути FBVs - это просто функции. Их легко читать и работать с ними, поскольку вы можете видеть, что именно происходит. Благодаря своей простоте, они отлично подходят для начинающих пользователей Django. Поэтому, если вы только начинаете работать с Django, рекомендуется иметь некоторые рабочие знания о FBVs, прежде чем погружаться в CBVs.

Плюсы и минусы

Плюсы

  • Явный поток кода (у вас есть полный контроль над тем, что происходит)
  • Проста в реализации
  • Легко понять
  • Отлично подходит для уникальной логики представления
  • Легко интегрировать с декораторами

Против

  • Много повторяющегося (бойлерплейт) кода
  • Обработка HTTP методов через условное ветвление
  • Не используют преимущества ООП
  • Труднее поддерживать

Быстрый пример

Пример FBV выглядит следующим образом:

from django.shortcuts import render, redirect
from django.views import View


def task_create_view(request):
    if request.method == 'POST':
        form = TaskForm(data=request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse('task-list'))

    return render(request, 'todo/task_create.html', {
        'form': TaskForm(),
    })

Это представление принимает request, выполняет некоторую логику и возвращает HttpResponse. Просто взглянув на код, мы можем увидеть первый недостаток: условное ветвление. Для каждого метода HTTP мы должны создать отдельную ветвь. Это может увеличить сложность кода и привести к спагетти-коду.

Следующим недостатком FBV является то, что они плохо масштабируются. По мере того, как ваша кодовая база будет становиться все больше и больше, вы заметите много повторяющегося (boilerplate) кода для работы с моделями (особенно с операциями CRUD). Попробуйте представить себе, насколько представление для создания статей будет отличаться от примера выше... Они были бы практически одинаковыми.

Для того чтобы использовать FBV, мы должны зарегистрировать их в файле urls.py следующим образом:

urlpatterns = [
    path('create/', task_create_view, name='task-create'),
]

Вы должны выбрать FBV, если вы работаете над сильно адаптированной логикой представления. Другими словами, FBV - это отличный вариант использования для представления, которое не разделяет много кода с другими представлениями. Несколько реальных примеров использования FBV: представление статистики, представление диаграммы и представление сброса пароля.

Todo App (использование FBVs)

Давайте рассмотрим, как простое приложение todo, позволяющее выполнять CRUD-операции, можно написать, используя только FBVs.

Сначала мы инициализируем наш проект, определим наши модели, создадим HTML-шаблоны, а затем начнем работать над views.py. Вероятно, в итоге у нас получится что-то вроде этого:

# todo/views.py

from django.shortcuts import render, get_object_or_404, redirect

from .forms import TaskForm, ConfirmForm
from .models import Task


def task_list_view(request):
    return render(request, 'todo/task_list.html', {
        'tasks': Task.objects.all(),
    })


def task_create_view(request):
    if request.method == 'POST':
        form = TaskForm(data=request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse('task-list'))

    return render(request, 'todo/task_create.html', {
        'form': TaskForm(),
    })


def task_detail_view(request, pk):
    task = get_object_or_404(Task, pk=pk)
    return render(request, 'todo/task_detail.html', {
        'task': task,
    })


def task_update_view(request, pk):
    task = get_object_or_404(Task, pk=pk)

    if request.method == 'POST':
        form = TaskForm(instance=task, data=request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse('task-detail', args={pk: pk}))

    return render(request, 'todo/task_update.html', {
        'task': task,
        'form': TaskForm(instance=task),
    })


def task_delete_view(request, pk):
    task = get_object_or_404(Task, pk=pk)

    if request.method == 'POST':
        form = ConfirmForm(data=request.POST)
        if form.is_valid():
            task.delete()
            return HttpResponseRedirect(reverse('task-list'))

    return render(request, 'todo/task_delete.html', {
        'task': task,
        'form': ConfirmForm(),
    })

Вы можете получить полный исходный код на GitHub.

В итоге мы получили простую и понятную логику представления. Вы не сможете сильно улучшить этот код.

Представления на основе классов (CBV)

Виды на основе классов, которые были введены в Django 1.3, предоставляют альтернативный способ реализации представлений в виде объектов Python вместо функций. Они позволяют нам использовать принципы ООП (самое главное - наследование). Мы можем использовать CBVs для обобщения частей нашего кода и извлечения их в виде представлений суперкласса.

CBV также позволяют вам использовать встроенные в Django общие представления на основе классов и миксины, которые мы рассмотрим в следующем разделе.

Плюсы и минусы

Плюсы

  • Являются расширяемыми
  • Они используют преимущества концепций ООП (самое главное - наследование)
  • Отлично подходят для написания CRUD представлений
  • Более чистый и многократно используемый код
  • Встроенные в Django общие CBVs
  • Они похожи на представления REST фреймворка Django

Против

  • Неявный поток кода (многое происходит в фоновом режиме)
  • Используется много миксинов, что может запутать
  • Более сложный и трудный для освоения
  • Декораторы требуют дополнительного импорта или переопределения кода

Для получения дополнительной информации просмотрите Каковы плюсы и минусы использования представлений на основе классов в Django/Python?

Быстрый пример

Давайте перепишем наш предыдущий пример FBV как CBV:

from django.shortcuts import render, redirect
from django.views import View


class TaskCreateView(View):

    def get(self, request, *args, **kwargs):
        return render(request, 'todo/task_create.html', {
            'form': TaskForm(),
        })

    def post(self, request, *args, **kwargs):
        form = TaskForm(data=request.POST)
        if form.is_valid():
            task = form.save()
            return redirect('task-detail', pk=task.pk)

        return self.get(request)

Мы видим, что этот пример не сильно отличается от подхода FBV. Логика более или менее одинакова. Основное отличие заключается в организации кода. Здесь каждый HTTP-метод рассматривается отдельным методом вместо условного ветвления. В CBV вы можете использовать следующие методы: get, post, put, patch, delete, head, options, trace.

Еще одним плюсом такого подхода является то, что HTTP-методы, которые не определены, автоматически возвращают 405 Method Not Allowed ответ.

При использовании FBVs вы можете использовать один из разрешенных декораторов методов HTTP, например @require_http_methods, чтобы добиться того же самого.

Поскольку URL-резольвер Django ожидает вызываемую функцию, нам нужно вызвать as_view() при регистрации их в urls.py:

urlpatterns = [
    path('create/', TaskCreateView.as_view(), name='task-create'),
]

Поток кода

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

  1. Диспетчер URL Django направляет HttpRequest на MyView.
  2. Диспетчер URL Django вызывает as_view() на MyView.
  3. as_view() вызывает setup() и dispatch().
  4. dispatch() вызывает метод для определенного метода HTTP или http_method_not_allowed().
  5. Возвращается HttpResponse.

Class based-views code flow

Todo App (использование CBVs)

Теперь давайте перепишем наше приложение todo, чтобы использовать только CBVs:

# todo/views.py

from django.shortcuts import render, get_object_or_404, redirect
from django.views import View

from .forms import TaskForm, ConfirmForm
from .models import Task


class TaskListView(View):

    def get(self, request, *args, **kwargs):
        return render(request, 'todo/task_list.html', {
            'tasks': Task.objects.all(),
        })


class TaskCreateView(View):

    def get(self, request, *args, **kwargs):
        return render(request, 'todo/task_create.html', {
            'form': TaskForm(),
        })

    def post(self, request, *args, **kwargs):
        form = TaskForm(data=request.POST)
        if form.is_valid():
            task = form.save()
            return redirect('task-detail', pk=task.pk)

        return self.get(request)


class TaskDetailView(View):

    def get(self, request, pk, *args, **kwargs):
        task = get_object_or_404(Task, pk=pk)

        return render(request, 'todo/task_detail.html', {
            'task': task,
        })


class TaskUpdateView(View):

    def get(self, request, pk, *args, **kwargs):
        task = get_object_or_404(Task, pk=pk)
        return render(request, 'todo/task_update.html', {
            'task': task,
            'form': TaskForm(instance=task),
        })

    def post(self, request, pk, *args, **kwargs):
        task = get_object_or_404(Task, pk=pk)
        form = TaskForm(instance=task, data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('task-detail', pk=task.pk)

        return self.get(request, pk)


class TaskDeleteView(View):

    def get(self, request, pk, *args, **kwargs):
        task = get_object_or_404(Task, pk=pk)
        return render(request, 'todo/task_confirm_delete.html', {
            'task': task,
            'form': ConfirmForm(),
        })

    def post(self, request, pk, *args, **kwargs):
        task = get_object_or_404(Task, pk=pk)
        form = ConfirmForm(data=request.POST)
        if form.is_valid():
            task.delete()
            return redirect('task-list')

        return self.get(request, pk)

Также не забудем сделать наш urls.py вызывающим as_view():

# todo/urls.py

from django.urls import path

from .views import TaskListView, TaskDetailView, TaskCreateView, TaskUpdateView, TaskDeleteView


urlpatterns = [
    path('', TaskListView.as_view(), name='task-list'),
    path('create/', TaskCreateView.as_view(), name='task-create'),
    path('<int:pk>/', TaskDetailView.as_view(), name='task-detail'),
    path('update/<int:pk>/', TaskUpdateView.as_view(), name='task-update'),
    path('delete/<int:pk>/', TaskDeleteView.as_view(), name='task-delete'),
]

Вы можете получить полный исходный код на GitHub.

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

Виды в Django, основанные на общих классах

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

Быстрый пример

Давайте рассмотрим пример:

from django.views.generic import CreateView


class TaskCreateView(CreateView):
    model = Task
    context_object_name = 'task'
    fields = ('name', 'description', 'is_done')
    template_name = 'todo/task_create.html'

Мы создали класс с именем TaskCreateView и унаследовали от него CreateView. Тем самым мы получили много функциональности, почти без кода. Теперь нам просто нужно установить следующие атрибуты:

  1. model определяет, с какой моделью Django работает представление.
  2. fields используется Django для создания формы (альтернативно, мы могли бы предоставить form_class).
  3. template_name определяет, какой шаблон использовать (по умолчанию /<app_name>/<model_name>_form.html).
  4. context_object_name определяет контекстный ключ, под которым экземпляр модели передается шаблону (по умолчанию object).
  5. success_url определяет, куда пользователь будет перенаправлен при успехе (альтернативно, вы можете установить get_absolute_url в вашей модели).

Для получения дополнительной информации об общих CBV обратитесь к официальной документации.

Как вы, вероятно, догадались, за кулисами с общими CBV происходит гораздо больше магии. Они могут быть запутанными даже для опытных разработчиков Django. Однако, когда вы разберетесь с ними, вы, вероятно, почувствуете себя волшебником.

Вам следует использовать общие CBV для представлений, которые выполняют общие задачи (например, CRUD-операции). Если вашему представлению нужно сделать что-то, что не покрывается CBV, используйте миксины или представление на основе функций.

Встроенные типы CBV в Django

На момент написания статьи Django поставляется с приличным количеством общих CBV, которые мы можем разделить на три категории:

Generic Display Views Generic Editing Views Generic Date-based Views

Designed to display data.

Provide a foundation for editing content.

Allow in-depth displaying of date-based data.

В ближайшее время мы рассмотрим практические примеры их использования.

View Mixins

Каждое типовое представление создано для решения одной задачи (например, отображение информации, создание/обновление чего-либо). Если ваше представление должно делать что-то большее, вы можете создать свое представление, используя миксины. Миксины - это классы, которые предлагают отдельные функциональные возможности, и их можно комбинировать для решения практически любой задачи.

Вы также можете выбрать в качестве основы общий CBV, а затем включить в него дополнительные миксины.

Даже общие CBV в Django состоят из миксинов. Давайте посмотрим на диаграмму CreateView:

CreateView diagram

Мы видим, что он использует ряд миксинов, таких как ContextMixin, SingleObjectMixin и FormMixin. Они имеют удобные для программистов названия, поэтому вы должны иметь общее представление о том, что делает каждый из них, исходя из их названия.

Миксины требуют много времени для освоения и часто могут быть запутанными. Если вы только начинаете работать с миксинами, я бы посоветовал вам начать с чтения Using mixins with class-based views.

Todo App (используя общие CBVs Django)

Теперь давайте перепишем приложение todo в последний раз, используя общие представления Django, основанные на классах:

# todo/views.py

from django.views.generic import ListView, DetailView, DeleteView, UpdateView, CreateView


class TaskListView(ListView):
    model = Task
    context_object_name = 'tasks'


class TaskCreateView(CreateView):
    model = Task
    context_object_name = 'task'
    fields = ('name', 'description', 'is_done')
    template_name = 'todo/task_create.html'


class TaskDetailView(DetailView):
    model = Task
    context_object_name = 'task'


class TaskUpdateView(UpdateView):
    model = Task
    context_object_name = 'task'
    fields = ('name', 'description', 'is_done')
    template_name = 'todo/task_update.html'


class TaskDeleteView(DeleteView):
    model = Task
    context_object_name = 'task'
    success_url = '/'

Вы можете получить полный исходный код на GitHub.

Используя общие CBVs Django, мы разделили наш код пополам. В дальнейшем код становится намного чище и легче поддерживать, но его может быть намного сложнее читать и понимать, если вы новичок в общих CBV.

Заключение

Один тип зрения не лучше другого. Все зависит от ситуации и личных предпочтений. Иногда лучше FBV, а иногда лучше CBV. Постарайтесь запомнить их плюсы и минусы, чтобы принять правильное решение при написании конкретного вида. Если вы все еще не совсем уверены, когда и что использовать, вы можете немного попрактиковаться, преобразуя FBV в CBV и наоборот. Кроме того, вы можете использовать эту блок-схему:

CBV vs FBV Flowchart

Лично я предпочитаю FBV для небольших проектов (которые невозможно решить с помощью общих CBV) и выбираю CBV при работе с большими кодовыми базами.

Вернуться на верх