От переводчика: предлагаю вам перевод начала презентации Michael Fairley — Extending Ruby with Ruby. Я перевел только первую часть из трех, потому что она имеет максимальные практические ценность и пользу, на мой взгляд. Тем не менее, настоятельно рекомендую ознакомиться с полной презентацией, в которой помимо Python приводятся примеры заимствования фишек из Haskell и Scala.
В Python есть такая штука — декораторы, которая представляет собой синтаксический сахар для добавления в методы и функции кусочков часто используемой функциональности. Сейчас я покажу вам некоторые примеры того, что такое декораторы и почему они могли бы быть полезны и в Ruby.
Раньше я очень много работал с Python и декораторы функции определенно являются тем, чего мне так не хватает с тех пор, и кроме того тем, что может помочь практически всем нам сделать наш код на Ruby чище.
Возьмем Ruby и притворимся, что нам нужно перевести деньги с одного банковского аккаунта на другой. Вроде все просто, так?
Мы вычитаем сумму из баланса аккаунта «from»…
И прибавляем эту сумму к балансу аккаунта «to»…
И сохраняем оба аккаунта.
Но тут есть пара недочетов, самый очевидный из которых — отсутствие транзакции (если «from.save!» завершится успешно, а «to.save!» — нет, то деньги растворятся в воздухе).
К счастью, ActiveRecord делает решение этой проблемы очень простым. Мы просто оборачиваем наш код в блок метода транзакции и это гарантирует нам, что внутри блока все завершается либо успешно, либо нет.
Давайте теперь посмотрим на этот же пример в Python. Версия без транзакции выглядит почти точь в точь как в Ruby.
Но стоит добавить транзакцию и код начинает выглядеть уже не так изящно.
В этом методе 10 строк кода, но только 4 из них реализуют нашу бизнес-логику.
Другие же 6 строк — шаблон для запуска нашей логики внутри транзакции. Это уродливо и слишком многословно, но что еще хуже — вы должны помнить все эти строки, включая правильную обработку ошибок и семантику отката.
Так как же нам сделать это красивее и меньше повторять себя? В Python нет блоков, поэтому фокус как в Ruby здесь не пройдет. Однако в Python есть возможность легко передавать и переназначать методы. Поэтому мы можем написать функцию «transactional», которая будет принимать в качестве аргумента другую функцию и возвращать эту же функцию, но уже обернутую в шаблонный код транзакции.
А вот как может выглядеть функция «transactional»…
Она получает функцию («send_money» в нашем примере) как свой единственный аргумент.
Определяет новую функцию.
Новая функция содержит в себе шаблон для оборачивания бизнес-логики в транзакцию.
Внутри шаблона вызывается оригинальная функция, которой передаются аргументы, которые были переданы новой функции.
И наконец, новая функция возвращается.
Таким образом, мы передаем функцию «send_money» в функцию «transactional», которую только что определили, которая в свою очередь возвращает новую функцию, которая делает все тоже самое что и функция «send_money», но делает все это внутри транзакции. И далее мы присваиваем эту новую функцию нашей функции «send_money», переопределяя ее оригинальное содержимое. Теперь, когда бы мы не вызвали функцию «send_money», будет вызвана версия с транзакцией.
И вот то, к чему я все это время вел. Эта идиома настолько часто используется в Python, что для ее поддержки добавили специальный синтаксис — декоратор функции. И именно так вы делаете что-либо транзакционным в Django ORM.
Теперь вы думаете: «Ну и что? Ты только что показал как эта декораторная мумба-юмба решает ту же проблему, которую решают блоки. Зачем нам эта шляпа в Ruby?» Ну что ж, давайте взглянем на случай, в котором блоки уже не выглядят так элегантно.
Пускай у нас есть метод, который вычисляет значение n-ого элемента в последовательности Фибоначчи.
Он медленный, поэтому мы хотим его мемоизовать. Общепринятый подход для этого — распихать повсюду «||=», который страдает тем же недугом, что и первый пример с транзакцией — мы смешиваем код нашего алгоритма с дополнительным поведением, которым хотим его окружить.
К тому же мы забыли тут пару вещей, как например, тот факт, что «nil» и «false» не могут быть мемоизованы таким способом: еще один момент, о котором необходимо постоянно помнить.
Хорошо, мы можем решить это с помощью блока, но у блоков нет доступа к имени или аргументам функции, которая их вызывает, поэтому нам придется передавать эту информацию явно.
А теперь, если мы начнем добавлять больше блоков вокруг основной функциональности…
… мы будем вынуждены снова и снова перепечатывать имя метода и его аргументы.
Это довольно хрупкая конструкция и она сломается в тот же миг, как только мы решим каким-либо образом изменить сигнатуру метода.
Тем не менее, это можно решить путем добавления вот такой штуки сразу после определения нашего метода.
И это должно вам напомнить то, что мы видели в Python — когда модификация метода шла сразу после самого метода.
Почему же сообществу Python не понравилось такое решение? Две причины:
Давайте посмотрим на наш пример с Фибоначчи в Python.
Мы хотим его мемоизовать, поэтому мы декорируем его функцией «memoize».
И если мы хотим измерять время работы нашего метода или синхронизировать его вызовы, то мы просто добавляем еще один декоратор. Вот и все.
А теперь я покажу вам как добиться этого в Ruby (используя «+» вместо «@» и первую букву как заглавную). И самое прикольное, что мы можем добавить этот синтаксис декоратора в Ruby, который очень близок к синтаксису в Python, с помощью всего лишь 15 строчек кода.
Давайте вернемся к нашему примеру «send_money». Мы хотим добавить к нему декоратор «Transactional».
«Transactional» является подклассом «Decorator», который мы обсудим чуть ниже.
У него всего один метод «call», который будет вызван вместо нашего оригинального метода. В качестве аргументов он получает метод, который должен «обернуть», его аргументы и его блок, которые будут переданы ему при вызове.
Открываем транзакцию.
И далее вызываем оригинальный метод внутри блока транзакции.
Обратите внимание, что структура нашего декоратора отличается от того, как декораторы работают в Python. Вместо того, чтобы определять новую функцию, которая будет получать аргументы, наш декоратор в Ruby будет получать сам метод и его аргументы при каждом вызове. Мы вынуждены так сделать из-за семантики привязки методов к объектам в Ruby, о которой мы поговорим чуть ниже.
Что же внутри класса «Decorator»?
Эта штука — «+@» — оператор «унарный плюс», поэтому этот метод будет вызван, когда мы вызываем «+DecoratorName», как мы сделали с «+Transactional».
Так же нам нужен способ получить текущий декоратор.
И способ обнулить текущий декоратор.
Класс, который хочет иметь декорируемые методы должен быть расширен модулем «MethodDecorators».
Можно было бы расширить сразу класс «Class», но я думаю, что лучшей практикой в данном случае будет оставить такое решение на усмотрение конечного пользователя.
«method_added» — это приватный метод класса, который вызывается каждый раз когда в классе определяется новый метод, давая нам удобный способ поймать момент начала создания метода.
Вызываем родительский «method_added». Об этом можно легко забыть, переопределяя методы вроде «method_added», «method_missing» или «respond_to?», но если вы этого не делаете, то легко можете сломать другие библиотеки.
Получаем текущий декоратор и прерываем функцию, если декоратора нет, иначе обнуляем текущий декоратор. Декоратор важно обнулить, потому что дальше мы переопределяем метод, что снова вызывает наш «method_added».
Извлекаем оригинальную версию метода.
И переопределяем его.
«instance_method» на самом деле возвращает объект класса «UnboundMethod», который представляет собой метод, который не знает какому объекту он принадлежит, поэтому мы должны привязать его к текущему объекту.
И затем мы вызываем декоратор, передавая ему оригинальный метод и аргументы для него.
Конечно тут есть еще ряд невероятно важных моментов, которые должны быть решены, прежде чем этот код можно будет считать готовым к production среде.
Реализация, которую я привел позволяет использовать только один декоратор для каждого метода, но мы хотим иметь возможность использоват�� больше одного декоратора.
«define_method» определяет публичные методы, но мы хотим приватные и защищенные методы, которые можно было бы декорировать с соблюдением их области видимости.
«method_added» и «define_method» работают только для методов экземпляра класса, поэтому нужно придумать что-то еще, чтобы декораторы стали работать для методов самого класса.
В примере с Python я показал, что мы можем передавать декоратору значения. Мы хотим, чтобы у нас была возможность создавать какие угодно индивидуальные экземпляры декораторов для наших методов.
github.com/michaelfairley/method_decorators
Я реализовал все эти возможности, добавил исчерпывающий набор тестов и выкатил все это в виде gem. Используйте это, потому что я думаю это может сделать ваш код чище, повысить его читаемость и упростить его поддержку.
Декораторы функции
В Python есть такая штука — декораторы, которая представляет собой синтаксический сахар для добавления в методы и функции кусочков часто используемой функциональности. Сейчас я покажу вам некоторые примеры того, что такое декораторы и почему они могли бы быть полезны и в Ruby.
Раньше я очень много работал с Python и декораторы функции определенно являются тем, чего мне так не хватает с тех пор, и кроме того тем, что может помочь практически всем нам сделать наш код на Ruby чище.
Возьмем Ruby и притворимся, что нам нужно перевести деньги с одного банковского аккаунта на другой. Вроде все просто, так?
def send_money(from, to, amount) from.balance -= amount to.balance += amount from.save! to.save! end
Мы вычитаем сумму из баланса аккаунта «from»…
from.balance -= amount
И прибавляем эту сумму к балансу аккаунта «to»…
to.balance += amount
И сохраняем оба аккаунта.
from.save! to.save!
Но тут есть пара недочетов, самый очевидный из которых — отсутствие транзакции (если «from.save!» завершится успешно, а «to.save!» — нет, то деньги растворятся в воздухе).
К счастью, ActiveRecord делает решение этой проблемы очень простым. Мы просто оборачиваем наш код в блок метода транзакции и это гарантирует нам, что внутри блока все завершается либо успешно, либо нет.
def send_money(from, to, amount) ActiveRecord::Base.transaction do from.balance -= amount to.balance += amount from.save! to.save! end end
Давайте теперь посмотрим на этот же пример в Python. Версия без транзакции выглядит почти точь в точь как в Ruby.
def send_money(from, to, amount): from.balance -= amount to.balance += amount from.save() to.save()
Но стоит добавить транзакцию и код начинает выглядеть уже не так изящно.
def send_money(from, to, amount): try: db.start_transaction() from.balance -= amount to.balance += amount from.save() to.save() db.commit_transaction() except: db.rollback_transaction() raise
В этом методе 10 строк кода, но только 4 из них реализуют нашу бизнес-логику.
from.balance -= amount to.balance += amount from.save() to.save()
Другие же 6 строк — шаблон для запуска нашей логики внутри транзакции. Это уродливо и слишком многословно, но что еще хуже — вы должны помнить все эти строки, включая правильную обработку ошибок и семантику отката.
def send_money(from, to, amount): try: db.start_transaction() ... db.commit_transaction() except: db.rollback_transaction() raise
Так как же нам сделать это красивее и меньше повторять себя? В Python нет блоков, поэтому фокус как в Ruby здесь не пройдет. Однако в Python есть возможность легко передавать и переназначать методы. Поэтому мы можем написать функцию «transactional», которая будет принимать в качестве аргумента другую функцию и возвращать эту же функцию, но уже обернутую в шаблонный код транзакции.
def send_money(from, to, amount): from.balance -= amount to.balance += amount from.save() to.save() send_money = transactional(send_money)
А вот как может выглядеть функция «transactional»…
def transactional(fn): def transactional_fn(*args): try: db.start_transaction() fn(*args) db.commit_transaction() except: db.rollback_transaction() raise return transactional_fn
Она получает функцию («send_money» в нашем примере) как свой единственный аргумент.
def transactional(fn):
Определяет новую функцию.
def transactional_fn(*args):
Новая функция содержит в себе шаблон для оборачивания бизнес-логики в транзакцию.
try: db.start_transaction() ... db.commit_transaction() except: db.rollback_transaction() raise
Внутри шаблона вызывается оригинальная функция, которой передаются аргументы, которые были переданы новой функции.
fn(*args)
И наконец, новая функция возвращается.
return transactional_fn
Таким образом, мы передаем функцию «send_money» в функцию «transactional», которую только что определили, которая в свою очередь возвращает новую функцию, которая делает все тоже самое что и функция «send_money», но делает все это внутри транзакции. И далее мы присваиваем эту новую функцию нашей функции «send_money», переопределяя ее оригинальное содержимое. Теперь, когда бы мы не вызвали функцию «send_money», будет вызвана версия с транзакцией.
send_money = transactional(send_money)
И вот то, к чему я все это время вел. Эта идиома настолько часто используется в Python, что для ее поддержки добавили специальный синтаксис — декоратор функции. И именно так вы делаете что-либо транзакционным в Django ORM.
@transactional def send_money(from, to, amount): from.balance -= amount to.balance += amount from.save() to.save()
И что?
Теперь вы думаете: «Ну и что? Ты только что показал как эта декораторная мумба-юмба решает ту же проблему, которую решают блоки. Зачем нам эта шляпа в Ruby?» Ну что ж, давайте взглянем на случай, в котором блоки уже не выглядят так элегантно.
Пускай у нас есть метод, который вычисляет значение n-ого элемента в последовательности Фибоначчи.
def fib(n) if n <= 1 1 else fib(n - 1) + fib(n - 2) end end
Он медленный, поэтому мы хотим его мемоизовать. Общепринятый подход для этого — распихать повсюду «||=», который страдает тем же недугом, что и первый пример с транзакцией — мы смешиваем код нашего алгоритма с дополнительным поведением, которым хотим его окружить.
def fib(n) @fib ||= {} @fib[n] ||= if n <= 1 1 else fib(n - 1) + fib(n - 2) end end
К тому же мы забыли тут пару вещей, как например, тот факт, что «nil» и «false» не могут быть мемоизованы таким способом: еще один момент, о котором необходимо постоянно помнить.
def fib(n) @fib ||= {} return @fib[n] if @fib.has_key?(n) @fib[n] = if n <= 1 1 else fib(n - 1) + fib(n - 2) end end
Хорошо, мы можем решить это с помощью блока, но у блоков нет доступа к имени или аргументам функции, которая их вызывает, поэтому нам придется передавать эту информацию явно.
def fib(n) memoize(:fib, n) do if n <= 1 1 else fib(n - 1) + fib(n - 2) end end end
А теперь, если мы начнем добавлять больше блоков вокруг основной функциональности…
def fib(n) memoize(:fib, n) do time(:fib, n) do if n <= 1 1 else fib(n - 1) + fib(n - 2) end end end end
… мы будем вынуждены снова и снова перепечатывать имя метода и его аргументы.
def fib(n) memoize(:fib, n) do time(:fib, n) do synchronize(:fib) do if n <= 1 1 else fib(n - 1) + fib(n - 2) end end end end end
Это довольно хрупкая конструкция и она сломается в тот же миг, как только мы решим каким-либо образом изменить сигнатуру метода.
Тем не менее, это можно решить путем добавления вот такой штуки сразу после определения нашего метода.
def fib(n) if n <= 1 1 else fib(n - 1) + fib(n - 2) end end ActiveSupport::Memoizable.memoize :fib
И это должно вам напомнить то, что мы видели в Python — когда модификация метода шла сразу после самого метода.
# Ruby def fib(n) ... end ActiveSupport::Memoizable.memoize :fib # Python def fib(n): ... fib = memoize(fib)
Почему же сообществу Python не понравилось такое решение? Две причины:
- вы больше не можете отследить выполнение вашего кода сверху вниз;
- слишком просто переместить куда-то метод и забыть это сделать с кодом, который шел после него.
Давайте посмотрим на наш пример с Фибоначчи в Python.
def fib(n): if n <= 1: return 1 else return fib(n - 1) + fib(n - 2)
Мы хотим его мемоизовать, поэтому мы декорируем его функцией «memoize».
@memoize def fib(n): if n <= 1: return 1 else return fib(n - 1) + fib(n - 2)
И если мы хотим измерять время работы нашего метода или синхронизировать его вызовы, то мы просто добавляем еще один декоратор. Вот и все.
@synchronize @time @memoize def fib(n): if n <= 1: return 1 else return fib(n - 1) + fib(n - 2)
А теперь я покажу вам как добиться этого в Ruby (используя «+» вместо «@» и первую букву как заглавную). И самое прикольное, что мы можем добавить этот синтаксис декоратора в Ruby, который очень близок к синтаксису в Python, с помощью всего лишь 15 строчек кода.
+Synchronized +Timed +Memoized def fib(n) if n <= 1 1 else fib(n - 1) + fib(n - 2) end end
Погружаемся
Давайте вернемся к нашему примеру «send_money». Мы хотим добавить к нему декоратор «Transactional».
+Transactional def send_money(from, to, amount) from.balance -= amount to.balance += amount from.save! to.save! end
«Transactional» является подклассом «Decorator», который мы обсудим чуть ниже.
class Transactional < Decorator def call(orig, *args, &blk) ActiveRecord::Base.transaction do orig.call(*args, &blk) end end end
У него всего один метод «call», который будет вызван вместо нашего оригинального метода. В качестве аргументов он получает метод, который должен «обернуть», его аргументы и его блок, которые будут переданы ему при вызове.
def call(orig, *args, &blk)
Открываем транзакцию.
ActiveRecord::Base.transaction do
И далее вызываем оригинальный метод внутри блока транзакции.
orig.call(*args, &blk)
Обратите внимание, что структура нашего декоратора отличается от того, как декораторы работают в Python. Вместо того, чтобы определять новую функцию, которая будет получать аргументы, наш декоратор в Ruby будет получать сам метод и его аргументы при каждом вызове. Мы вынуждены так сделать из-за семантики привязки методов к объектам в Ruby, о которой мы поговорим чуть ниже.
Что же внутри класса «Decorator»?
class Decorator def self.+@ @@decorator = self.new end def self.decorator @@decorator end def self.clear_decorator @@decorator = nil end end
Эта штука — «+@» — оператор «унарный плюс», поэтому этот метод будет вызван, когда мы вызываем «+DecoratorName», как мы сделали с «+Transactional».
def self.+@
Так же нам нужен способ получить текущий декоратор.
def self.decorator @@decorator end
И способ обнулить текущий декоратор.
def self.clear_decorator @@decorator = nil end
Класс, который хочет иметь декорируемые методы должен быть расширен модулем «MethodDecorators».
class Bank extend MethodDecorators +Transactional def send_money(from, to, amount) from.balance -= amount to.balance += amount from.save! to.save! end end
Можно было бы расширить сразу класс «Class», но я думаю, что лучшей практикой в данном случае будет оставить такое решение на усмотрение конечного пользователя.
module MethodDecorators def method_added(name) super decorator = Decorator.decorator return unless decorator Decorator.clear_decorator orig_method = instance_method(name) define_method(name) do |*args, &blk| m = orig_method.bind(self) decorator.call(m, *args, &blk) end end end
«method_added» — это приватный метод класса, который вызывается каждый раз когда в классе определяется новый метод, давая нам удобный способ поймать момент начала создания метода.
def method_added(name)
Вызываем родительский «method_added». Об этом можно легко забыть, переопределяя методы вроде «method_added», «method_missing» или «respond_to?», но если вы этого не делаете, то легко можете сломать другие библиотеки.
super
Получаем текущий декоратор и прерываем функцию, если декоратора нет, иначе обнуляем текущий декоратор. Декоратор важно обнулить, потому что дальше мы переопределяем метод, что снова вызывает наш «method_added».
decorator = Decorator.decorator return unless decorator Decorator.clear_decorator
Извлекаем оригинальную версию метода.
orig_method = instance_method(name)
И переопределяем его.
define_method(name) do |*args, &blk|
«instance_method» на самом деле возвращает объект класса «UnboundMethod», который представляет собой метод, который не знает какому объекту он принадлежит, поэтому мы должны привязать его к текущему объекту.
m = orig_method.bind(self)
И затем мы вызываем декоратор, передавая ему оригинальный метод и аргументы для него.
decorator.call(m, *args, &blk)
Что еще?
Конечно тут есть еще ряд невероятно важных моментов, которые должны быть решены, прежде чем этот код можно будет считать готовым к production среде.
Множественные декораторы
Реализация, которую я привел позволяет использовать только один декоратор для каждого метода, но мы хотим иметь возможность использоват�� больше одного декоратора.
+Timed +Memoized def fib(n) ... end
Область видимости
«define_method» определяет публичные методы, но мы хотим приватные и защищенные методы, которые можно было бы декорировать с соблюдением их области видимости.
private +Transactional def send_money(from, to, amount) ... end
Методы класса
«method_added» и «define_method» работают только для методов экземпляра класса, поэтому нужно придумать что-то еще, чтобы декораторы стали работать для методов самого класса.
+Memoize def self.calculate ... end
Аргументы
В примере с Python я показал, что мы можем передавать декоратору значения. Мы хотим, чтобы у нас была возможность создавать какие угодно индивидуальные экземпляры декораторов для наших методов.
+Retry.new(3) def post_to_facebook ... end
gem install method_decorators
github.com/michaelfairley/method_decorators
Я реализовал все эти возможности, добавил исчерпывающий набор тестов и выкатил все это в виде gem. Используйте это, потому что я думаю это может сделать ваш код чище, повысить его читаемость и упростить его поддержку.
