Pull to refresh
0

Мой подход к Class Based Views

Reading time5 min
Views9.4K
Original author: Luke Plant
Люк Плант (Luke Plant) — программист-фрилансер с многолетним стажем, один из ключевых разработчиков Django.

Когда-то я писал о своей неприязни к Class Based Views (CBV) в Django. Их использование заметно усложняет код и увеличивает его объём, при этом CBV мешают применять некоторые достаточно распространённые шаблоны (скажем, когда две формы представлены в одном view). И судя по всему, я не единственный из разработчиков Django, придерживающийся такой точки зрения.

Но в этом посте я хочу рассказать об ином подходе, который я применил в одном из проектов. Этот подход можно охарактеризовать одной фразой: «Создавайте свой собственный базовый класс».

При достаточно простых model view использование CBV в Django может сэкономить время. Но в более сложных случаях вы столкнётесь с рядом трудностей, как минимум, придётся погрузиться в изучение документации.

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

Заимствование хороших идей


Метод as_view, предоставляемый классом View в Django, вещь замечательная. Этот метод внедрили после многочисленных дискуссий для облегчения изоляции запроса путём создания нового экземпляра класса для обработки каждого нового запроса. Я с удовольствием позаимствовал эту идею.

Отказ от плохих идей


Лично мне не нравится метод dispatch, поскольку он предполагает совершенно разную обработку GET и POST, хотя они зачастую пересекаются (особенно в случаях обработки типичных форм). Кроме того, при просмотре отклонённых POST-запросов, когда достаточно просто проигнорировать определённые данные, этот метод требует написания дополнительного кода, что для меня является багом.

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

Также мне не нравится, что шаблоны автоматически именуются на основании имён моделей и т.д. Это программирование по соглашению, что излишне усложняет жизнь при поддержке кода. Ведь кому-то придётся грепать, чтобы выяснить, где же используется шаблон. То есть при использовании такой логики вы ДОЛЖНЫ ЗНАТЬ, где искать информацию о том, используется ли шаблон вообще и как он используется.

Выравнивание стека


Гораздо легче управлять относительно единообразным набором (flat set) базовых классов, чем большим набором из классов-примесей (mixins) и базовых классов. Благодаря единообразности стека я могу не писать безумные хаки для прерывания наследования.

Написание нужного API


Помимо прочего, в CBV Django мне не нравится вынужденная многословность при добавлении новых данных в context в достаточно простых ситуациях, когда вместо одной строки приходится писать четыре:

class MyView(ParentView):
    def get_context_data(self, **kwargs):
        context = super(MyView, self).get_context_data(**kwargs)
        context['title'] = "My title"  # Это единственная строка, которую я хочу написать!
        return context

На самом деле, обычно всё ещё хуже, поскольку добавляемые в context данные могут вычисляться с помощью другого метода и висеть на self, чтобы их мог найти get_context_data. К тому же, чем больше кода, тем легче сделать ошибку. Например, если вы забудете про вызов super, то всё может пойти наперекосяк.

Подыскивая примеры на Github, я пересмотрел сотни образчиков кода наподобие этого:

class HomeView(TemplateView):
    # ...

    def get_context_data(self):
        context = super(HomeView, self).get_context_data()
        return context

Я не обращал на это особого внимания, пока не сообразил: люди используют стандартные генераторы/снипеты для создания новых CBV (пример 1, пример 2, пример 3). Если людям нужны подобные ухищрения, это означает, что вы создали слишком громоздкий API.

Могу посоветовать: представьте, какой бы вы хотели получить API, и реализуйте его. Например, для статического добавления в context я хотел бы написать это:

class MyView(ParentView):
    context = {'title': "My title"}

А для динамического добавления:

class MyView(ParentView):
    def context(self):
        return {'things': Thing.objects.all()
                          if self.request.user.is_authenticated()
                          else Thing.objects.public()}

    # Или, возможно, используя lambda:
    context = lambda self: ...

Также мне хотелось бы автоматически аккумулировать любой context, определяемый ParentView, даже если я не вызываю super явным образом. В конце концов, нам почти всегда хочется добавлять данные в context. И, при необходимости, подкласс должен убирать специфические наследуемые данные, присваивая ключ None.

Также мне хотелось бы иметь возможность напрямую добавлять данные в context для любого метода в моём CBV. Например, настраивая/обновляя переменную экземпляра:

class MyView(ParentView):

    def do_the_thing(self):
        if some_condition():
            self.context['foo'] = 'bar'

Само собой, при этом ничто не должно быть испорчено на уровне класса, а изоляция запроса не должна быть нарушена. При этом все методы должны работать предсказуемо и безо всяких затруднений. А заодно нельзя допустить возможность случайного изменения изнутри метода определяемого классом словаря context.

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

Я предпочитаю делать это более прозрачным образом, используя для атрибута класса и метода имя magic_context. Так я не подкладываю свинью тем, кто будет потом поддерживать код. Если что-то называется magic_foo, то большинство людей полюбопытствуют, почему это оно «волшебное» и как оно работает.

В реализации используется несколько хитростей, и в первую очередь такая: с помощью reversed(self.__class__.mro()) извлекаются все super-классы и их атрибуты magic_context, а также итеративно обновляется содержащий их словарь.

Обратите внимание, что метод TemplateView.handle крайне прост, он лишь вызывает другой метод, который и выполняет всю работу:

class TemplateView(View):
    # ...
    def handle(self, request):
        return self.render({})

Это означает, что подклассу, определяющему handle для выполнения нужной логики, не нужно вызывать super. Ему достаточно напрямую вызвать такой же метод:

class MyView(TemplateView):
    template_name = "mytemplate.html"

    def handle(self, request):
        # логика здесь...
        return self.render({'some_more': 'context_data'})

Кроме того, я использую ряд привязок (hooks) для обработки таких вещей, как AJAX-валидация при представлении формы, подгрузка RSS/Atom для представлений в виде списков, и т.д. Это выполняется довольно просто, поскольку я контролирую базовые классы.

В заключение


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

Недостаток этого подхода заключается в том, что вы не облегчите работу программистам, которые будут поддерживать ваш код, если они выучили API для Django CBV. Ведь в вашем проекте будет использоваться другой набор базовых классов. Однако преимущества всё же с лихвой компенсируют это неудобство.
Tags:
Hubs:
Total votes 8: ↑8 and ↓0+8
Comments3

Articles

Information

Website
www.nixsolutions.com
Registered
Founded
1994
Employees
1,001–5,000 employees