Оригинал этой статьи опубликован в журнале «Системный администратор» №9 (130) за сентябрь 2013.
Как известно, в языке Python существует красивый механизм декораторов, расширяющих функционал объекта без изменения интерфейса. Это довольно мощное средство, попользоваться им удобно и приятно. Но вот проблема: наш язык программирования — Ruby!
На самом деле никакой проблемы нет, и в Ruby достаточно возможностей, чтобы решать подобные задачи не менее эффективно, чем в конкурирующих технологиях.
… … …
Универсальность всегда увеличивает сложность и накладные расходы. Так что мое мнение: жили мы без декораторов в Ruby и еще поживем. Тем не менее сама методика декорирования кода, безусловно, заслуживает внимания и может с успехом применяться в самых разных задачах.
Декоратор в общем смысле — шаблон проектирования, предусматривающий динамическое подключение дополнительной функциональности к некоторому уже имеющемуся объекту без изменения его интерфейса. Старая функциональность оказывается как бы обернута в новую.
Декоратор в языке программирования Python — специальная синтаксическая конструкция, «оборачивающая» заданную функцию с использованием ранее определенной функции-обертки. Оборачивающая функция принимает в качестве аргументов заданную и, возможно, какие-то дополнительные параметры, которые в дальнейшем указываются при использовании декоратора, и возвращает замещающую функцию.
Простой пример:
Идея этой статьи навеяна постом на Хабре о декораторах в языке Python1, а также некоторыми другими материалами в сети на ту же тему. Авторы данных материалов явно гордятся такой возможностью языка, что прямо таки заставляет правоверного рубиста выяснить, а как обстоят дела с аналогами в любимом языке. Конечно, надо понимать, что при всей близости по времени появления, объектно-ориентированности и области применения, Python и Ruby все же разные языки, соответственно, аналоги получаются не один к одному. Но можно говорить о схожих решениях схожих задач. Если мы откроем вышеупомянутый хабрапост, то увидим, в частности:
«Для того, чтобы понять, как работают декораторы, в первую очередь следует осознать, что в Python’е функции — это тоже объекты»
И тут уже приходится остановиться: в Ruby, во-первых, нет функций, есть только методы, а во-вторых, методы объектами не являются. Все плохо? Ничего подобного. Возможностей метапрограммирования в Ruby более чем достаточно для того, чтобы реализовать аналогичную функциональность. Это и будет предметом настоящей статьи.
Пара слов о том, зачем это надо: способы применения могут быть самые разные — от ведения логов и замеров производительности до проверки прав пользователя или каких-то еще сложных условий перед выполнением каждого действия. Причем, обрамлять своими проверками мы можем любые методы, в том числе принадлежащие классам стандартной библиотеки языка, или библиотек, разрабатываемых третьими лицами. Поскольку исходный код имеющихся библиотек не модифицируется, мы не создаем себе никаких препятствий к установке обновлений и исправлений, что важно для проектов с длительным жизненным циклом.
Блоки, методы и объекты Proc
Здесь и далее я описываю сложные, но хорошо документированные, особенности языка применительно к основной теме статьи, кратко, не всегда формально точно и, по возможности, просто. Для изучения языка лучше всего обратиться к более-менее официальным2 и неофициальным3 руководствам.
Итак, что мы имеем на уровне языка? Во-первых, это, конечно, методы. В принципе — те же функции, но определенные для класса
и исполняющиеся в контексте объекта. Замечу, что метод всегда определен для какого-то класса, даже если формально он задан
непосредственно объекту: так называемые синглтон-методы это методы так называемых синглтон-классов. Методы, определенные
в глобальном контексте принадлежат классу Object
. С помощью метода method
можно получить для данного метода объект класса
Method
(«Шишков, прости…»), который уже есть полноценный ruby-объект.
Во-вторых, это блоки — их можно рассматривать как анонимные функции, образующие замыкание с тем контекстом, в котором они
определены. При этом язык так устроен, что определяем блок мы всегда, передавая его в некий метод, где он доступен, помимо особых
средств языка, и как объект класса Proc
.
Одним из таких методов, принимающих блоки, является метод класса Module
define_method
, который волшебным образом превращает
блок в метод. С другой стороны, объект класса Proc
, а также любой объект, для которого определен метод to_proc
, в том числе
и класса Method
, может быть передан в качестве блока посредством префикса &
.
Демонстрация:
На выходе должны получить что-то вроде этого:
Здесь мы из метода alpha
получили объект, а метод beta
наоборот, создали из объекта. «Object.send :define_method
»
вместо «Object.define_method
» пришлось использовать потому, что define_method
— приватный.
Теперь, определившись, что у нас есть, можно перейти к рассмотрению того, что можно из этого сделать…
Шаг первый — функция-обертка
Очевидно, что там, где в Python функция, принимающая и возвращающая функцию, у нас будет метод, принимающий объект класса
Proc
, а лучше — для универсальности — блок, и возвращающий объект класса Proc
.
Давайте напишем обертку-логгер:
И протестируем ее:
Получаем (в предположении, что определение обертки и тестирующий код находятся в файле deco2.rb
, как у меня4):
Здесь хорошо видно, что мы получили исключение, вывели информацию о нем и отправили его дальше по стеку вызовов. Это сделано затем, чтобы и в плане исключений обертка вела себя так же, как исходная функция.
Чтобы в начале и конце у нас был метод, требуется добавить в общем-то немного:
Получаем метод, создаем обертку и на ее основе переопределяем метод с тем же именем. Проверяем как-то так:
Шаг второй — в правильном контексте
Примеры выше даны для глобального контекста, в котором, однако, на практике методы определяют редко. Нормальное расположение методов — в контексте класса или модуля. Или попросту модуля, поскольку класс в данном случае есть его разновидность (очень специфическая, но все же).
Поместим метод wrap
в явном виде в класс Object
(на самом деле, он и так там, просто сделаем определение более
ясным) и заставим его принимать не только блоки, но и непосредственно объекты класса Method
, чтобы иметь доступ
к имени метода, а также класса UnboundMethod
, чтобы оперировать методами, не привязанными к конкретному объекту.
И попробуем:
Получим:
Нетрудно заметить, что использование wrap_method
выглядит весьма похоже на стандартные модификаторы private
, protected
и public
. Давайте еще усилим эту похожесть (а заодно и похожесть на python-декораторы) — при вызове без параметров, метод
будет действовать на все последующие определения. Модифицируем wrap_method
:
Метод класса Module
method_added
вызывается при любом определении метода. Чтобы не уйти в бесконечный цикл, нам приходится
дополнительно ввести флаг, говорящий о том, что текущее определение — это наша обертка, которую заново оборачивать не нужно.
Кстати, здесь мы вместо того, чтобы перекрыть метод method_added
создаем (опять же) над ним обертку. Сделано это затем, чтобы
нам не помешали его возможные переобъявления. Проверим на следующем коде:
Должно получиться на выходе:
Аналогично можно модифицировать и wrap_singleton_method
, если очень хочется.
Шаг третий — фабрика генераторов
Ну и наконец, давайте решим задачу в более-менее общем виде. Пусть у нас будет способ генерировать различные «декораторы»,
задав имя и блок, возвращающий proc
-обертку. Блок будет принимать UnboundMethod
и произвольные именованные параметры.
Только именованные, поскольку неименованный список мы будем вместе с ними передавать при вызове декоратора — это имена
декорируемых методов, как и в примерах выше. В python-декораторах так делать не принято, зато в Ruby подобное сплошь и рядом.
Для упрощения и сокращения кода далее я использую синтаксическую конструкцию для задания именованных параметров, которая появилась только в Ruby версии 2.0 (предыдущие примеры полностью работоспособны и в 1.9). Сделать тоже самое в предыдущих версиях вполне реально, но несколько длиннее.
Вот такое короткое определение:
Прогоним тестовый пример:
И получим:
Идем дальше?
Как видим, все работает. Кроме того, определения декораторов прекрасно наследуются. Для дальнейшего развития можно поработать над тем, чтобы они еще и «включались» при добавлении mixin-модуля, предусмотреть отмену и/или переключение и так далее. Но… так уж ли все это нужно? Тем более, что есть не столь универсальный, зато очень простой и достаточный в большинстве случаев способ сделать обертку:
Универсальность всегда увеличивает сложность и накладные расходы. Так что лично мое мнение: жили мы без декораторов в Ruby, и еще поживем. Тем не менее сама методика декорирования кода, безусловно, заслуживает внимания и может с успехом применяться в самых разных задачах.
-
«Понимаем декораторы в Python’e, шаг за шагом», http://habrahabr.ru/post/141411/ и 141501/, перевод с английского — Владислав Степанов; оригинал: “What are Python decorators?”, Renaud Gaudin, http://yeleman.com/what-are-python-decorators/. ↩
-
Dave Thomas, with Chad Fowler and Andy Hunt, «Programming Ruby: The Pragmatic Programmers’ Guide», бесплатная версия первого издания — http://www.ruby-doc.org/docs/ProgrammingRuby/ [en]. ↩
-
Викиучебник по Ruby, http://ru.wikibooks.org/wiki/Ruby. ↩
-
Полные тексты всех примеров можно взять на GitHub Gist: https://gist.github.com/shikhalev/6259566. ↩