В Elixir’е есть концепция behaviours
, или «поведенческих шаблонов», если угодно. Обратимся к официальной документации:
Протоколы — механизм, позволяющий реализовать полиморфизм в Elixir. Использование протокола доступно для любого типа данных, реализующего этот протокол.
О чем это вообще? Ну, сущности Elixir, или, как их иногда называют, «термы», неизменяемы. В Ruby мы привыкли определять методы на объектах, и эти методы просто изменяют объекты, как требуется. В Elixir’е это невозможно. Наверное, каждый, кто изучал ООП, разбирал стандартный пример, демонстрирующий полиморфизм: класс Animal
, с подклассами, по разному определяющими метод sound
:
class Animal
def sound
raise "Я — абстрактный зверь, я хранитель тишины (и тайны по совместительству)."
end
end
class Dog < Animal
def sound
puts "[LOG] Я собака, я лаю."
"гав"
end
end
class Cat < Animal
def sound
puts "[LOG] Я кот, я мяучу"
"мяу"
end
end
Теперь мы можем вызвать метод sound
на экземпляре любого животного, не утруждая себя предварительным определением класса. В Elixir’е все иначе, ведь там нет «методов, определенных на объектах». Для того, чтобы добиться примерно такой функциональности (типичным примером того, где это необходимо, является интерполяция в строках "#{object}"
), мы можем определить протокол.
Заметка на полях: еще можно использовать behaviours
, но для простоты и краткости мы остановимся именно на протоколах.
Протокол — это интерфейс, объявленный с использованием макроса defprotocol
. Для животного примера, приведенного выше, он выглядит так:
defprotocol Noisy do
@doc "Produces a sound for the animal given"
def sound(animal)
end
Реализация располагается в defimpl
:
defimpl Noisy, for: Dog do
def sound(animal), do: "woof"
end
defimpl Noisy, for: Cat do
def sound(animal), do: "meow"
end
Теперь мы можем использовать протокол, не заботясь о проверках, что там за зверь:
ExtrernalSource.animal
|> Noisy.sound
Ладно. А зачем нам вообще может потребоваться этот паттерн в руби? У нас уже есть полиморфизм, прямо из коробки, разве нет? Ну да. И нет. Наиболее очевидным примером ситуации, когда использование протоколов уместно, будет выделение общего поведения у классов, которые определены не нашим собственным кодом. Путь Рельс, получивший широкое распространение в руби благодаря DHH (Давиду Хайнемайеру Ханссону, создателю Ruby on Rails — перев.), — это манкипатчинг. Ирония заключается в том, что я лично люблю манкипатчинг.
Но все же иногда подход, использующий протоколы, выглядит более работоспособным. Вместо переоткрытия класса Integer
для переопределения методов для работы с датами, мы просто определяем соответствующий протокол с методами типа to_days
.
Таким образом, вместо something.to_days
у нас будет DateGuru.to_days(something)
. Весь код, отвечающий за преобразование дат и вообще операции над датами, будет располагаться в одном месте, гарантируя, что никто эти методы не перезапишет, и вообще, целостность не пострадает.
Я не говорю, что такой подход лучше. Я говорю, что он другой.
Чтобы все это попробовать, нам придется предоставить какой-нибудь DSL
для упрощения создания протоколов в руби. Давайте создадим его. Начнем, как обычно, с тестов. Вот так должно выглядеть объявление протокола:
module Protocols::Arithmetics
include Dry::Protocol
defprotocol do
defmethod :add, :this, :other
defmethod :subtract, :this, :other
defmethod :to_s, :this
def multiply(this, other)
raise "Умеем умножать только на целое" unless other.is_a?(Integer)
(1...other).inject(this) { |memo,| memo + this }
end
end
defimpl Protocols::Arithmetics, target: String do
def add(this, other)
this + other
end
def subtract(this, other)
this.gsub /#{other}/, ''
end
def to_s
this
end
end
defimpl target: [Integer, Float], delegate: :to_s, map: { add: :+, subtract: :- }
end
Давайте разберем этот код. Мы определили протокол Arithmetics
, отвечающий за сложение и вычитание. Как только эти операции определены для экземпляров какого-нибудь класса, умножение (multiply
) мы получаем бесплатно. Пример использования такого протокола: Arithmetics.add(42, 3) #? 45
. Наш DSL
поддерживает делегирование методов, маппинг и явное определение.
Этот надуманный и упрощенный пример не выглядит очень уж осмысленным, но он достаточно хорош для прогона наших тестов. Пора к ним уже и приступить:
expect(Protocols::Arithmetics.add(5, 3)).to eq(8)
expect(Protocols::Arithmetics.add(5.5, 3)).to eq(8.5)
expect(Protocols::Arithmetics.subtract(5, 10)).to eq(-5)
expect(Protocols::Arithmetics.multiply(5, 3)).to eq(15)
expect do
Protocols::Arithmetics.multiply(5, 3.5)
end.to raise_error(RuntimeException, "Умеем умножать только на целое")
Ну вот, мы и готовы заняться реализацией. Это на удивление просто.
Весь код помещается в один-единственный модуль. Мы назовем его BlackTie
, поскольку мы тут разговариваем про протоколы. Прежде всего, этот модуль будет хранить список соответствий объявленных протоколов и их реализаций.
module BlackTie
class << self
def protocols
@protocols ||= Hash.new { |h, k| h[k] = h.dup.clear }
end
def implementations
@implementations ||= Hash.new { |h, k| h[k] = h.dup.clear }
end
end
...
Заметка на полях: трюк с default_proc
в объявлении хэша (Hash.new { |h, k| h[k] = h.dup.clear }
) помогает создать хэш с прозрачным «глубоким» доступом (обращение по несуществующему ключу сколь угодно глубоко вернет пустой хэш).
Реализация defmethod
тривиальна: мы просто сохраняем метод в глобальном списке соответствий для текущего протокола (в глобальном хэше @protocols
):
def defmethod(name, *params)
BlackTie.protocols[self][name] = params
end
Объявление самого протокола чуть более заковыристо (некоторые детали в этой заметке опущены, чтобы не замусоривать общую картину; полный код доступен тут).
def defprotocol
raise if BlackTie.protocols.key?(self) || !block_given?
ims = instance_methods(false)
class_eval(&Proc.new)
(instance_methods(false) - ims).each { |m| class_eval { module_function m } }
singleton_class.send :define_method, :method_missing do |method, *args|
raise Dry::Protocol::NotImplemented.new(:method, self.inspect, method)
end
BlackTie.protocols[self].each do |method, *|
singleton_class.send :define_method, method do |receiver = nil, *args|
impl = receiver.class.ancestors.lazy.map do |c|
BlackTie.implementations[self].fetch(c, nil)
end.reject(&:nil?).first
raise Dry::Protocol::NotImplemented.new(:protocol, self.inspect, receiver.class) unless impl
impl[method].(*args.unshift(receiver))
end
end
end
Вкратце, в этом коде четыре блока. Прежде всего мы проверяем, что определение протокола отвечает всем необходимым условиям. Затем мы выполняем блок, переданный в этот метод, запоминая, какие методы добавились. Эти методы мы экспортируем посредством module_function
. В третьем блоке мы определяем method_missing
, который отвечает за выброс исключения с внятным сообщением об ошибке при попытке вызывать не существующие методы. И, наконец, мы определяем методы, либо делегируя их соответствующей реализации, если таковая существует, или выбрасывая внятное исключение, в случае, если для данного объекта реализация не найдена.
Ну, осталось только определить defimpl
. Код ниже тоже слегка упрощен, полная версия там же по ссылке.
def defimpl(protocol = nil, target: nil, delegate: [], map: {})
raise if target.nil? || !block_given? && delegate.empty? && map.empty?
# builds the simple map out of both delegates and map
mds = normalize_map_delegates(delegate, map)
Module.new do
mds.each(&DELEGATE_METHOD.curry[singleton_class]) # delegation impl
singleton_class.class_eval(&Proc.new) if block_given? # block takes precedence
end.tap do |mod|
mod.methods(false).tap do |meths|
(BlackTie.protocols[protocol || self].keys - meths).each_with_object(meths) do |m, acc|
logger.warn("Implicit delegate #{(protocol || self).inspect}##{m} to #{target}")
DELEGATE_METHOD.(mod.singleton_class, [m] * 2)
acc << m
end
end.each do |m|
[*target].each do |tgt|
BlackTie.implementations[protocol || self][tgt][m] = mod.method(m).to_proc
end
end
end
end
module_function :defimpl
Невзирая на кажущуюся невнятность этого кода, он очень прост: мы создаем анонимный модуль, определяем на нем методы и назначаем его главным исполняющим методов, делегированных из протокола. Вызов Arithmetics.add(5, 3)
приведет к определению ресивера (5
), ретроспективного поиска реализации (defimpl Arithmetics, target: Integer
) и вызову соответствуюзего метода (:+
). Это все определяется строкой
defimpl target: [Integer, ...], ..., map: { add: :+, ... }
Если мне не удалось убедить вас в полезности изложенного подхода, я попробую еще раз. Представьте себе протокол Tax
(налог). Он можен быть определен для таких классов, как ItemToSell
, Shipment
, Employee
, Lunch
и так далее. Даже если эти классы пришли в ваш проект из разных гемов и источников данных.
> Репозиторий dry-behaviour гема на github.
Наслаждайтесь!
Комментарии (28)
vanburg
22.12.2016 15:49> Ирония заключается в том, что я лично люблю манкипатчинг
Как по мне, возможность МП как раз и убивает (добивает?) руби-экосистему, и является причиной, почему я перешел на элексир. Слишком много свободы дает этот прекрасный язык (я о руби). Очень много соблазнов, ведущих к кошмарным решениям. Как говорится: «попробовали бы они сделать это в go», который по рукам бьет за любую попытку отойти строгих правил.am-amotion-city
22.12.2016 15:56И да, и нет. Вот пример выше как раз про самосознание, которое мешает хорошим разработчикам на руби манкипатчить все, что шевелится, как завещал DHH.
Есть Петр со своим
dry-rb
, который идеологически очень правильный. Трейлблейзеры всякие, и так далее. Вон люди подтягиваются тоже.
На мой вкус, эликсир лучше не тем, что не позволит выстрелить себе в ногу (еще как позволит, на самом деле, как только проникаешься и начинаешь писать макросами и коллбэками времени компиляции :)). Эликсир лучше виртуальной машиной эрланга, потому что MRI уже сейчас — нонсенс, а JRuby до сих пор вызывает ощущение красивой, но нерабочей игрушки (может быть, я и ошибаюсь, впрочем.)
printercu
22.12.2016 18:36Долго сдерживал себя от написания критичного комментария, но аргументов всё больше а понимания всё меньше.
Вообще говоря, взяться за перевод меня побудило то, что эта имплементация (даже не так важно, чего именно) — блестящий пример внятного DSL
Методы по 20+ строк — это не блестящий пример. Ни капли объяснения. Объяснили бы зачем хотя бы сохраняются
params
вdefmethod
?
потому что MRI уже сейчас — нонсенс
Если такие протоколы в продакшене использовать, то остается только догадываться, насколько оптимален код в остальном.
Про реализацию немного.
ims = instance_methods(false)
class_eval(&Proc.new)
(instance_methods(false) - ims).each { |m| class_eval { module_function m } }
Вместо всего этого достаточно
singleton_class.class_eval
.
class_eval(&Proc.new)
Запрет на
&block
? Или "потому что могу!".
- https://github.com/am-kantox/dry-behaviour/blob/master/lib/dry/behaviour/black_tie.rb#L50-L55 всё заменяется
this.send(target, *args, **params, &?)
. (Сюда же:&?
что О_о??? потому что могу!) - Память то течёт в рельсах в девелопменте. Ах да, они же не труЪ. Этот пункт тогда можно не считать, пусть мучаются.
- ...
Конечно, если автор руби не знает достаточно хорошо, он и будет сочинять такие библиотеки. Ведь всё запросто решается созданием нескольких модулей. Я допускаю, что такие протоколы могут найти применение в руби, но не могу представить себе и единственного варианта (автор нам не даёт и подсказки, как их применять в жизни).
Зачем окружающим такой код показывать, тем более с целью научить писать дсл. Я считаю, что такие статьи вредные.
PS. В питоне в инстанс методы, передают первым аргументом сам инстанс. Говорят, это очень явно и всем нравится. Ждём
dry-explicit
. Потомdry-decorators
(для методов, не объектов).
PPS. До сих пор в догадках теряюсь, кто плюсует такую статью. На гитхабе у репа ни звезды. Друзья?! Накрутка?!
am-amotion-city
22.12.2016 18:47- Чем вам всем методы по 20+ строк не угодили?
- Протоколы тут ни при чем, у MRI серьезные проблемы с параллелизацией.
- Нет, это не эквивалентно
singleton_class.class_eval
. Можете попытаться понять почему. &block
не эквивалентен&Proc.new
, гуглить по словам «на стеке».- Нет, это неверно.
- Может быть.
И нет, это не накрутка и не друзья. Доказать, впрочем, мне это не удастся.
printercu
23.12.2016 00:36- Действительно, был не прав. Не учёл, что def могут идти в перемешку с дсл.
- Я честно пытался, ничего не нашёл. Пробовал сравнивать caller внутри блока с &block и Proc.new — одинаковые. Расскажите, пожалуйста, в чём отличие.
- https://github.com/printercu/dry-behaviour/commit/046163b3216f884d2bc7dee757cfac15e7b1f341 Ок, был неправ, по-другому:
this.send(target, *args, &?)
. Тесты проходят.
am-amotion-city
23.12.2016 08:42Вот, видите, вы говорите, что статья вредная, а сами уже на пути к разбору подробностей и деталей, которые обычно остаются за кадром для среднестатистического рубиста.
Передача блока через явное определение в сигнатуре (
&block
) не создает вообще никаких объектов, на стек виртуальной машины просто передается указатель.block_given?
таким образом просто проверяет, есть на стеке указатель, или нет.yield
и его экплицитный аналогblock.()
просто передадут управление по этому указателю. Это сделано в YARV’е, насколько я понимаю, для скорости.
Proc.new
же создаст руби объект из этого блока. Со всеми вытекающими: мы теперь медленнее, и платим за создание объекта; зато можем прикрутить к нему аспект, например. Поставить точку останова. Отладить. Да что угодно.
Справедливости ради, хочу отметить, что не все воодушевлены существованием этой возможности в интерпретаторе Коши Сасада. Но мне лично, как раз, кажется, что это правильная имплементация.
this.send(target, *args, &?)
: у меня в домашнем геме со всяким мусором есть точно такой же кусок кода. Я не помню, то ли это наследие 1.9, когда double-splatted только появились, то ли в каких-то неочевидных местах оно проседает. Кажется, если вы последним параметром в*args
передадите хэш, начинается путаница, или типа того. Под рукой сейчас нет ничего, на чем можно было бы проверить, если прямо очень интересно — могу поднять свои заметки пятилетней давности.
Ну и отвечу заодно на «
&?
что О_о??? потому что могу!» — а что плохого в том, чтобы называть переменные максимально внятно? Я со своей командой точно так же полтора года боролся, прежде чем они привыкли и увидели в этом прелесть. Перенастройте клавиатуру один раз и живите счастливо всю жизнь: такая запись гораздо более лаконичная, изящная, да и просто внятная, чем&cb
/&block
.printercu
23.12.2016 11:16Я знаю, что yield быстрее: https://github.com/JuanitoFatas/fast-ruby#proccall-and-block-arguments-vs-yieldcode
В этом то и плохо: вы оптимизируете там, где оптимизация не нужна в принципе:
- Рядом циклы, куча других тяжелых инструкций.
- Протоколы будут создаваться только во время загрузки приложения, и их вряд ли будет больше 1000.
- Proc.new создает объект на стеке. Насколько такая связка быстрее (или уже медленнее?) чем явный &block.
- По ссылке видно, что там миллионы оп/с. Это экономия сотен пикосекунд! Сравните это со временем, потраченным в кейсе на 4 строки, в нём 2..4 вызова метода. Он к тому же выполняется не только на этапе загрузки.
УпсWarming up -------------------------------------- proc_new 79.098k i/100ms and_block 82.507k i/100ms Calculating ------------------------------------- proc_new 1.220M (± 5.6%) i/s - 6.091M in 5.008569s and_block 1.395M (± 4.5%) i/s - 7.013M in 5.038238s Comparison: and_block: 1394884.4 i/s proc_new: 1220305.9 i/s - 1.14x slower
require 'benchmark/ips' def target yield end def proc_new target &Proc.new end def and_block(&block) target(&block) end Benchmark.ips do |x| x.report(:proc_new) { proc_new { 1 + 1 } } x.report(:and_block) { and_block { 1 + 1 } } x.compare! end
am-amotion-city
23.12.2016 11:27В этом то и плохо: вы оптимизируете там, где оптимизация не нужна в принципе:
Я не уверен, что мне удалось распарсить русский язык в этой фразе, но если я понял правильно, читать вы не умеете.
Proc.new
всегда медленнее. Зато всегда с объектом. Оптимизации никакой нет, наоборот, есть ровно обратный процесс: мы незначительно жертвуем производительностью ради возможности воткнуть аспект / отладить код объектом.
вы гонитесь за уменьшениям количества объектов на стеке
Щито? Где это?
без объяснений бы советуете читать код, того ненужного case
Щито? Где это?
Вы разговариваете с тараканами в вашей голове. Нравится вам писать код, как завещал Батцов — не смею мешать. Нравится ограничивать себя в XXI веке использованием только ASCII — ради бога. Считаете вы, что метод длиной 20 строк непонятен? — Мои соболезнования. Хотели меня уязвить словами «индусский код»? — не получилось, код не мой, да и не вижу я в нем ничего индусского. Про утечки памяти тоже смешно.
Продолжайте писать модельки в рельсиках по 2 строчки, чтобы не разозлить рубокоп, что я могу еще сказать.
printercu
23.12.2016 12:052.3.0 :003 > def x(&block); [block.class, block.object_id]; end; x {} => [Proc, 70195284674480]
Это разве не объект? Что с ним нельзя сделать такого, что можно с Proc.new. Код приведите, пожалуйста.
am-amotion-city
23.12.2016 12:35Пожалуйста:
def test_cb(&cb) puts [cb.class.ancestors.inspect, cb.()] end def test_proc Proc.new.tap do |cb| puts [cb.class.ancestors.inspect, cb.()] end end Proc.prepend(Module.new do def initialize(*args, &cb) puts "INIT #{[block_given?, cb, *args].inspect}" super end def call(*args, &cb) puts "CALL #{[block_given?, cb, *args].inspect}" super end end) puts '='*40 test_cb { "INSIDE TEST_CB"} puts '='*40 test_proc { "INSIDE TEST_PROC"} puts '='*40
Выведет:
# ======================================== # CALL [false, nil] # [#<Module:0x0000000213a068>, Proc, Object, Kernel, BasicObject] # INSIDE TEST_CB # ======================================== # INIT [false, nil] # CALL [false, nil] # [#<Module:0x0000000213a068>, Proc, Object, Kernel, BasicObject] # INSIDE TEST_PROC # ========================================
Внезапно
Proc#initialize
не был вызван в случае эксплицитного блока. Вообще.printercu
23.12.2016 12:50А зачем такое писать? Как это в реальной жизни пригодится?
зато можем прикрутить к нему аспект, например. Поставить точку останова. Отладить. Да что угодно.
Что? А обычные брэйкпоинты на строчках уже не модные? Как вы будете отлаживать код, где (о ужас) нет Proc.new? Про аспект вообще впервые слышу. Ну да, нам среднестатистическим рубистам это только предстоит постичь (или даже не предстоит).
am-amotion-city
23.12.2016 13:21+1Вы меня спросили «чем отличается» — я ответил. Вас устраивает, что блок создается через магию, в обход стандартного вызова конструктора? — прекрасно, меня — нет. Не нужны вам нестандартные профайлеры, или логгеры, которые присасываются и меряют/пишут статистику по вызовам
Proc
? — ну и отлично, мне иногда нужны.
Вообще, тактика «это неверно, покажите код»—«вот, пожалуйста»—«а, да, оказывается это верно, но, значит, это никому не нужно» — довольно редко приводит к успеху.
printercu
23.12.2016 12:06Что смешного в утечках памяти? Классы-протоколы в ключах хэша при релоаде остануться, а новые будут созданы.
am-amotion-city
23.12.2016 12:47Классы-протоколы в ключах хэша при релоаде остануться, а новые будут созданы.
Да ну? А как же:
raise if BlackTie.protocols.key?(self)
То есть, новые точно не будут созданы. Ну а старые умрут вместе с классом. Именно поэтому, кстати, рубокоп (который не всегда только мешает) советует создавать не class variables, а class instance variables. Если бы тут были переменные класса, вы были бы правы (они еще плохи тем, что шарятся между всеми наследниками, но это тут ни при чем, вроде).
printercu
23.12.2016 12:53$ rails c 2.3.0 (main):0[1] > User.hash => 518965653690491956 2.3.0 (main):0[2] > reload! Reloading... => true 2.3.0 (main):0[3] > User.hash => -3861495400228448213
Не будет эксепшена. А утечка будет.
printercu
23.12.2016 12:56Как они умрут вместе с классом?
@protocols
— инстанс переменная BlackTie, которая не очищается при релоаде никак сейчас. Ну и это не WeakMap чтобы автоматически чистить старые ключи.am-amotion-city
23.12.2016 13:35Ох.
class TestA; end class User attr_reader :test_a @test_a = TestA.new ... end
Внимательно следим:
main ? ObjectSpace.each_object(TestA).count #? 1 main ? User.hash #? -1786282447015903859 main ? reload! Reloading... #? true main ? User.hash #? -1071192552921021983 main ? ObjectSpace.each_object(TestA) { |c| puts c.__id__ } #? 93622000 #? 1 main ? reload! Reloading... #? true main ? User.hash #? 308471798834384769 main ? ObjectSpace.each_object(TestA) { |c| puts c.__id__ } #? 101380860 #? 1
Кого тут надо эксплицитно очищать? При чем тут
WeakMap
, простите?printercu
23.12.2016 13:53Так будет понятнее?2.3.0 (main):0[1] > Dry::BlackTie.protocols.size => 0 2.3.0 (main):0[2] > Adder NameError: uninitialized constant Adder::Protocols from /Users/max/Workspace/ruby/jam_delivery/app/models/adder.rb:15:in `<module:Adder>' 2.3.0 (main):0[3] > Adder => Adder 2.3.0 (main):0[4] > Dry::BlackTie.protocols.size => 2 2.3.0 (main):0[5] > reload! Reloading... => true 2.3.0 (main):0[6] > Dry::BlackTie.protocols.size => 2 2.3.0 (main):0[7] > Adder => Adder 2.3.0 (main):0[8] > Dry::BlackTie.protocols.size => 3 2.3.0 (main):0[9] > reload! Reloading... => true 2.3.0 (main):0[10] > Dry::BlackTie.protocols.size => 3 2.3.0 (main):0[11] > Adder => Adder 2.3.0 (main):0[12] > Dry::BlackTie.protocols.size => 4 2.3.0 (main):0[13] > Dry::BlackTie.protocols.keys => [ [0] Adder, [1] Adder, [2] Adder, [3] Adder ]
am-amotion-city
23.12.2016 14:18А, вы имплементацию в модель запихали? Да, теперь понимаю, в чем проблема. В жирных моделях. Сиречь, как обычно, виноваты рельсы со своими подходами, а валим все на здоровую голову.
Никто в здравом уме и твердой памяти не станет класть протоколы внутрь моделей. Они сбоку, чем и хороши. Как и нормальные валидаторы (серьезно, посмотрите на
dry-rb
, в частности — наdry-validation
) — они не внутри рельс, а сбоку.printercu
23.12.2016 14:35Вы что? В какие модели? Я же написал, что пример точно из ридми. Только убрал module Protocols. Положил в app/models просто в целях тестирования. В нее нельзя положить файл? Нужно чтобы не было релоада, и самому перезапускать сервер при правке протокола? Про то что это в деве на рельсах, это я в самом первом комментарии написал.
am-amotion-city
23.12.2016 14:54Все, что лежит внутри
app
в рельсах автоматически релоадится (что, в принципе, удобно) и автоматически прелоадится (причем, как бог на душу положит), что приводит к катастрофам при, например, случайно одинаковых именах в разных неймспейсах. Поэтому все, что напрямую рельс не касается, люди обычно складывают вlib
.printercu
23.12.2016 15:14Ок. Положите в lib. Скорее всего, вы захотите
require_dependency 'protocols/adder'
вместоrequire 'protocols/adder'
, чтобы не перезапускать сервер в деве, при каждой правке. Тут опять будет утечка.am-amotion-city
23.12.2016 15:23При каждой правке чего? При каждой правке самого протокола (они обычно просты, как три копейки, поэтому правок обычно одна) — я смирюсь с утечкой при релоадах.
Вообще, я не понимаю предмет дискуссии: вы хотите, чтобы я с вами согласился, что память в
dev
приreload!
расходуется неэкономно? — я согласен, мне не хватит жизни, чтобы утекли все мои 128G.
Я согласен также с автором гема: писать защитный код против релоада (!) в дев-моде (!!) в рельсах (!!!) — это за гранью добра и зла. Можно подумать, это единственная проблема с памятью в дев-моде.
Не нравится вам такой подход? — я не агитирую ни в коем случае. Нам вот понравился, мы используем, рады.
printercu
23.12.2016 12:19Представьте себе протокол Tax (налог). Он можен быть определен для таких классов, как ItemToSell, Shipment, Employee, Lunch и так далее.
Вот только для товаров — это ндс, для сотрудников — ндфл, для доставки — я не знаю, и уж тем более не понимаю, какое отношение к остальным имеют обеды. И как все эти объекты связаны между собой.
am-amotion-city
23.12.2016 12:42В том-то и дело, что никак не связаны. Мы определяем имплементацию протокола для каждого из этих объектов, а потом считаем общий налог как:
[*items, *shipments, *employees, *lunches].map(&Tax.method(:get)).inject(:+)
akzhan
То, что Вы хотели реализовать через протокол, насколько я понял,
в Ruby принято реализовывать через уточнения.
https://ruby-doc.org/core-2.1.1/doc/syntax/refinements_rdoc.html
am-amotion-city
refinements
даже рядом не стояли с протоколами, вы все неверно поняли.refinements
не взлетели и в продакшене их [фактически] никто не использует [кроме совсем фриков].am-amotion-city
Вообще говоря, взяться за перевод меня побудило то, что эта имплементация (даже не так важно, чего именно) — блестящий пример внятного
DSL
, а новичкам очень не хватает понимания «как оно работает». Пример с пиццей всем успел надоесть до колик, а техники наподобие создания анонимного модуля, исполнения в его биндинге текущего класса и получение референсов на методы — очень емко описаны вот этим примером.На мой взгляд, это даже скорее пособие по написанию
DSL
, хотя протоколы сами по себе очень полезная штука, которая уже доказала уместность в нашем продакшене.