Как создать синглтон в Ruby?
Лично мне приходят на ум 4 способа.
Способ первый: include Singleton
В стандартной библиотеке определен модуль Singleton
, который производит
некоторые действия над классом, в частности:
- Делает
.new
приватным - Добавляет
.instance
, который создает и/или возвращает экземпляр - Переопределяет
#dup
и#clone
, чтобы те вызывали ошибку
Полагаю, это классическая реализация синглтона. Во всяком случае, я бы писал
что-то подобное, будь я Java-программистом. Ничего особенного, все работает.
Способ второй: модуль с module_function
У Module
есть метод #module_function
, который позволяет использовать
определяемые методы "на себе". Такой подход используется, например, в
Math
. Пример:
module M
module_function
def f
:f
end
end
M.f # ==> :f
Я бы не рекомендовал такую реализацию синглтона по нескольким причинам:
- Это все же остается миксином, который можно включить в другой
класс/модуль. Это, конечно, не страшно, но что-то здесь не так. Сделать приватные методы можно только с помощью костылей, т.к. module_function
создает публичную копию метода в самом себе. Я с ходу придумал только такой:
module M module_function def f g end def g 'hello' end singleton_class.send(:private, :g) end M.f # ==> 'hello' M.g # ==> NoMethodError
- (Личная причина) Модули, использующие
module_function
по моему мнению должны
быть сборником stateless методов, помогающих в чем-либо. Соответственно,
include MyModule
будет использоваться только для того, чтобы сделать методы
доступными в текущем модуле без обращения кMyModule
. Такой сценарий
использования приводится в "The Ruby Programming Language" сMath
Кстати, можно для этих же целей использовать extend self
вместо
module_function
. Это избавит от проблемы приватных методов. Но, допустим,
небезызвестный ruby-style-guide не одобряет такой подход (ссылка:
https://github.com/bbatsov/ruby-style-guide#module-function)
Думаю, очевидно, что extend self
работает иначе, но я не уверен, что есть
какая-то опасная разница.
upd. все-таки не очень очевидно. extend self
заставляет модуль добавить самого себя в список подключенных модулей (не знаю, как это проще написать), а module_function
создает копии методов. Если конкретно, то посмотрите на код:
module M
module_function
def f
:f
end
end
class A
include M
end
M.f # ==> :f
# module_function делает методы приватными в include
A.new.send(:f) # ==> :f
module M
def f
:TROLOLO
end
end
A.new.send(:f) # ==> :TROLOLO
M.f # ==> :f
Способ третий: класс/модуль только с методами класса
class MyClass
def self.f
:f
end
def self.g
:g
end
end
MyClass.f # ==> :f
или так:
class MyClass
class << self
def f
:f
end
private
def g
:g
end
end
end
MyClass.f # ==> :f
MyClass.g # ==> NoMethodError
Разумеется, вместо class
можно использовать module
. В вышеупомянутом
стайл-гайде такой подход не рекомендуется. Вместо него рекомендуют
module_function
.
В моей практике такой подход встречался чаще всего. Лично мне он всегда казался
каким-то страшным костылем, но при этом он мне нравится больше использования
Singleton
, т.к. MySingleton.do_something
для меня выглядит привлекательнее
MySingleton.instance.do_something
.
Создать экземпляр Object
В последнее время я постоянно использую такой подход:
MySingleton = Object.new
class << MySingleton
def f
g
end
private
def g
puts 'hello'
end
end
Теперь наш синглтон — это просто экземпляр Object
с нужными нам методами:
MySingleton.class # ==> Object
Вот только и здесь есть проблемы:
- Мы можем использовать
#clone
/#dup
. Решение: переопределить их, как это
сделано вSingleton
- При инспектировании объекта мы получаем что-то вроде
#<Object: ...>
. Решение: переопределить методы#to_s
и#inspect
. Кстати,
ruby-style-guide рекомендует делать это на всех "собственных" (локальных? Не
могу подобрать слово) классах. Ссылка:
https://github.com/bbatsov/ruby-style-guide#define-to-s - пишут, что у такого подхода есть проблемы с генерацией документации. Не
могу подтвердить или опровергнуть, т.к. не использую генераторы. Ссылка:
https://practicingruby.com/articles/ruby-and-the-singleton-pattern-dont-get-along
Небольшое отступление: class << self
Думаю, все видели синтаксис:
class MyClass
class << self
def do_something_on_class
...
end
end
def do_something_on_instance
...
end
end
При этом я неоднократно замечал, что человек не знает, что означает эта
конструкция. Собственно, в Ruby у объектов на самом деле есть два класса: тот,
чьим экземпляром он является, и т.н. "singleton class" — класс синглтона.
Наверняка вы видели примеры, где мы определяем методы прямо на объектах.
Как-то так:
x = Object.new
def x.hey
'hey'
end
x.hey # ==> 'hey'
В класс-ориентированном ООП у объекта нет своих методов. Поведение объекта
определяется классом, которому он принадлежит. Поэтому мы не можем определить
метод на объекте с помощью def x.hey
, мы должны определить его в классе. Вот
только если мы сделаем это, то тогда все экземпляры Object
должны будут
получить метод #hey
, чего мы не хотим. Поэтому Ruby создает "дополнительный"
класс у объекта, называемый класс синглтона. Получить его можно с помощью метода
#singleton_class
. В общем, я увлекся и, наверное, только запутал тех, кто не
знал о "singleton class". Это очень интересная сторона Ruby, поэтому предлагаю
прочитать о ней самостоятельно.
Собственно, если коротко, то конструкция class << some_object
"входит" в класс
сингтона. Сравните:
class A # enter class A scope
def hey
'hey'
end
end
class << A # enter class A singleton class scope
def hey
'hello'
end
end
A.new.hey # ==> 'hey'
A.hey # ==> 'hello'
Комментарии (11)
immaculate
13.10.2017 21:33Есть паттерны, которые не имеют смысла в динамических языках. Singleton — один из них.
crmMaster
14.10.2017 01:06Ты даешь им объект класса, а они делают include Singleton.
Что дальше? аннотации? или… может… СТАТИЧЕСКАЯ ТИПИЗАЦИЯ?
Кошмар рубиста наяву.
Спасибо, поплакал :)Nondv Автор
14.10.2017 01:15Тоже крайне удивлен результатами опроса. Может быть, это джависты портят статистику? Хммм....
immaculate
14.10.2017 09:52Понимаете, паттерны — это не серебряная пуля. Не что-то вечное и неизменное, написанное на скрижалях и подходящее всем языкам на веки вечные. Паттерны — это довольно старая книга, написанная, к тому же, для языков со статической типизацией.
Пытаться дословно реализовать все паттерны из книги во всех языках — это бессмысленный карго-культ. Я вам выше написал, что Singleton не имеет никакого практического и теоретического смысла в Ruby. Все это многократно уже обсуждалось, на hacker news, например, можно найти статьи об этом.
Nondv Автор
14.10.2017 12:20Вы забываете, что синглтон в Руби — это еще и способ красиво оформить код. Я, например, пока и сам не попадал в ситуации, где использование синглтона было бы продиктовано архитектурными соображениями.
Мне вот, допустим, не нравится видеть
new
каждый раз, когда я хочу воспользоваться чем-то вродеSomeExternalApi
:
SomeApi.new.do_something # vs SomeApi.do_something
Синглтон — это не паттерн N из книги банды четырех. Это просто идея (впрочем, как и большинство паттернов). Далеко не всегда они решают какую-то архитектурную задачу, зачастую они просто позволяют сделать код красивее (понятнее).
Предвижу минусы, но я настаиваю на этой точке зрения.
immaculate
14.10.2017 13:09К сожалению, плохо знаком с Ruby. В Python это делается при помощи статических методов или методов класса. Или при помощи паттерна Borg. Думаю, что и в Ruby должно быть что-то похожее.
А вообще, мне кажется что тащить в другой язык инородные приемы и парадигмы — напрасная трата сил. Я как раз сейчас работаю с относительно большим проектом, который написал человек как первый на Python после Java. Это просто нечитаемый ужас-ужас-ужас. В Риме надо поступать как римляне. Если хочется писать на Java, то не надо превращать в Java Python или Ruby. Можно просто писать на Java.
Nondv Автор
14.10.2017 13:20статических методов или методов класса
Этот способ указан в посте. С парой причин, почему он лично мне не нравится.
тащить в другой язык инородные приемы и парадигмы — напрасная трата сил
В руби шикарно уживаются приемы из ФП. Более того, о каких приемах в контексте данного поста идет речь?
Синглтон — всего лишь название, чтобы всем было понятно, что речь идет о единственном представителе своего класса.
Например, как я указал в посте, в приеме, который мне нравится больше всего, есть проблема, связанная с тем, что другие инстансы создать можно. Но на самом деле это не проблема, т.к. даже если инстансы появятся, это никакого вреда не принесет, т.к. синглтон в руби во многих (если не во всех) случаях нужен просто для красоты.
immaculate
14.10.2017 13:27Singleton — это просто глобальная переменная. Я вот ни разу в жизни еще не сталкивался с ситуацией, где он реально требуется. Мне кажется, что это скорее антипаттерн, а то, с какими целями вы его пытаетесь пересоздать — двойной антипаттерн.
Nondv Автор
14.10.2017 13:34Я вам привел две строчки для сравнения:)
Это просто делает код красивее.
Что является антипаттерном? Приемы, которые нанесут вред коду? Какой вред вы видете в глобальной константе, содержащей stateless-объект с нужными методами?
Ну и еще можно, конечно, говорить о том, что это избавляет от множеств созданий/уничтожений объекта и т.д. и т.п., но это роли не играет, разумеется.
P.S. мне нравится этот диалог, но, по-моему, мы совсем уж ушли от изначальной темы про способы создания синглтонов в Ruby (на котором Вы не пишете).
UPD. Вы упомянули статические методы. Класс в данном случае ведь тоже объект. Просто в питоне и джаве это не подтверждается на уровне языка. Я могу ошибаться, но в ООП нет "статических методов". Есть только объекты и их методы. Соответственно, говоря о статических методах, мы начинаем интерпретировать класс как самостоятельный объект (в Ruby так оно и есть)
Nondv Автор
14.10.2017 13:41Уточнил. Я не прав насчет питона. В питоне классы — тоже объекты, что просто замечательно
Nicklasos
Будь Вы Java-программистом, то использовали бы shared instance в DI контейнере, а не сомнительные паттерны.