Миксины для “классов” в JavaScript
Одинаковый код в нескольких местах — это боль. Сегодня я напишу пару слов про повторяющиеся куски классов. Люди давно придумали решение — можно вынести одинаковые методы и свойства в общий базовый класс, а если такового нет — использовать примеси. Существует миллион реализаций данного паттерна для JavaScript, я хочу детально остановиться на подходе, когда миксин попадает в цепочку наследования.
Проблема в картинках
Начнем с визуализации нашей проблемы. Допустим у нас есть два базовых класса и от них наследуются два дочерних класса.
В какой-то момент в дочерних классах появляется необходимость в одинаковом функционале. Обычная копипаста будет выглядеть на нашей схеме вот так:
Очень часто бывает, что данный функционал не имеет ничего общего с родительскими классами, поэтому выносить его в какой-то базовый класс нелогично и неправильно. Вынесем его в отдельное место — миксин. С точки зрения языка миксин может быть обычным объектом.
А теперь обсудим момент, ради которого написана вся статья — как правильно замешивать наш миксин в классы.
Исходя из собственного опыта, могу заявить, что самый удобный способ — это создание временного класса на основе миксина и подстановка его в очередь наследования.
Плюсы данного подхода
Пишем код
Во всех последующих примерах будет использоваться конкретная реализация — библиотека Backbone.Mix. Посмотрев код, вы обнаружите, что он чрезвычайно прост, поэтому вы можете легко адаптировать его для своего любимого фреймворка.
Давайте посмотрим, как применять миксины, встраивающиеся в цепочку наследования, в реальной жизни и прочувствуем плюсы данного подхода на практике. Представьте, что вы пишете сайт )) и на вашем сайте есть разные штуки, которые можно закрывать — попапы, хинты и т.п. Все они должны слушать клик по элементу с CSS классом close и скрывать элемент. Миксин для этого может выглядеть так:
Вмешиваемся.
Довольно просто, не правда ли? Теперь наша цепочка наследования выглядит так:
Здесь и далее в примерах используется библиотека backbone-super
Примеси, которые не мешают..
… а помогают. Бывает замес выходит не хилый, и одним миксином не обойтись. Например, представьте что мы — крутые пацаны, и пишем лог в IndexedDB, а еще у нас для этого свой миксин — Loggable 
Тогда к попапу мы будем мешать уже два миксина:
Синтаксис вроде не сложный. На схеме это будет выглядеть так:
Как видите, цепочка наследования выстроится в зависимости от порядка подключения миксинов.
Зависимые миксины
А теперь представьте ситуацию, что к нам подходит наш аналитик и сообщает, что хочет собирать статистику по всем закрытиям попапов, хинтов — всего, что может закрываться. Конечно же, у нас давно есть миксин Trackable для таких случаев, с того времени, как мы делали регистрацию на сайте.
И в цепочке наследования Trackable должен оказаться раньше, чем Closable :
Код для миксинов с зависимостями немного усложнится:
Документируй миксины правильно
На этом, пожалуй всё, счастливого вмешивания!
На этом занятии мы с вами вынесем общий код классов представлений в отдельный класс, который можно воспринимать как миксин (mixin). Те из вас, кто хорошо знаком с ООП уже знают, что такое миксины и для чего они служат. Но я все же сделаю небольшую ремарку и подробнее поясню этот момент.
Для материальных товаров все эти характеристики имеют смысл:
Но для информационных, таких как электронная книга или приложение для смартфона, габариты и вес не определены. Тем не менее, система магазина в целом, обращается к этим свойствам объектов (товаров). Как сделать так, чтобы каждый объект, представляющий товар, имел по умолчанию все нужные свойства? Конечно, мы можем непосредственно в классе их прописать:
Но это начинает плохо работать, при наличии разветвленной иерархии объектов, когда каждый класс товара представляется отдельным классом:
Тогда пришлось бы в каждом прописывать эти методы, что нехорошо. Поэтому, как вы уже догадались, все это выносится в базовый класс, например, Goods. Но, иногда, наборы разрозненных классов не имеют единого базового, либо базовый класс находится на таком уровне абстракции, что в него писать такие конкретные методы – прямой путь к мешанине кода. Вот как раз для таких случаев, когда нужно дополнительно к уже существующей иерархии добавить какие-либо общие для разнородных классов данные и/или методы и применяется механизм миксинов.
В разных языках программирования миксины реализуются по разному. В частности, в Python, благодаря наличию механизма множественного наследования, примеси можно добавлять в виде отдельного базового класса:
Например, наши классы представлений можно дополнительно унаследовать от класса DataMixin, который мы отдельно определим. Причем, этот класс лучше прописывать первым в списке наследования:
Так как в нем могут быть атрибуты и методы, которые, затем, используются конструктором следующего класса – ListView. То есть, в Python, класс, записанный первым, первым и обрабатывается. Поэтому данные базового класса DataMixin переопределят (при необходимости) атрибуты следующего класса ListView.
Давайте теперь определим наш класс DataMixin и уберем дублирование кода из классов представлений. Первый вопрос, где прописать этот класс? Обычно, в Django все дополнительные, вспомогательные классы объявляют в отдельном файле приложения utils.py. Мы так и поступим. Создадим этот файл и в нем запишем класс DataMixin, следующим образом:
Обратите внимание, я перенес сюда и главное меню, т.к. оно используется напрямую классом DataMixin. И вначале идет импорт моделей, т.к. мы используем класс Category для получения всех категорий.
Если вы помните, мы категории в шаблоне base.html сейчас отображаем с помощью созданного нами тега show_categories. Это был искусственный пример, демонстрирующий возможность создания пользовательских тегов, теперь я его уберу и вместо него буду использовать переменную cats, которую передадим в шаблон. Соответственно, в шаблоне вернем строчки для отображения рубрик:
Итак, что же делает класс DataMixin? Смотрите, в нем объявлен вспомогательный метод get_user_context() для формирования контекста шаблона по умолчанию. Также, при необходимости, мы можем передавать ему именованные аргументы, которые также будут помещаться в контекст. Благодаря этому методу, нам не придется в классах представлений каждый раз прописывать ссылки на главное меню и категории.
Итак, класс миксин объявлен, осталось прописать его в качестве базового у классов представлений. Для этого в файле views.py мы импортируем модуль utils.py:
И унаследуем класс WomenHome также и от DataMixin:
Осталось изменить метод get_context_data(), следующим образом:
Смотрите, мы здесь вызываем метод get_user_context() класса DataMixin, указав, дополнительно параметр title. Получаем сформированный словарь c_def со всеми стандартными ключами и объединяем его со словарем context. В конце, возвращаем объединенные данные. Все, дублирование в методе get_context_data() устранено.
По аналогии, меняем и все остальные классы представлений:
Все, переходим на сайт и видим, что страницы отображаются также как и ранее, но теперь все работает совместно с классом DataMixin. Вот пример того, как миксины в Django позволяют устранять дублирование кода в классах представлений.
Конечно, в класс DataMixin можно прописывать не только методы, но и общие атрибуты, если они есть, то есть, выносить любую общую информацию.
Миксины фреймворка Django
В Django есть стандартные миксины, которые можно использовать совместно с классами представлений. Использовать их достаточно просто, я покажу пример одного такого миксина:
который позволяет ограничить доступ к странице для неавторизованных пользователей. Подробную информацию об этом классе можно посмотреть по этой ссылке:
Давайте вначале выполним его импорт в файле views.py:
А, затем, добавим в класс AddPage:
Причем, прописывать желательно самым первым, т.к. он имеет наибольшую важность. Хотя, в нашем случае первые два миксина можно записывать в любом порядке, они никак между собой не пересекаются.
По идее все. Если теперь попробовать выйти из админки (то есть, стать не зарегистрированным пользователем) и перейти на добавление поста, то увидим страницу с кодом 404. Давайте улучшим этот поведение, сделаем его более дружественным. Для этого, в классе AddPage (после добавления миксина LoginRequiredMixin) можно прописать специальный атрибут:
который указывает адрес перенаправления для незарегистрированного пользователя. В данном случае, мы его отправляем в админ-панель. Переходим на главную страницу, затем, на добавление статьи и автоматом перенаправляемся на форму авторизации.
Конечно, прописывать конкретный URL-адрес – это не лучшая практика, поэтому, давайте, воспользуемся функцией reverse_lazy для формирования маршрута по его имени:
Также, вместо перенаправлений, можно генерировать страницу с кодом 403 – доступ запрещен. Для этого достаточно прописать атрибут:
Если похожий функционал нужно реализовать для функций представлений, а не классов, то здесь уже используется декоратор login_required, например, так:
Теперь эта страница доступна только авторизованным пользователям. Я уберу его, т.к. он здесь не к месту. Это просто демонстрация того, как ограничить доступ, работая с функциями представлениями.
Наконец, давайте сделаем отображение пункта «Добавить статью» только для авторизованных пользователей. Для этого я в классе DataMixin буду удалять этот пункт, если пользователь не авторизован:
Здесь используется объект request, у которого имеется объект user, а у того, в свою очередь, специальный булевый атрибут is_authenticated, указывающий на авторизацию текущего пользователя (если True – авторизован, False – в противном случае). Подробнее об этом также можно посмотреть на странице:
Ну и в заключение этого занятия, давайте сделаем вывод только тех рубрик, которые содержат статьи. Для этого мы будем выбирать рубрики с использованием агрегирующей функции:
Как работает эта строчка мы говорили на предыдущем занятии. Далее, в шаблоне base.html пропишем проверку при выводе рубрик:
Все, теперь у нас появляются только те рубрики, у которых есть статьи, что более логично.
Видео по теме
#2. Модель MTV. Маршрутизация. Функции представления
#3. Маршрутизация, обработка исключений запросов, перенаправления
#4. Определение моделей. Миграции: создание и выполнение
#6. Шаблоны (templates). Начало
#7. Подключение статических файлов. Фильтры шаблонов
#8. Формирование URL-адресов в шаблонах
#9. Создание связей между моделями через класс ForeignKey
#10. Начинаем работу с админ-панелью
#11. Пользовательские теги шаблонов
#12. Добавляем слаги (slug) к URL-адресам
#13. Использование форм, не связанных с моделями
#14. Формы, связанные с моделями. Пользовательские валидаторы
#15. Классы представлений: ListView, DetailView, CreateView
#16. Основы ORM Django за час
#18. Постраничная навигация (пагинация)
#19. Регистрация пользователей на сайте
#20. Делаем авторизацию пользователей на сайте
#21. Оптимизация сайта с Django Debug Toolbar
#22. Включаем кэширование данных
#23. Использование капчи captcha
#24. Тонкая настройка админ панели
#25. Начинаем развертывание Django-сайта на хостинге
#26. Завершаем развертывание Django-сайта на хостинге
© 2021 Частичное или полное копирование информации с данного сайта для распространения на других ресурсах, в том числе и бумажных, строго запрещено. Все тексты и изображения являются собственностью сайта
Что такое миксины в Python и как их использовать.
В Python нет какого либо специального синтаксиса для поддержки миксинов, по этому классы миксинов, легко можно перепутать с обычными классами, но при этом у них действительно очень большая как семантическая, так и реальная разница с обычными классами.
Так как для реализации поведения миксинов используется простое множественное наследование, то это требует от программиста большой дисциплины, поскольку нарушает одно из основных допущений для миксинов: их ортогональность к дереву наследования, т. е. классы, не зависят друг от друга. В Python миксины живут в обычном дереве наследования, предоставляя дополнительную функциональность и избегают создания иерархий, которые слишком сложны для понимания программистом.
Пример множественного наследования.
Пример использования класса миксина/примеси.
В отличие от приведенного выше примера, класс миксина не предназначен для использования отдельно. Он предоставляет новые методы или переопределяет имеющиеся методы.
Например, в стандартной библиотеке Python, в модуле socketserver есть несколько миксинов. Выдержка из документации:
С помощью этих классов миксинов могут быть созданы поточные версии каждого типа сервера. Например, ThreadingUDPServer создается следующим образом:
Простой пример для понимания поведения классов миксинов.
Иногда молодые программисты не до конца понимают принцип MRO в Python и по этому в некоторых случаях не правильно используют классы миксинов.
Множественное наследование и миксины в Python
Недавно я пересмотрел три своих старых статьи о представлениях на основе классов Django (class-based views), которые написал для своего блога, обновив их до Django 3.0 (вы можете найти их здесь), и еще раз обнаружил, большое количество кода использующего классы mixin для улучшения повторного использования кода. По своему опыту я понял, что миксины не очень популярны в Python, поэтому решил изучить их лучше, тем самым освежив свои знания теории ООП.
Чтобы полностью оценить содержание поста, убедитесь, что вы усвоили два столпа ООП: делегирование, в частности, как оно реализуется посредством наследования, и полиморфизм. Этот пост подробнее расскажет о делегировании а этот пост о полиморфизме.
Множестенное наследование: благодать и проклятье
Общая концепция
Чтобы обсудить миксины, мы должны начать с одного из самых спорных вопросов в мире ООП: множественное наследование. Это естественное расширение концепции простого наследования, когда класс автоматически делегирует разрешенные метода и атрибуты другому классу (от родительского классу к дочернему).
Позвольте мне заявить еще раз, поскольку это важно для остальной части обсуждения: наследование — это просто механизм автоматического делегирования.
Делегирование было введено в ООП как способ уменьшить дублирование кода. Когда объекту нужна определенная функция, он просто делегирует ее другому классу (явно или неявно), поэтому код пишется только один раз.
Давайте рассмотрим пример сайта управления кодом (совершенно вымышленный и не вдохновленный каким-либо существующим продуктом). Давайте предположим, что мы создали следующую иерархию
которая позволяет нам помещать в pull request только определенный код, требуемый этим элементом. Вызовы методов и делегирование — это не что иное, как сообщения между объектами, поэтому иерархия делегирования — это простая сетевая система.
К сожалению, использование наследования вместо композиции часто приводит к системам, которые, как это ни парадоксально, увеличивают дублирование кода. Основная проблема заключается в том, что наследование может напрямую делегироваться только одному классу (родительскому классу), в отличие от композиции, где объект может делегировать любому количеству других классов. Это ограничение наследования означает, что у нас может быть класс, который наследует от другого, потому что ему нужны некоторые его функции, но при этом так же получает все другие функции, которые он не хочет или не должен иметь.
Давайте продолжим пример портала управления кодом и рассмотрим проблему, которая представляет собой элемент, который мы хотим сохранить в системе, но который пользователь не может просмотреть. Если мы создадим иерархию, как это
в итоге мы помещаем функции, связанные с процессом проверки, в объект, который не должен иметь их. Стандартное решение этой проблемы состоит в том, чтобы увеличить глубину иерархии наследования и получать их от нового более простого предка.
Тем не менее, этот подход перестает быть жизнеспособным, как только объект должен наследовать от данного класса, но не от родителя этого класса. Например, элемент, который должен быть рецензируемым, но не назначаемым, например, best practice, которую мы хотим добавить на сайт. Если мы хотим продолжать использовать наследование, единственным решением на этом этапе является дублирование кода, который реализует рецензируемую природу элемента (или код, который реализует назначаемую функцию) и создание двух разных иерархий классов.
Обратите внимание, что это даже не принимает во внимание, что новый reviewable item может нуждаться в атрибутах assignable item, что требует другого уровня глубины в иерархии, где мы выделяем эти функции в более общем классе. Так что, к сожалению, есть вероятность, что это только первый из многих компромиссов, которые нам придется принять, чтобы сохранить систему в стабильном состоянии, если мы не сможем изменить наш подход.
Вышеупомянутая ситуация может быть затем решена с помощью pull request наследования как от класса, который предоставляет функции assign, так и от класса, который реализует функции reviewable.
Вообще говоря, множественное наследование вводится, чтобы дать программисту возможность продолжать использовать наследование без дублирования кода, сохраняя иерархию классов проще и чище. В конце концов, все, что мы делаем в разработке программного обеспечения, это пытаемся разделить задачи, то есть изолировать функции, и множественное наследование может помочь сделать это.
Это всего лишь примеры, которые могут быть действительными или нет, в зависимости от конкретного случая, но они ясно показывают проблемы, которые могут возникнуть у нас даже при очень простой иерархии из 4 классов. Многие из этих проблем явно возникают из-за того, что мы хотели реализовать делегирование только посредством наследования, и я осмелюсь сказать, что 80% архитектурных ошибок в проектах ООП происходят из-за использования наследования вместо композиции и использования God объектов(божественных объектов), то есть классов, которые несут ответственность за слишком много различных частей системы. Всегда помните, что ООП родился с идеей взаимодействия небольших объектов посредством сообщений, поэтому соображения, которые мы учитываем для монолитных архитектур, верны и здесь.
Тем не менее, поскольку наследование и композиция реализуют два разных типа делегирования (быть и иметь), они оба ценны, и множественное наследование — это способ снять ограничение единственного поставщика, которое возникает из-за наличия только одного родительского класса.
Почему это так противоречиво?
Учитывая то, что я только что сказал, множественное наследование кажется благословением. Когда объект может наследоваться от нескольких родителей, мы можем легко распределять обязанности между различными классами и использовать только те, которые нам нужны, способствуя повторному использованию кода и избегая «божественных» объектов.
К сожалению, все не так просто. Прежде всего, мы сталкиваемся с проблемой, с которой сталкивается каждая микросервисно-ориентированная архитектура, то есть с риском перехода от «божественных» объектов (экстремальная монолитная архитектура) к почти пустым объектам (экстремально распределенный подход), обременяя программиста слишком тонким детальным контролем, который в конечном итоге приводит к созданию системы, в которой отношения между объектами настолько сложны, что становится невозможным понять влияние изменений в коде.
Однако в множественном наследовании существует более насущная проблема. Как это происходит с естественным наследованием, родители могут предоставить одну и ту же «генетическую черту» в двух разных вариантах, но у получающегося в результате этого человека будет только одна. Оставляя в стороне генетику (которая невероятно сложнее, чем программирование) и возвращаясь к ООП, мы сталкиваемся с проблемой, когда объект наследуется от двух других объектов, которые предоставляют тот же атрибут.
Итак, если ваш класс Child наследует от родителей Parent1 и Parent2, и оба предоставляют метод __init__, какой из них следует использовать вашему объекту?
Ситуация может ухудшиться, так как родители могут иметь разные входные параметры общего метода, например
Проблема может быть расширена еще дальше, введя общего предка выше Parent1 и Parent2.
Как видите, у нас уже есть проблема, когда мы представляем нескольких родителей, а общий предок просто добавляет новый уровень сложности. Класс предка может быть явно в любой точке дерева наследования (дедушка, бабушка и дедушка и т. д.), Важной частью является то, что он является общим для Parent1 и Parent2. Это так называемая проблема алмаза, так как граф наследования имеет форму ромба
Таким образом, в то время как при наследовании с одним родителем правила просты, с множественным наследованием у нас сразу возникает более сложная ситуация, в которой нет тривиального решения. Помогает ли все это реализовать множественное наследование?
Как мы вскоре увидим, есть решения этой проблемы, но этот дополнительный уровень сложности делает множественное наследование чем-то, что не очень легко вписывается в дизайн. Помните, что наследование — это механизм автоматического делегирования. По этим причинам множественное наследование часто изображается как пугающее и запутанное, и, как правило, ему уделяется время только в продвинутых курсах ООП, по крайней мере, в мире Python. Я считаю, что каждый программист на Python должен ознакомиться с ним и узнать, как им воспользоваться.
Множественное наследование: путь Python
Посмотрим, как можно решить алмазную проблему. В отличие от генетики, мы, программисты, не можем позволить себе какой-либо уровень неопределенности или случайности в наших процессах, поэтому при наличии возможной неоднозначности, как та, которая создается множественным наследованием, нам необходимо записать правило, которое будет строго соблюдаться в каждом случае. В Python это правило называется MRO (Method Resolution Order — Порядок разрешения методов), которое было введено в Python 2.3 и описано в этом документе Микеле Симионато.
Можно многое сказать о MRO и лежащем в основе алгоритме линеаризации C3, но для целей этого поста достаточно посмотреть, как он решает проблему алмазов. В случае множественного наследования Python следует обычным правилам наследования (автоматическое делегирование предку, если атрибут не присутствует локально), но порядок следования по дереву наследования теперь включает все классы, указанные в сигнатуре класса. В приведенном выше примере Python будет искать атрибуты в следующем порядке: Child, Parent1, Parent2, Ancestor.
Таким образом, как и в случае стандартного наследования, это означает, что первый класс в списке, который реализует определенный атрибут, будет выбранным поставщиком для этого разрешения. Пример может прояснить вопрос
В этом случае экземпляр c Child обеспечит rewind, open, close и flush. Когда вызывается c.rewind, выполняется код в Ancestor, так как это первый класс в списке MRO, который предоставляет этот метод. Метод open предоставляется Parent1, а close — Parent2. Если вызывается метод c.flush, код предоставляется самим классом Child, который переопределяет его, переопределяя класс, предоставленный Parent2.
Как мы видим в методе flush, Python не меняет своего поведения, когда речь идет о методе, переопределенной несколькими родителями. Первая реализация метода с таким именем выполняется, и реализация родителя не вызывается автоматически. Как и в случае стандартного наследования, мы должны определять классы с соответствующими сигнатурами методов.
Под капотом
Как множественное наследование работает внутри? Как Python создает список MRO?
У Python очень простой подход к ООП (более подробнее см. Здесь). Классы сами являются объектами, поэтому они содержат структуры данных, которые используются языком для предоставления функций, и делегирование не исключение. Когда мы запускаем метод для объекта, Python по умолчанию используется метод __getattribute__ (предоставляемый object), который использует __class__ для доступа к классу из экземпляра и __bases__ для поиска родительских классов. Последний, в частности, является кортежем, поэтому он упорядочен и содержит все классы, от которых наследуется текущий класс.
MRO создается с использованием только __bases__, но основной алгоритм не так уж простой и связан с монотонностью линеаризации результирующего класса. Но на самом деле это не так страшно, как кажется, но, скорее всего не то, что вы хотели бы прочитать во время отдыха. Но если это так, то вышеупомянутый документ Микеле Симионато содержит все подробности о линеаризации классов, которые вы всегда хотели исследовать, к примеру лежа на пляже.
Наследование и интерфейсы
Чтобы приблизиться к миксинам, нам нужно подробно обсудить наследование и, в частности, роль сигнатур методов.
В Python, когда вы переопределяете метод, предоставленный классом-предком, вы должны решить, когда и как вызывать его начальную реализацию. Это дает программисту свободу решать, нужно ли им просто дополнить метод или полностью заменить его. Помните, что единственное, что делает Python, когда класс наследует от другого, — это автоматически делегирует методы, которые не были реализованы в наследнике.
Когда класс наследует от другого, мы в идеале создаем объекты, которые сохраняют обратную совместимость с интерфейсом родительского класса, чтобы позволить их полиморфное использование. Это означает, что когда мы наследуем класс и переопределяем метод, изменяя его сигнатуру, мы делаем что-то опасное и, по крайней мере, с точки зрения полиморфизма. Посмотрите на этот пример
Обратите внимание, что Square меняет подпись __init__ и resize. Теперь, когда мы создаем экземпляры этих классов, нам нужно помнить что в Square другая подпись __init__
Обычно мы признаем, что расширенная версия класса принимает больше параметров при инициализации, так как мы не ожидаем, что она будет полиморфной в __init__. Проблемы возникают, когда мы пытаемся использовать полиморфизм в других методах, например, изменяя размер всех объектов GraphicalEntity в списке
Поскольку r1, r2 и q1 — все объекты, которые наследуются от GraphicalEntity, мы ожидаем, что они предоставят интерфейс, предоставленный этим классом, но это не удастся, потому что Square изменил сигнатуру resize. То же самое произойдет, если мы создадим их в цикле for из списка классов, но, как я уже сказал, общепринято, что дочерние классы изменяют сигнатуру метода __init__. Это не так, например, в системе на основе плагинов, где все плагины должны быть инициализированы одинаково.
Это классическая проблема в ООП. Хотя мы, как люди, воспринимаем квадрат просто как немного особенный прямоугольник, с точки зрения интерфейса эти два класса различны, и поэтому мы не должны находиться в одном и том же дереве наследования, когда имеем дело с измерениями. Это важное соображение: Rectangle и Square полиморфны в методе move, но не в __init__ и resize. Итак, вопрос в том, можем ли мы как-то разделить две природы movable и resizable.
Теперь для обсуждения интерфейсов, полиморфизма и причин, лежащих в их основе, потребуется совершенно отдельный пост, поэтому в следующих разделах я собираюсь игнорировать этот вопрос и просто считать интерфейс объекта необязательным. Таким образом, вы найдете примеры объектов, которые нарушают интерфейс родителя, и объектов, которые его хранят. Просто помните: всякий раз, когда вы изменяете сигнатуру метода, вы изменяете (неявный) интерфейс объекта, и таким образом вы останавливаете полиморфизм. Я буду обсуждать в другой раз, если я считаю это правильным или неправильным.
Миксины
MRO — это хорошее решение, которое предотвращает двусмысленность, но оставляет программистам ответственность за создание разумных деревьев наследования. Алгоритм помогает разрешать сложные ситуации, но это не значит, что мы должны их создавать. Итак, как мы можем использовать множественное наследование, не создавая системы, которые слишком сложны для понимания? Кроме того, возможно ли использовать множественное наследование для решения проблемы управления двойной (или множественной) природой объекта, как в предыдущем примере с movable и resizable формой?
Решение исходит из смешанных классов: это небольшие классы, которые предоставляют атрибуты, но не включены в стандартное дерево наследования, работая скорее как «дополнения» к текущему классу, чем как настоящие предки. Миксины происходят от языка программирования LISP, а именно от того, что можно считать первой версией Common Lisp Object System, расширением Flavors. Современные языки ООП реализуют миксины разными способами: например, в Scala есть функции, называемые traits, которые живут в своем собственном пространстве с определенной иерархией, и которые не мешают правильному наследованию классов.
Классы миксинов в Python
Python не поддерживает миксины с какой-либо выделенной функцией языка, поэтому мы используем множественное наследование для их реализации. Это явно требует большой дисциплины от программиста, поскольку это нарушает одно из основных допущений для миксинов: их ортогональность к дереву наследования. В Python так называемые миксины — это классы, которые живут в обычном дереве наследования, но они остаются небольшими, чтобы избежать создания иерархий, которые слишком сложны для понимания программистом. В частности, миксины не должны иметь общих предков, кроме объекта, с другими родительскими классами.
Давайте посмотрим на простой пример
Здесь класс ResizableMixin наследуется не от GraphicalEntity, а непосредственно от object, поэтому ResizableGraphicalEntity получает от него только метод resize. Как мы уже говорили, это упрощает дерево наследования ResizableGraphicalEntity и помогает снизить риск проблемы алмаза. Это позволяет нам свободно использовать GraphicalEntity в качестве родителя для других классов без необходимости наследовать методы, которые нам не нужны. Пожалуйста, помните, что это происходит потому, что классы предназначены для того, чтобы этого избежать, а не из-за особенностей языка: алгоритм MRO просто гарантирует, что всегда будет однозначный выбор в случае нескольких предков.
Mixins обычно не могут быть слишком общими. В конце концов, они предназначены для добавления определенных функций в классы, но эти новые функции часто взаимодействуют с другими уже существующими функциями расширенного класса. В этом случае метод resize взаимодействует с атрибутами size_x и size_y, которые должны присутствовать в объекте. Очевидно, что есть примеры чистых mixins, но, поскольку они не требуют инициализации, их область действия определенно ограничена.
Использование миксинов для взламывания наследования
Благодаря MRO программисты Python могут использовать множественное наследование для переопределения методов, которые объекты наследуют от своих родителей, что позволяет им создавать классы без дублирования кода. Давайте посмотрим на этот пример
Как вы можете видеть, класс Button расширяет класс GraphicalEntity классическим способом, используя super для вызова родительского метода __init__ перед добавлением нового атрибута состояния status. Теперь, если я хочу создать класс SquareButton, у меня есть два варианта.
Я мог бы просто переопределить __init__ в новом классе
который выполняет запрошенное задание, но тесно связывает особенность наличия одного измерения с Button. Если бы мы хотели создать круглое изображение, мы не могли бы наследовать от SquareButton, так как изображение имеет другую природу.
Второй вариант — это выделение объектов, связанных с наличием одного измерения в классе mixin, и добавление его в качестве родительского для нового класса.
Второе решение дает тот же конечный результат, но способствует повторному использованию кода, поскольку теперь класс SingleDimensionMixin можно применять к другим классам, производным от GraphicalEntity, и заставлять их принимать только один размер, тогда как в первом решении эта функция была тесно связана с предком Button класс.
Обратите внимание, что позиция миксина важна. Поскольку super следует MRO, вызываемый метод отправляется в ближайший класс линеаризации. Если вы поместите SingleDimensionMixin после Button в определении SquareButton, Python выдаст ошибку. В этом случае вызов b = SquareButton (10, 20, 200) и сигнатура метода __init__ (self, pos_x, pos_y, size_x, size_y) не будут совпадать.
Тем не менее, миксины используются не только тогда, когда вы хотите изменить интерфейс объекта. Используя super, мы можем достичь интересных конструкций, таких как
Здесь LimitSizeButton вызывает __init__ своего первого родителя, который является Button. Это, однако, делегирует вызов следующему классу в MRO перед инициализацией self.status, поэтому вызов отправляется LimitSizeMixin, который сначала выполняет некоторые изменения и в конечном итоге отправляет его исходному получателю, GraphicalEntity.
Помните, что в Python вы никогда не будете вынуждены вызывать родительскую реализацию метода, поэтому миксин здесь также может остановить механизм диспетчеризации, если это является требованием бизнес-логики нового объекта.
Реальный пример: представления на основе классов Django
Наконец, давайте перейдем к первоначальному источнику вдохновения для этого поста: кодовой базе Django. Я покажу вам, как программисты Django использовали множественные классы наследования и миксины для содействия повторному использованию кода, и теперь вы, надеюсь, поймете все причины, стоящие за ними.
Выбранный мной пример можно найти в коде универсальных представлений(generic views), в частности в двух классах: TemplateResponseMixin и TemplateView.
Как вы, возможно, знаете, класс Django View является прародителем всех представлений на основе классов и предоставляет метод диспетчеризации dispatch, который преобразует методы HTTP-запросов в вызовы функций Python (CODE). Теперь TemplateView — это представление, которое отвечает на запрос GET, отображающий шаблон с данными, поступающими из контекста, передаваемого при вызове представления. Учитывая механизм, лежащий в основе представлений Django, TemplateView должен реализовать метод get и вернуть содержимое HTTP-ответа. Код класса
Как вы можете видеть, TemplateView — это View, но он использует два миксина для добавления функций. Давайте посмотрим на TemplateResponseMixin
[Я удалил код класса, так как он не имеет решающего значения для настоящего обсуждения, вы можете увидеть полный класс здесь]
Понятно, что TemplateResponseMixin просто добавляет к любому классу два метода get_template_names и render_to_response. Последний вызывается в методе get TemplateView для создания ответа. Давайте посмотрим на упрощенную схему вызовов:
Как мы уже говорили, миксины не могут быть слишком общими, и здесь мы видим хороший пример миксина, предназначенного для работы с конкретными классами. TemplateResponseMixin должен применяться к классам, которые содержат self.request (CODE), и хотя это не означает исключительно классы, производные от View, ясно, что он был разработан для дополнения этого конкретного типа.
Выводы
Заключение
Я надеюсь, что этот пост помог вам понять немного больше, как работает множественное наследование, и меньше быть напуганным этим. Я также надеюсь, что мне удалось показать вам, что классы должны быть тщательно спроектированы, и что при создании системы классов нужно учитывать многое. Еще раз, пожалуйста, помните что композиция, это мощный и часто забываемый инструмент.
Обратная связь
Не стесняйтесь связаться со мной в Twitter, если у вас есть вопросы. Страница вопросов GitHub — лучшее место для отправки исправлений.




























