Люк Плант (Luke Plant) — программист-фрилансер с многолетним стажем, один из ключевых разработчиков Django.
Когда-то я писал о своей неприязни к Class Based Views (CBV) в Django. Их использование заметно усложняет код и увеличивает его объём, при этом CBV мешают применять некоторые достаточно распространённые шаблоны (скажем, когда две формы представлены в одном view). И судя по всему, я не единственный из разработчиков Django, придерживающийся такой точки зрения.
Но в этом посте я хочу рассказать об ином подходе, который я применил в одном из проектов. Этот подход можно охарактеризовать одной фразой: «Создавайте свой собственный базовый класс».
При достаточно простых model view использование CBV в Django может сэкономить время. Но в более сложных случаях вы столкнётесь с рядом трудностей, как минимум, придётся погрузиться в изучение документации.
Избежать всего этого можно, например, с помощью упрощённой реализации CBV. Лично я пошёл ещё дальше и начал с нуля, написав собственный базовый класс, позаимствовав лучшие идеи и внедрив только то, что мне нужно.
Метод as_view, предоставляемый классом
Лично мне не нравится метод
Поэтому я отказался от этого метода в пользу простой функции
Также мне не нравится, что шаблоны автоматически именуются на основании имён моделей и т.д. Это программирование по соглашению, что излишне усложняет жизнь при поддержке кода. Ведь кому-то придётся грепать, чтобы выяснить, где же используется шаблон. То есть при использовании такой логики вы ДОЛЖНЫ ЗНАТЬ, где искать информацию о том, используется ли шаблон вообще и как он используется.
Гораздо легче управлять относительно единообразным набором (flat set) базовых классов, чем большим набором из классов-примесей (mixins) и базовых классов. Благодаря единообразности стека я могу не писать безумные хаки для прерывания наследования.
Помимо прочего, в CBV Django мне не нравится вынужденная многословность при добавлении новых данных в
На самом деле, обычно всё ещё хуже, поскольку добавляемые в
Подыскивая примеры на Github, я пересмотрел сотни образчиков кода наподобие этого:
Я не обращал на это особого внимания, пока не сообразил: люди используют стандартные генераторы/снипеты для создания новых CBV (пример 1, пример 2, пример 3). Если людям нужны подобные ухищрения, это означает, что вы создали слишком громоздкий API.
Могу посоветовать: представьте, какой бы вы хотели получить API, и реализуйте его. Например, для статического добавления в
А для динамического добавления:
Также мне хотелось бы автоматически аккумулировать любой
Также мне хотелось бы иметь возможность напрямую добавлять данные в
Само собой, при этом ничто не должно быть испорчено на уровне класса, а изоляция запроса не должна быть нарушена. При этом все методы должны работать предсказуемо и безо всяких затруднений. А заодно нельзя допустить возможность случайного изменения изнутри метода определяемого классом словаря
Когда вы закончите мечтать, то, вероятно, обнаружите, что ваш воображаемый API слишком трудно реализовать из-за особенностей самого языка, нужно его как-то модифицировать. Тем не менее, проблема решаема, хотя это и выглядит немного волшебством. Обычно определение метода в подклассе без использования
Я предпочитаю делать это более прозрачным образом, используя для атрибута класса и метода имя
В реализации используется несколько хитростей, и в первую очередь такая: с помощью
Обратите внимание, что метод TemplateView.handle крайне прост, он лишь вызывает другой метод, который и выполняет всю работу:
Это означает, что подклассу, определяющему
Кроме того, я использую ряд привязок (hooks) для обработки таких вещей, как AJAX-валидация при представлении формы, подгрузка RSS/Atom для представлений в виде списков, и т.д. Это выполняется довольно просто, поскольку я контролирую базовые классы.
Основная идея заключается в том, что вы не обязаны ограничиваться возможностями Django. В него не интегрировано глубоко ничего, что относится к CBV, поэтому ваши собственные реализации будут ничем не хуже, а то и лучше. Я рекомендую вам написать именно тот код, который нужен для вашего проекта, а затем создать базовый класс, который заставит его работать.
Недостаток этого подхода заключается в том, что вы не облегчите работу программистам, которые будут поддерживать ваш код, если они выучили API для Django CBV. Ведь в вашем проекте будет использоваться другой набор базовых классов. Однако преимущества всё же с лихвой компенсируют это неудобство.
Когда-то я писал о своей неприязни к 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. Ведь в вашем проекте будет использоваться другой набор базовых классов. Однако преимущества всё же с лихвой компенсируют это неудобство.