Компания
89,49
рейтинг
13 января в 11:59

Разработка → Мой подход к Class Based Views перевод

Люк Плант (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. Ведь в вашем проекте будет использоваться другой набор базовых классов. Однако преимущества всё же с лихвой компенсируют это неудобство.
Автор: @NIX_Solutions Luke Plant
NIX Solutions
рейтинг 89,49

Комментарии (3)

  • 0
    Очень спорная статья. В «заключении» автор немного поправился, сказав, что в каждом случае надо думать головой, тут он прав…

    Но CBV, на мой взгляд, отличная идея. Лично мне очень удобно создать класс, подмешать пару mixins и получить поведение из уже готовых классов. В половине случаев этого хватает. Если не хватает — надо подумать — а не делаю ли я все слишком сложным?

    Лапша из методов вместо классов (Flask?), на мой, опять же, взгляд — только запутывает. В классах даже по имени можно понять что это View, а функция может быть просто вспомогательной.

    Смешно получилось про magic* функции — автор сначала возмущается тем, что неявно задаются имена шаблонов, а потом предлагает некие неявные магические функции.

    Явный вызов super(), как мне кажется, это вообще must have. Иногда не нужно его вызывать, если все, что мне надо я генерирую в контекст сам. Иногда я подмешиваю что-то в контекст, полученный из super().get_context(), но иногда нужно выполнить какие-то действия до того, как передавать выполнение родителю. Именно это и удобно в Python — управляемый вызов методов родителя.

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

    «Пишите свои базовые классы» — это вообще очевидно уже ко второму проекту на Django. Для каждого проекта он может быть своим, но тем не менее… И дает гибкость при наследовании.

    В этом и сила CBV.
    • 0
      Насчет не явности магических функций — они такие же не явные, как и любая другая самописная функция. Только для просмотра и понимания их реализации не нужно лезть в исходники фреймворка, или знать о конкретных его умолчаниях. Хотя я вообще слабо представляю, зачем человек, не знающий таких базовых умолчаний полезет поддерживать или дебажить проект на джанге.

      По поводу добавления контекста категорически согласен с автором. Весьма распространенная операция, и каждый раз пилить context = super(MyView, self).get_context_data() попросту утомительно.

      Копипаста функций в свой класс — нарушение принципа DRY. Так же возникает вопрос, зачем тогда вообще CBV, если не для наследования?

      У Вас с автором разный подход. Вы ищете, как сделать в рамках фреймворка, а он ищет, как сделать проще.
      • +1
        Python-way — явное лучше неявного.
        Упрощая дальше, можно дойти до RubyOnRails (no offense!), где магия на каждом шагу и у вас голова пухнет от понимания того, сколько всего на самом деле может выполняться в приложении неявно кроме вашего кода.
        Не поймите меня неправильно, я не настаиваю на том, чтобы все указывать явно, но золотая середина должна быть.
        Любой способ, удобный в одном случае — может стать совершенно неудобным в другом.

        Про спорный момент с контекстом: можно context сделать атрибутом объекта, как request, тогда он может автоматически заполняться родителем в определенный момент. И в любом месте self.context будет доступен уже полузаполненным — используйте.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка