Задумывались ли вы, что значит «смешивать» функциональность в ваших классах? Вы видели, как 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, которую вы хотели бы обсудить, оставьте комментарий.