В 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)


  1. akzhan
    22.12.2016 01:16
    -1

    То, что Вы хотели реализовать через протокол, насколько я понял,


    в Ruby принято реализовывать через уточнения.


    https://ruby-doc.org/core-2.1.1/doc/syntax/refinements_rdoc.html


    1. am-amotion-city
      22.12.2016 09:13
      -1

      1. Это перевод.
      2. refinements даже рядом не стояли с протоколами, вы все неверно поняли.
      3. refinements не взлетели и в продакшене их [фактически] никто не использует [кроме совсем фриков].
      4. Принято давать ссылку на последнюю версию корки, этого можно достичь, убрав из ссылки версию вообще: https://ruby-doc.org/core/doc/syntax/refinements_rdoc.html


    1. am-amotion-city
      22.12.2016 09:19
      +1

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


      На мой взгляд, это даже скорее пособие по написанию DSL, хотя протоколы сами по себе очень полезная штука, которая уже доказала уместность в нашем продакшене.


  1. vanburg
    22.12.2016 15:49

    > Ирония заключается в том, что я лично люблю манкипатчинг
    Как по мне, возможность МП как раз и убивает (добивает?) руби-экосистему, и является причиной, почему я перешел на элексир. Слишком много свободы дает этот прекрасный язык (я о руби). Очень много соблазнов, ведущих к кошмарным решениям. Как говорится: «попробовали бы они сделать это в go», который по рукам бьет за любую попытку отойти строгих правил.


    1. am-amotion-city
      22.12.2016 15:56

      И да, и нет. Вот пример выше как раз про самосознание, которое мешает хорошим разработчикам на руби манкипатчить все, что шевелится, как завещал DHH.


      Есть Петр со своим dry-rb, который идеологически очень правильный. Трейлблейзеры всякие, и так далее. Вон люди подтягиваются тоже.


      На мой вкус, эликсир лучше не тем, что не позволит выстрелить себе в ногу (еще как позволит, на самом деле, как только проникаешься и начинаешь писать макросами и коллбэками времени компиляции :)). Эликсир лучше виртуальной машиной эрланга, потому что MRI уже сейчас — нонсенс, а JRuby до сих пор вызывает ощущение красивой, но нерабочей игрушки (может быть, я и ошибаюсь, впрочем.)


  1. printercu
    22.12.2016 18:36

    Долго сдерживал себя от написания критичного комментария, но аргументов всё больше а понимания всё меньше.


    1. Вообще говоря, взяться за перевод меня побудило то, что эта имплементация (даже не так важно, чего именно) — блестящий пример внятного DSL

      Методы по 20+ строк — это не блестящий пример. Ни капли объяснения. Объяснили бы зачем хотя бы сохраняются params в defmethod?


    2. потому что MRI уже сейчас — нонсенс

      Если такие протоколы в продакшене использовать, то остается только догадываться, насколько оптимален код в остальном.


      Про реализацию немного.


    3. ims = instance_methods(false)
      class_eval(&Proc.new)
      (instance_methods(false) - ims).each { |m| class_eval { module_function m } }

      Вместо всего этого достаточно singleton_class.class_eval.


    4. class_eval(&Proc.new)

      Запрет на &block? Или "потому что могу!".


    5. https://github.com/am-kantox/dry-behaviour/blob/master/lib/dry/behaviour/black_tie.rb#L50-L55 всё заменяется this.send(target, *args, **params, &?). (Сюда же: &? что О_о??? потому что могу!)
    6. Память то течёт в рельсах в девелопменте. Ах да, они же не труЪ. Этот пункт тогда можно не считать, пусть мучаются.
    7. ...

    Конечно, если автор руби не знает достаточно хорошо, он и будет сочинять такие библиотеки. Ведь всё запросто решается созданием нескольких модулей. Я допускаю, что такие протоколы могут найти применение в руби, но не могу представить себе и единственного варианта (автор нам не даёт и подсказки, как их применять в жизни).


    Зачем окружающим такой код показывать, тем более с целью научить писать дсл. Я считаю, что такие статьи вредные.


    PS. В питоне в инстанс методы, передают первым аргументом сам инстанс. Говорят, это очень явно и всем нравится. Ждём dry-explicit. Потом dry-decorators (для методов, не объектов).


    PPS. До сих пор в догадках теряюсь, кто плюсует такую статью. На гитхабе у репа ни звезды. Друзья?! Накрутка?!


    1. am-amotion-city
      22.12.2016 18:47

      1. Чем вам всем методы по 20+ строк не угодили?
      2. Протоколы тут ни при чем, у MRI серьезные проблемы с параллелизацией.
      3. Нет, это не эквивалентно singleton_class.class_eval. Можете попытаться понять почему.
      4. &block не эквивалентен &Proc.new, гуглить по словам «на стеке».
      5. Нет, это неверно.
      6. Может быть.

      И нет, это не накрутка и не друзья. Доказать, впрочем, мне это не удастся.


      1. printercu
        23.12.2016 00:36

        1. Действительно, был не прав. Не учёл, что def могут идти в перемешку с дсл.
        2. Я честно пытался, ничего не нашёл. Пробовал сравнивать caller внутри блока с &block и Proc.new — одинаковые. Расскажите, пожалуйста, в чём отличие.
        3. https://github.com/printercu/dry-behaviour/commit/046163b3216f884d2bc7dee757cfac15e7b1f341 Ок, был неправ, по-другому: this.send(target, *args, &?). Тесты проходят.


        1. 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.


          1. 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


            1. am-amotion-city
              23.12.2016 11:27

              В этом то и плохо: вы оптимизируете там, где оптимизация не нужна в принципе:

              Я не уверен, что мне удалось распарсить русский язык в этой фразе, но если я понял правильно, читать вы не умеете. Proc.new всегда медленнее. Зато всегда с объектом. Оптимизации никакой нет, наоборот, есть ровно обратный процесс: мы незначительно жертвуем производительностью ради возможности воткнуть аспект / отладить код объектом.


              вы гонитесь за уменьшениям количества объектов на стеке

              Щито? Где это?


              без объяснений бы советуете читать код, того ненужного case

              Щито? Где это?




              Вы разговариваете с тараканами в вашей голове. Нравится вам писать код, как завещал Батцов — не смею мешать. Нравится ограничивать себя в XXI веке использованием только ASCII — ради бога. Считаете вы, что метод длиной 20 строк непонятен? — Мои соболезнования. Хотели меня уязвить словами «индусский код»? — не получилось, код не мой, да и не вижу я в нем ничего индусского. Про утечки памяти тоже смешно.


              Продолжайте писать модельки в рельсиках по 2 строчки, чтобы не разозлить рубокоп, что я могу еще сказать.


              1. printercu
                23.12.2016 12:05

                2.3.0 :003 > def x(&block); [block.class, block.object_id]; end; x {}
                 => [Proc, 70195284674480]

                Это разве не объект? Что с ним нельзя сделать такого, что можно с Proc.new. Код приведите, пожалуйста.


                1. 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 не был вызван в случае эксплицитного блока. Вообще.


                  1. printercu
                    23.12.2016 12:50

                    А зачем такое писать? Как это в реальной жизни пригодится?


                    зато можем прикрутить к нему аспект, например. Поставить точку останова. Отладить. Да что угодно.

                    Что? А обычные брэйкпоинты на строчках уже не модные? Как вы будете отлаживать код, где (о ужас) нет Proc.new? Про аспект вообще впервые слышу. Ну да, нам среднестатистическим рубистам это только предстоит постичь (или даже не предстоит).


                    1. am-amotion-city
                      23.12.2016 13:21
                      +1

                      Вы меня спросили «чем отличается» — я ответил. Вас устраивает, что блок создается через магию, в обход стандартного вызова конструктора? — прекрасно, меня — нет. Не нужны вам нестандартные профайлеры, или логгеры, которые присасываются и меряют/пишут статистику по вызовам Proc? — ну и отлично, мне иногда нужны.


                      Вообще, тактика «это неверно, покажите код»—«вот, пожалуйста»—«а, да, оказывается это верно, но, значит, это никому не нужно» — довольно редко приводит к успеху.


              1. printercu
                23.12.2016 12:06

                Что смешного в утечках памяти? Классы-протоколы в ключах хэша при релоаде остануться, а новые будут созданы.


                1. am-amotion-city
                  23.12.2016 12:47

                  Классы-протоколы в ключах хэша при релоаде остануться, а новые будут созданы.

                  Да ну? А как же:


                  raise if BlackTie.protocols.key?(self)

                  То есть, новые точно не будут созданы. Ну а старые умрут вместе с классом. Именно поэтому, кстати, рубокоп (который не всегда только мешает) советует создавать не class variables, а class instance variables. Если бы тут были переменные класса, вы были бы правы (они еще плохи тем, что шарятся между всеми наследниками, но это тут ни при чем, вроде).


                  1. 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

                    Не будет эксепшена. А утечка будет.


                  1. printercu
                    23.12.2016 12:56

                    Как они умрут вместе с классом? @protocols — инстанс переменная BlackTie, которая не очищается при релоаде никак сейчас. Ну и это не WeakMap чтобы автоматически чистить старые ключи.


                    1. 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, простите?


                      1. 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
                        ]


                        1. am-amotion-city
                          23.12.2016 14:18

                          А, вы имплементацию в модель запихали? Да, теперь понимаю, в чем проблема. В жирных моделях. Сиречь, как обычно, виноваты рельсы со своими подходами, а валим все на здоровую голову.


                          Никто в здравом уме и твердой памяти не станет класть протоколы внутрь моделей. Они сбоку, чем и хороши. Как и нормальные валидаторы (серьезно, посмотрите на dry-rb, в частности — на dry-validation) — они не внутри рельс, а сбоку.


                          1. printercu
                            23.12.2016 14:35

                            Вы что? В какие модели? Я же написал, что пример точно из ридми. Только убрал module Protocols. Положил в app/models просто в целях тестирования. В нее нельзя положить файл? Нужно чтобы не было релоада, и самому перезапускать сервер при правке протокола? Про то что это в деве на рельсах, это я в самом первом комментарии написал.


                            1. am-amotion-city
                              23.12.2016 14:54

                              Все, что лежит внутри app в рельсах автоматически релоадится (что, в принципе, удобно) и автоматически прелоадится (причем, как бог на душу положит), что приводит к катастрофам при, например, случайно одинаковых именах в разных неймспейсах. Поэтому все, что напрямую рельс не касается, люди обычно складывают в lib.


                              1. printercu
                                23.12.2016 15:14

                                Ок. Положите в lib. Скорее всего, вы захотите require_dependency 'protocols/adder' вместо require 'protocols/adder', чтобы не перезапускать сервер в деве, при каждой правке. Тут опять будет утечка.


                                1. am-amotion-city
                                  23.12.2016 15:23

                                  При каждой правке чего? При каждой правке самого протокола (они обычно просты, как три копейки, поэтому правок обычно одна) — я смирюсь с утечкой при релоадах.


                                  Вообще, я не понимаю предмет дискуссии: вы хотите, чтобы я с вами согласился, что память в dev при reload! расходуется неэкономно? — я согласен, мне не хватит жизни, чтобы утекли все мои 128G.


                                  Я согласен также с автором гема: писать защитный код против релоада (!) в дев-моде (!!) в рельсах (!!!) — это за гранью добра и зла. Можно подумать, это единственная проблема с памятью в дев-моде.


                                  Не нравится вам такой подход? — я не агитирую ни в коем случае. Нам вот понравился, мы используем, рады.


  1. printercu
    23.12.2016 12:19

    Представьте себе протокол Tax (налог). Он можен быть определен для таких классов, как ItemToSell, Shipment, Employee, Lunch и так далее.

    Вот только для товаров — это ндс, для сотрудников — ндфл, для доставки — я не знаю, и уж тем более не понимаю, какое отношение к остальным имеют обеды. И как все эти объекты связаны между собой.


    1. am-amotion-city
      23.12.2016 12:42

      В том-то и дело, что никак не связаны. Мы определяем имплементацию протокола для каждого из этих объектов, а потом считаем общий налог как:


      [*items, *shipments, *employees, *lunches].map(&Tax.method(:get)).inject(:+)