Люк Плант (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. Ведь в вашем проекте будет использоваться другой набор базовых классов. Однако преимущества всё же с лихвой компенсируют это неудобство.
ссылка на оригинал статьи http://habrahabr.ru/post/274995/
Добавить комментарий