Private module methods in Ruby

Viewed 95516

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.

10 Answers

I think the best way (and mostly how existing libs are written) to do this is by creating 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)
      translate(text)
    end

    private

    def translate(text)
      # do some private stuff here
    end
  end

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

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

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

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.

This method won't allow sharing data with the private methods unless you explicitly pass the data by method parameters.

module Thing
  extend self

  def pub
    puts priv(123)
  end

  private
  
  def priv(value)
    puts "Private method with value #{value}"
  end
end

Thing.pub
# "Private method with value 123"

Thing.priv
# NoMethodError (private method `priv' called for Thing:Module)

Here's a solution for how you can have multiple classes nested within a single module, with the ability to call a private method on the module that's accessible from any of the nested classes, by making use of extend:

module SomeModule

  class ClassThatDoesNotExtendTheModule
    class << self
      def random_class_method
        private_class_on_module
      end
    end
  end

  class ClassThatDoesExtendTheModule
    extend SomeModule
  
    class << self
      def random_class_method
        private_class_on_module
      end
    end
  end

  class AnotherClassThatDoesExtendTheModule
    extend SomeModule
  
    class << self
      def random_class_method
        private_class_on_module
      end
    end
  end

  private

  def private_class_on_module
    puts 'some private class was called'
  end
  
end

Some output to show the solution in action:

> SomeModule::ClassThatDoesNotExtendTheModule.random_class_method

NameError: undefined local variable or method `private_class_on_module' for SomeModule::ClassThatDoesNotExtendTheModule:Class


> SomeModule::ClassThatDoesExtendTheModule.random_class_method

some private class was called


> SomeModule::ClassThatDoesExtendTheModule.private_class_on_module

NoMethodError: private method `private_class_on_module' called for SomeModule::ClassThatDoesExtendTheModule:Class


> SomeModule::AnotherClassThatDoesExtendTheModule.random_class_method

some private class was called


> SomeModule::AnotherClassThatDoesExtendTheModule.random_class_method

NoMethodError: private method `private_class_on_module' called for SomeModule::AnotherClassThatDoesExtendTheModule:Class
Related