Двойная отправка в Ruby с уточнениями

Уточнения - это относительно новая функция Ruby. Они действительно классные, но на момент написания я не мог найти ничего интересного из того, что с ними делали люди, поэтому вот вам предложение:

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

Сначала мы рассмотрим, что такое двойная диспетчеризация, а затем рассмотрим, как реализовать это с помощью классических структур Ruby и как это сделать с помощью уточнений.

Просто для ясности в предметной области: диспетчеризация относится к выбору реализации метода. Это вопрос:

Какой фрагмент кода мне следует запускать, когда определенное сообщение отправляется определенному объекту?

Таким образом, однократная отправка выбирает реализацию метода на основе типа одного объекта; Подумайте о разнице между bird.fly() и human.fly(). У первого просто взлетит bird. Последнее потребовало бы human найти путь к самолету или обрыву. В этом случае есть две реализации метода fly(). Мы выбираем, какой из них мы хотим выполнить, в зависимости от типа объекта, для которого мы вызываем метод.

Затем двойная отправка выбирает метод на основе типа двух разных объектов. Это похоже на вызов thing_that_eats.eat(biscuit) и thing_that_eats.eat(coffee), когда объект-человек знает, что нужно жевать печенье, но не кофе.

Это сложно для языка с утиным типом, потому что методы идентифицируются по классу, к которому они вызываются, а также по их арности, а не по типу их входных аргументов. Выполнение следующего в Ruby просто перезапишет первый метод приема пищи, и thing_that_eats задохнется, когда встретит заманчивое печенье:

Чтобы сохранить thing_that_eats, вам нужно знать, чем вы его кормите, и вызывать такие методы, как thing_that_eats.eat_crunchy_food(biscuit) и thing_that_eats.eat_liquid_food(soup). В качестве альтернативы вы можете передать любой тип еды в eat и заставить объект опрашивать класс еды, чтобы он мог делегировать определенному методу для обработки этой группы еды. Однако различные влиятельные люди, похоже, не любят такие объекты проверки типов.

Так как же избежать гастрономического кризиса, не изменив весь сценарий? Решение (которое мы будем придерживаться для продолжения повествования) - заставить взаимодействующие объекты работать вместе.

Совместная работа объектов для реализации двойной отправки предполагает передачу управления между ними, чтобы в конечном итоге найти правильную реализацию метода. Учтите следующее:

Обратите внимание, как управление переходит от thing_that_eats к biscuit и обратно к thing_that_eats:

eat вызывается thing_that_eats с biscuit в качестве аргумента. thing_that_eats однако, понятия не имеет, что biscuit это CrunchyFood и что его нужно жевать. Чтобы исправить это, thing_that_eats предполагает, что все, что передается ему в этом методе, имеет be_eaten_by_thing_that_eats метод.

be_eaten_by_thing_that_eats вызывается biscuit с thing_that_eats (self) в качестве аргумента. На этом этапе biscuit может сделать безопасное предположение, что то, что ему передается, является экземпляром ThingThatEats (это прямо в имени метода!), И он, очевидно, знает, что сам является экземпляром Biscuit. Учитывая всю эту информацию, biscuit может пойти дальше и сообщить thing_that_eats eat_crunchy_food о себе.

И вот оно: Double Dispatch. Мы выбрали реализацию метода на основе (1) класса объекта, для которого был вызван метод, и (2) класса аргумента, который был передан методу.

Но как насчет принципа «открыто-закрыто», о котором вы говорите? Что произойдет, если у меня нет доступа к исходному коду LiquidFood и мне нужно реализовать thing_that_eats.eat_liquid_food? Что, если SpoiledFood настолько раздут, что добавление к нему еще кода, такого как SpoiledFood.be_eaten_by_thing_that_eats, сильно ухудшит ваше состояние?

Итак, мы хотели бы иметь возможность определить реализацию eat на ThingThatEats, которая конкретно работает на LiquidFood. Мы не хотим изменять класс LiquidFood, потому что было бы нежелательно, если бы этот новый код повлиял на остальную часть кодовой базы. К счастью, у Руби есть доработки!

Представьте себе следующее:

Здесь мы определяем необходимые методы экземпляра LiquidFood в уточнении, которое мы активируем внутри класса ThingThatEats, а Blam-o! LiquidFood полностью игнорирует ThingThatEats, но ThingThatEats может иметь реализацию метода, специально разработанного для работы с LiquidFood и, возможно, еще одним для MushyFood и т. Д. Точно так же, как если бы на самом деле поддерживалась двойная отправка.

Сейчас самое время извлечь be_eaten_by_thing_that_eats из CrunchyFood, что мы определили ранее. Вставьте его в уточнение CrunchyFood в FoodRefinery, чтобы мы могли сохранить все эти специальные методы, которые будут вызываться только из одного контекста в одном месте.

Так почему это круто? Две причины:

  1. Это позволяет нам изолировать очень специфический код от остальной части кодовой базы и объединить все различные реализации для разных классов.
  2. Это отличная альтернатива проверке типов объектов.

Однако будьте осторожны, делая это в ситуации, когда вы будете using модулем уточнений во многих местах. Разработчики, не знакомые с нашей кодовой базой, могут отреагировать на NoMethodError, поступивший от MushyFood, реализуя be_eaten_by_thing_that_eats в MushyFood, а не в модуле уточнений.