Статьи

Руби Миксинс

Задумывались ли вы, что значит «смешивать» функциональность в ваших классах? Вы видели, как include и extend используемый в Ruby код и задавались вопросом, что происходит? Если так, то вы попали в нужное место. Миксины могут предложить большую гибкость вашему коду, и к концу этой статьи вы узнаете, как это сделать с помощью Ruby.

Что такое миксин?

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

Как насчет наследования?

Одна из областей, в которой я люблю использовать миксин — это регистрация. В моих Rails-приложениях мне нравится, что каждая модель имеет доступ к логгеру без какой-либо глобальной константы. Первоначально я думал об использовании некоторого класса базовой модели (который мог бы наследовать от ActiveRecord :: Base), который обеспечивал бы доступ к регистратору. Однако мне не очень понравилась идея, что все мои модели должны расширять специальный класс, чтобы получить регистратор. Да, они уже расширили бы ActiveRecord :: Base, так в чем же разница, верно?

Ну, а что если я хочу добавить доступ к регистратору в других частях моего приложения, которые не наследуются от ActiveRecord :: Base (или специального подкласса)? Ruby поддерживает только одиночное наследование, поэтому миксины казались лучшим решением.

Включить против Расширить

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

 module Logging def logger @logger ||= Logger.new(opts) end end class Person include Logging def relocate logger.debug "Relocating person..." # do relocation stuff end end 

В приведенном выше примере определен модуль Logging который предоставляет экземпляр объекта Logger через метод logger . Класс Person включает модуль Logging который добавляет свои методы в качестве методов экземпляра к классу Person . Стоит отметить, что все, что определено в модуле, добавляется в класс, включая методы (public, protected, private), атрибуты (например, attr_accessor :name ) и константы.

Если бы вместо этого я хотел предоставить регистратор с помощью метода класса, я бы использовал метод extend как показано ниже.

 module Logging def logger @@logger ||= Logger.new(opts) end end class Person extend Logging def relocate Person.logger.debug "Relocating person..." # could also access it with this self.class.logger.debug "Relocating person..." end end 

Между двумя последними фрагментами кода показаны три различия. В последнем случае вы можете видеть, что метод «logger» создает переменную класса @@logger вместо переменной экземпляра. Я сделал это, потому что ожидал, что Logger будет распространен на другие классы, а не включен. Конечно, я также использовал метод extend вместо include . Наконец, доступ к методу logger осуществляется через класс, а не экземпляр, такой как Person.logger.debug .

Также возможно смешать модуль с одним экземпляром во время выполнения, как показано в следующем примере.

 module Logger def logger @logger ||= Logger.new(opts) end end class Person; end p = Person.new # p.logger -- this would throw a NoMethodError p.extend Logger p.logger.debug "just a test" 

Включено против Расширенного

В некоторых случаях вам может понадобиться узнать, когда ваш модуль был включен или расширен. Один из вариантов использования может помочь в реализации типов классов, которые могут включать ваш модуль. Например, у меня был модуль, который проверял, существует ли таблица для моих моделей в приложении Rails. Однако я хотел убедиться, что мой модуль был включен только в классы, которые унаследованы от ActiveRecord::Base потому что модуль вызвал ActiveRecord::Base#table_exists? метод.

 module VerifyTableExistance def self.included(base) raise "#{self.name} can only be included into classes that inherit from ActiveRecord::Base, #{base.name} does not." unless (base < ActiveRecord::Base) raise "#{base.name} does not have a table yet" unless base.table_exists? end end class Person < ActiveRecord::Base include VerifyTableExistance end 

Модуль VerifyTableExistance реализует included метод класса, который вызывается всякий раз, когда модуль включается в другой модуль или класс. Это позволяет мне проверить, что класс, включающий модуль, является своего рода ActiveRecord::Base и, в этом примере, проверить, существует ли таблица для модели.

Другой распространенный шаблон — это определение модуля, который будет смешивать методы экземпляра и класса.

 module AcmeModel def self.included(base) base.extend(ClassMethods) end def brand "acme" end module ClassMethods def all # get all of the AcmeModel instances [] end end end class Widget include AcmeModel end w = Widget.new w.brand # "acme" is the output, Widget.all # invoke the class method that was added методы module AcmeModel def self.included(base) base.extend(ClassMethods) end def brand "acme" end module ClassMethods def all # get all of the AcmeModel instances [] end end end class Widget include AcmeModel end w = Widget.new w.brand # "acme" is the output, Widget.all # invoke the class method that was added 

В этом примере AcmeModel предоставляет метод экземпляра с именем brand . Более того, он переопределяет included метод класса, предоставляемый классом Module . included метод является обратным вызовом, который вызывается всякий раз, когда модуль включен другим классом или модулем. В этот момент мы расширяем класс, который включает AcmeModel , класс Widget в этом примере, с ClassMethods модуля ClassMethods .

Наконец, вы также должны знать, что ваш модуль может переопределить метод extended класса, который действует как обратный вызов при расширении модуля.

Вывод

Надеюсь, вы получили краткий обзор о том, что такое миксины и как их использовать в Ruby. Если нет, или если вы чувствуете, что я что-то пропустил, пожалуйста, оставьте комментарии. Это моя первая статья, и я надеюсь, что еще много. Кроме того, если есть конкретная область Ruby, которую вы хотели бы обсудить, оставьте комментарий.