13 October 2017

А как вы создаете синглтоны в Ruby?

RubyRuby on Rails

Как создать синглтон в Ruby?
Лично мне приходят на ум 4 способа.


image


Способ первый: 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'
Tags:rubyrailssingletonсинглтон
Hubs: Ruby Ruby on Rails
+3
5.4k 7
Comments 11
Top of the last 24 hours