Что такое «синглтон-класс» в Ruby
Наверное каждый разработчик Ruby знает о существовании «синглтон-класса» (англ. Singleton Class, также известен как «метакласс» и «eigenclass»), но при этом не каждый знает что это и какова его роль. В то же время понимание синглтон-класса является важным и полезным, и открывает новые возможности для программиста, особенно в метапрограммировании. Цель данной статьи - дать представление о синглтон-классе, его роли и особенностях.
Не путайте термин «Singleton Class» с термином «Singleton Pattern», обозначающим шаблон проектирования «Одиночка», который также имеет реализацию в Ruby с использованием одноименного модуля Singleton
, но это уже другая история.
Итак, приступим. Предположим у нас есть вполне обычный Ruby-класс A
и объект этого класса a
.
class A
def foo
"bar"
end
end
a = A.new
a.foo # => "bar"
Как известно, для любого Ruby объекта можно определить методы, которые будет иметь только данный конкретный объект, но не будут иметь другие экземпляры того же класса. Такие методы называются «синглтон-методами» и для их определения нужно явно указать какому объекту они принадлежат, подставив объект перед именем метода. Покажем это на примере уже имеющегося у нас объекта a
.
# определение синглтон-метода для объекта `a`
def a.singleton_foo
"singleton_bar"
end
# синглтон-методы доступны только тем объектам, для которых они определены
a.singleton_foo # => "singleton_bar"
# и не доступны другим экземплярам того же класса
a2 = A.new
a2.singleton_foo # => NoMethodError: undefined method `singleton_foo' for #<A:0x0000000001a696f8>
Если вы заметили что-то общее в терминах «синглтон-метод» и «синглтон-класс» и это заставило вас задуматься, то на этот раз интуиция вас не подвела. Эти понятия действительно тесно связаны, а подробности вы найдете далее в статье.
Таким образом мы можем иметь объекты одного и того же класса, но с разным набором методов. Но ведь Ruby - это объектно-ориентированный язык программирования, и из теории ООП известно, что если у объекта есть метод, то он должен быть определен в его классе или иерархии наследования.
Проверим, что класс A
действительно ничего не знает о существовании определенного нами для его объекта a
синглтон-метода.
A.instance_methods.include?(:foo) # => true
A.instance_methods.include?(:singleton_foo) # => false
Неужели Ruby нарушает принципы ООП? Возможно где-то и нарушает, но точно не в этом месте. Синглтон-методы хранятся в специальном неявном классе, индивидуальном для каждого объекта в Ruby. Как вы уже догадались, это и есть синглтон-класс. В Ruby имеется метод singleton_class
, с помощью которого можно получить ссылку на синглтон-класс любого объекта.
# получаем синглтон-класс объкта `a`
singleton_class_of_a = a.singleton_class # => #<Class:#<A:0x0000000001ab6958>>
# находим потерянный метод
singleton_class_of_a.instance_methods.include?(:singleton_foo) # => true
Теперь вы знаете что такое синглтон-класс и как его получить. Но на этом история не заканчивается, всё самое интересное и сложное впереди. Давайте на данном этапе подведем некоторые промежуточные итоги.
Что нужно запомнить:
- В Ruby можно для любого объекта определить методы, которые будут доступны только этому объекту, а не всем экземплярам того же класса. Такие методы называются «синглтон-методами».
- Синглтон-методы объекта хранятся в специальном неявном классе этого объекта, который называется «сингтон-классом».
- Каждый объект имеет свой индивидуальный синглтон-класс, который можно получить с помощью метода
singleton_class
.
Классы - тоже объекты
Может быть кто-то до сих пор не обратил на это внимание, но фактически в Ruby абсолютно всё является объектами, даже сами классы. Рассмотрим к примеру класс String
. Мы можем пользоваться им как обычным объектом:
- присвоить его переменной;
- передать методу как аргумент;
- получить его уникальный идентификатор с помощью метода
object_id
; - и, самое главное, узнать экземпляром какого класса он является.
# переменная хранит ссылку на класс
string_class = String
# передача класса как аргумент метода
def class_name(klass)
klass.name
end
class_name(String) # => "String"
# или с использованием переменной, ссылающейся на класс
class_name(string_class) # => "String"
# идентификатор объекта-класса
String.object_id # => 5349120
# экземпляром какого класса является класс String
String.class # => Class
# повеселимся )
String.class.class.class.class.class.class # => Class
Class.class # => Class
В Ruby каждый класс является по сути объектом и сам по себе, как объект, является экземпляром класса Class
, который, в свою очередь, как объект-класс, тоже является экземпляром класса Class
.
Далее в статье для удобства я буду называть объекты, которые являются классами, «объектами-классами».
Зачем я вам всё это рассказываю и как это связано с синглтон-классами?
Если любой класс является объектом, то для него, по аналогии с обычными объектами, можно определять синглтон-методы. Я вам больше скажу, вы это уже много раз видели и делали сами, даже не осознавая этого. Рассмотрим следующий код:
class A
def self.foo2
"bar2"
end
end
При определении метода на этот раз использовался префикс self.
. Вы, конечно, уже знакомы с таким способом определения метода и знаете, что в данном случае метод становится не методом экземпляра, а методом класса.
A.foo2 # => "bar2"
A.new.foo2 # => NoMethodError: undefined method `foo2' for #<A:0x0000000000dde000>
Ключевое слово self
, используемое внутри тела класса является ссылкой на этот класс. Таким образом данный код определения метода класса эквивалентен следующему коду:
class A
def A.foo2
"bar2"
end
end
И в данном случае абсолютно ничего не изменится, даже если вынести определение метода из тела класса:
class A
end
def A.foo2
"bar2"
end
Таким образом мы определили метод с явным указанием его владельца - класса A
. Если вы вернетесь в самое начало статьи, то обнаружите, что мы уже проделывали что-то подобное, но тогда владельцем метода являлся не класс, а объект.
def a.singleton_foo
"singleton_bar"
end
Вы уже догадались к чему я веду? Методы класса являются ничем иным, как синглтон-методами этого класса, если рассматривать класс как объект. В этом можно убедиться с помощью метода singleton_methods
, позволяющего получить синглтон-методы объекта.
A.singleton_methods # => [:foo2]
Если класс может иметь синглтон-методы, значит у него должен быть синглтон-класс, где эти методы хранятся? Давайте проверим.
singleton_class_of_A = A.singleton_class
singleton_class_of_A.instance_methods.include?(:foo2) # => true
Да, так и есть.
Можно считать, что в Ruby нет никаких так называемых "методов класса", а у каждого класса, так как он является объектом, есть свои собственные синглтон-методы, которые определены в его личном синглтон-классе.
При определении синглтон-методов мы всегда явно указывали их владельцев. Но этого можно не делать, если определять методы в контексте самого синглтон-класса, так же, как методы экземпляра определяются в контексте своего класса. Получить контекст синглтон-класса объекта можно с помощью конструкции class << object
, где object
- это и есть тот объект, контекст синглтон-класса которого мы хотим получить.
a = A.new
# открываем контекст синглтон-класса объекта `a`
class << a
# в контексте синглтон-класса не нужно явно указывать объект-владельца метода
def foo1
"bar1"
end
def foo2
"bar2"
end
end
a.foo1 # => "bar1"
a.foo2 # => "bar2"
Вы уже наверняка встречались с такой конструкцией при определении методов класса.
class A
class << self
# определение методов класса
# ...
end
end
Ключевое слово self
в контексте класса указывает на сам класс и приведенный выше код эквивалентен следующему:
class << A
# определение методов класса
# ...
end
Подведем итоги второй части статьи.
Что нужно запомнить:
- Классы в Ruby можно (и нужно) рассматривать как обычные объекты, экземпляры класса
Class
. - Ruby-классы, как и любые другие объекты, могут иметь синглтон-методы и, очевидно, имеют синглтон-класс.
- Так называемые "методы класса", если рассматривать класс как объект, фактически являются его синглтон-методами и определяются тем же способом - с явным указанием владельца.
- Определение синглтон-методов возможно и без явного указания объекта-владельца, если это происходит в контексте синглтон-класса, доступ к которому осуществляется с использованием конструкции
class << object
.
Иерархия наследования и поиск определения метода
Из теории ООП нам известно, что при вызове метода у объекта, поиск определения данного метода осуществляется сначала в классе, экземпляром которого объект непосредственно является, и далее вверх по иерархии наследования. Так же обстоят дела и в Ruby, но ввиду того, что мы теперь знаем о существовании синглтон-класса, у нас может возникнуть вопрос о том, где его место и как он влияет на алгоритм поиска метода.
Поиск метода экземпляра
Проведем эксперимент, определим два метода с одинаковым именем, один как метод экземпляра, а другой как синглтон-метод. Посмотрим на результат, возвращаемый вызовом метода.
class A
# метод экземпляра
def foo
"bar from class instance method"
end
end
a = A.new
# синглтон-метод с тем же именем
def a.foo
"bar from singleton method"
end
a.foo # => "bar from singleton method"
Оказалось, что синглтон-метод переопределил метод экземпляра. Давайте проверим, можно ли при определении синглтон-метода вызвать переопределяемый метод экземпляра с использованием ключевого слова super
.
def a.foo
result_from_parent_method = super
result_from_parent_method + " and from singleton method"
end
a.foo # => "bar from class instance method and from singleton method"
Синглтон-методы могут переопределять методы экземпляра точно так же, как методы дочернего класса переопределяют методы родительского. Можно предположить, что синглтон-класс встраивается в иерархию наследования, и как бы становится дочерним классом класса своего объекта. В нашем случае синглтон-класс объекта a
становится подклассом его класса A
. Давайте проверим это.
a.singleton_class.superclass == A # => true
a.kind_of? a.singleton_class # => true
# Тем не менее:
a.instance_of?(a.singleton_class) # => false
a.instance_of?(A) # => true
Получается, так оно и есть: синглтон-класс встраивается в иерархию наследования и участвует в алгоритме поиска метода в качестве первоначального источника, хотя судя по методу instance_of?
таковым не является, но это и к лучшему.
Есть, кстати, один полезный метод - ancestors
. Он позволяет получить иерархию наследования, т.е. список всех классов, в которых будет осуществляться поиск определения метода. Проверим, подтвердится ли наше только что сделанное открытие, вызвав этот метод у синглтон-класса.
a.singleton_class.ancestors # => [#<Class:#<A:0x00000000014598f8>>, A, Object, Kernel, BasicObject]
В полученном массиве на первом месте стоит синглтон-класс, у которого вызывался метод. Далее за ним следует класс A
и все его родительские классы. Это подтверждает то, что синглтон-класс объекта a
находится в иерархии наследования класса A
и при этом стоит перед ним, на первом месте, являясь его подклассом.
Таким образом, синглтон-класс любого объекта является подклассом класса этого объекта, и объект, хоть и неявно, как бы является его экземпляром, и в нем в первую очередь происходит поиск определения методов.
Поиск метода класса
Я бы не стал рассматривать отдельно поиск методов класса (ведь это синглтон-методы, роль которых в алгоритме поиска метода мы уже рассмотрели в контексте обычных объектов), если бы не один нюанс. Дело в том, что классы, помимо того, что ведут себя как обычные объекты, могут быть связаны механизмом наследования. Рассмотрим два класса, один из которых является подклассом другого.
class A; end
class B < A; end
У родительского класса определим метод класса (или синглтон-метод).
def A.foo
"bar from A singleton method"
end
A.foo # => "bar from A's singleton method"
Мы определили синглтон-метод у класса A
, который является самостоятельным объектом, отличным от B
, хоть они и связаны, как классы, механизмом наследования. Более того, метод был объявлен и определен после определения самих классов. Можно предположить, что класс B
не должен ничего знать о синглтон-методе, определенном для класса A
. Но мы знаем, что в Ruby методы класса, также как и методы экземпляра, наследуются.
B.foo # => "bar from A's singleton method"
Как это объяснить с точки зрения теории синглтон-класса и синглтон-методов?
Давайте попытаемся спроецировать наши знания о том, каким образом синглтон-класс встраивается в алгоритм поиска метода в случае обычных объектов, на классы. При вызове метода foo
у класса B
сначала происходит поиск метода среди его синглтон-методов. Но мы определяли синглтон-метод у класса A
, поэтому в синглтон-классе класса B
его по идее быть не должно. Затем происходит попытка поиска метода в классе класса B
, коим является Class
. Ну там то точно нашего сингтон-метода не может быть. Где же он тогда?
Нам не придётся долго мучиться в поисках ответа, нам поможет метод ancestors
. Получим иерархию наследования синглтон-класса класса B
.
B.singleton_class.ancestors # => [#<Class:B>, #<Class:A>, #<Class:Object>,
#<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
На первом месте стоит синглтон-класс класса B
(#<Class:B>). Обратите внимание на классы, идущие далее по цепочке. Это синглтон-класс класса A
(#<Class:A>) и синглтон-классы его родительских классов Object
(#<Class:Object>) и BasicObject
(#<Class:BasicObject>).
Оказывается, когда в Ruby наследуются классы, их синглтон-классы также наследуются. Когда класс B
унаследовал класс A
, синглтон-класс класса B
также унаследовал синглтон-класс класса A
, а последний, в свою очередь, унаследовал синглтон-класс родительского класса Object
, являющегося общим родительским классом всех классов Ruby, и так далее.
Теперь понятно как объявленный у класса A
синглтон-метод нашелся у класса B
. Сначала был произведен поиск метода в синглтон-классе класса B
и, в соответствии с принципом наследования, во всех его родительских классах, среди которых был синглтон-класс класса A
, где и находился определенный нами синглтон-метод.
Сформулируем выводы по 3 части статьи.
Что нужно запомнить:
- Синглтон-класс любого объекта фактически является подклассом класса этого объекта и может переопределять его методы.
- Синглтон-класс замыкает иерархию наследования, в нем в первую очередь происходит поиск определения метода (если конечно в сам синглтон-класс ничего не подмешали).
- При наследовании классов их синглтон-классы также наследуют друг друга, таким образом дочерние классы наследуют синглтон-методы своих родительских классов.
Применение знаний о синглтон-классе
В заключение приведу пару примеров того, как можно применить знания о синглтон-классе на практике. Мы можем динамически изменять поведение отдельных объектов, расширяя или модифицируя их синглтон-класс таким же образом, как это делается с обычными классами, например с помощью подмешивания модуля. Модификация синглтон-класса даёт возможность изменять поведение конкретного объекта, не оказывая при этом влияния на все другие объекты того же класса.
Предположим, что у нас есть следующие класс и модуль.
class Product
attr_reader :price
def initialize(price)
@price = price.to_f
end
end
module ProductWithDiscount
DISCOUNT_PERCENTAGE = 10
def self.included(base)
base.class_eval do
alias original_price price
def price
original_price - discount
end
end
end
def discount
return 0 if original_price == 0
original_price * DISCOUNT_PERCENTAGE / 100
end
end
Мы можем включить модуль в класс, тем самым изменив поведение всех экземпляров этого класса.
# до включения модуля
Product.new(1000).price # => 1000.0
Product.include ProductWithDiscount
# после включения модуля
Product.new(1000).price # => 900.0
Также можно использовать тот же самый модуль для изменения не всех объектов этого класса, а конкретного, включив модуль в синглтон-класс этого объекта.
product = Product.new(1000)
product.price # => 1000.0
# изменение конкретного объекта
product.singleton_class.include ProductWithDiscount
product.price # => 900.0
# другие экземпляры класса не изменены
Product.new(1000).price # => 1000.0
Следующий пример демонстрирует модификацию экземпляра через его синглтон-класс с помощью Proc
-объекта и метода class_exec
.
class Product
attr_reader :price
def initialize(price)
@price = price.to_f
end
end
extension =
proc do |percentage|
alias original_price price
def price
original_price - discount
end
define_method(:discount) do
return 0 if original_price == 0
original_price * percentage / 100
end
end
product1 = Product.new(1000)
product2 = Product.new(1000)
product1.price # => 1000.0
product2.price # => 1000.0
product1.singleton_class.class_exec(10, &extension)
product2.singleton_class.class_exec(50, &extension)
product1.price # => 900.0
product2.price # => 500.0
Благодарю за внимание! Надеюсь данная статья была для вас полезной. Если у вас есть какие-либо вопросы или комментарии, вы можете оставить их ниже, я буду рад на них ответить.
Комментарии (0)