Private module methods in Ruby

2019-01-12 16:57发布

问题:

I have a two part question

Best-Practice

  • I have an algorithm that performs some operation on a data structure using the public interface
  • It is currently a module with numerous static methods, all private except for the one public interface method.
  • There is one instance variable that needs to be shared among all the methods.

These are the options I can see, which is the best?:

  • Module with static ('module' in ruby) methods
  • Class with static methods
  • Mixin module for inclusion into the data structure
  • Refactor out the part of the algorithm that modifies that data structure (very small) and make that a mixin that calls the static methods of the algorithm module

Technical part

Is there any way to make a private Module method?

module Thing
  def self.pub; puts "Public method"; end
  private
  def self.priv; puts "Private method"; end
end

The private in there doesn't seem to have any effect, I can still call Thing.priv without issue.

回答1:

I think the best way (and mostly how existing libs are written) do this by making a class within the module that deals with all the logic, and the module just provides a convenient method, e.g.

module GTranslate
  class Translator
    def perform( text ); 'hola munda'; end
  end

  def self.translate( text )
    t = Translator.new
    t.perform( text )
  end
end


回答2:

There's also Module.private_class_method, which arguably expresses more intent.

module Foo
  def self.included(base)
    base.instance_eval do
      def method_name
        # ...
      end
      private_class_method :method_name
    end
  end
end

For the code in the question:

module Thing
  def self.pub; puts "Public method"; end
  def self.priv; puts "Private method"; end
  private_class_method :priv
end

Ruby 2.1 or newer:

module Thing
  def self.pub; puts "Public method"; end
  private_class_method def self.priv; puts "Private method"; end
end


回答3:

You can use the "included" method to do fancy things when a module is mixed in. This does about what you want I think:

module Foo
  def self.included(base)
    class << base 
      def public_method
        puts "public method"
      end
      def call_private
        private_method
      end
      private
      def private_method
        puts "private"
      end
    end
  end
end

class Bar
  include Foo
end

Bar.public_method

begin
  Bar.private_method
rescue
  puts "couldn't call private method"
end

Bar.call_private


回答4:

module Writer
  class << self
    def output(s)
      puts upcase(s)
    end

    private

    def upcase(s)
      s.upcase
    end
  end
end

Writer.output "Hello World"
# -> HELLO WORLD

Writer.upcase "Hello World"
# -> so.rb:16:in `<main>': private method `upcase' called for Writer:Module (NoMethodError)


回答5:

Unfortunately, private only applies to instance methods. The general way to get private "static" methods in a class is to do something like:

class << self
  private

  def foo()
   ....
  end
end

Admittedly I haven't played with doing this in modules.



回答6:

A nice way is like this

module MyModule
  class << self
    def public_method
      # you may call the private method here
      tmp = private_method
      :public
    end

    private def private_method
      :private
    end
  end
end

# calling from outside the module
puts MyModule::public_method


回答7:

The best pattern that I've found by doing this in Rails is to give up on modules that want to have private methods and use a Singleton class instead. It doesn't feel right but it does work and seems cleaner that then other examples I've seen in this question.

Would love to hear other opinions on this.

Example:

ErrorService.notify("Something bad happened")

class ErrorService
  include Singleton

  class << self
    delegate :notify, to: :instance
  end

  def notify(message, severity: :error)
    send_exception_notification(message)
    log_message(message, severity)
  end

  private

  def send_exception_notification(message)
    # ...
  end

  def log_message(message, severity)
    # ...
  end
end


回答8:

Make a private module or class

Constants are never private. However, it's possible to create a module or class without assigning it to a constant.

So an alternative to :private_class_method is to create a private module or class and define public methods on it.

module PublicModule
  def self.do_stuff(input)
    @private_implementation.do_stuff(input)
  end

  @private_implementation = Module.new do
    def self.do_stuff(input)
      input.upcase # or call other methods on module
    end
  end
end

Usage:

PublicModule.do_stuff("whatever") # => "WHATEVER"

See the docs for Module.new and Class.new.



回答9:

What's about storing methods as lambdas within class variables/constants?

module MyModule
  @@my_secret_method = lambda {
    # ...
  }
  # ...
end

For test:

module A
  @@C = lambda{ puts "C" }
  def self.B ; puts "B"; @@C[] ; end
  private     # yeah, this has no sense, just for experiment
  def self.D ; puts "D"; @@C[] ; end
end

for expr in %w{ A::B A.B A::C A.C A::D A.D }
  eval expr rescue puts expr
end

Here we see that C can be successfully used by B and D, but not from outside.