views:

48

answers:

2

Hi,

I would like to allow a person object (instanced from a Person class) to speak a language (which is a collection of public methods stored in Language module):

class Person
  attr_accessor :current_language

  def quit
    # Unselect the current language, if any:
    @current_language = nil
  end
end

Suppose that languages are the following:

module Language
  module Japanese
    def konnichiwa
      "こんにちは! (from #{@current_language} instance variable)"
    end

    def sayounara
      "さようなら。"
    end
  end

  module French
    def bonjour
      "Bonjour ! (from #{@current_language} instance variable)"
    end

    def au_revoir
      "Au revoir."
    end
  end

  module English
    def hello
      "Hello! (from #{@current_language} instance variable)"
    end

    def bye
      "Bye."
    end
  end
end

Example of use:

person = Person.new

person.current_language # => nil
person.hello            # => may raise a nice no method error

person.current_language = :english
person.hello    # => "Hello! (from english instance variable)"
person.bonjour  # => may also raise a no method error
person.quit

person.current_language = :french
person.bonjour  # => "Bonjour ! (from french instance variable)"

As you can see, a language is such as a protocol. So a person can switch on a specific protocol, but only one at a time.

For modular reasons, storing each language into a module is friendly. So I think this way is the more logical Ruby way, isn't it.

But, I believe that it is not possible to write something like this:

class Person
  include "Language::#{@current_language}" unless @current_language.nil?
end

According to you, what should be the best practice to do so?

Any comments and messages are welcome. Thank you.

Regards

A: 

You can do this pretty elegantly in Ruby if you arrange your modules correctly.

module LanguageProxy
  def method_missing(phrase)
    if @current_language.respond_to?(phrase)
      @current_language.send(phrase)
    else
      super
    end
  end
end

module Language
  module French
    def self.bonjour
      "Bonjour ! (from #{@current_language} instance variable)"
    end

    def self.au_revoir
      "Au revoir."
    end
  end

  module English
    def self.hello
      "Hello! (from #{@current_language} instance variable)"
    end

    def self.bye
      "Bye."
    end
  end
end

class Person
  attr_accessor :current_language

  include LanguageProxy

  def quit
    @current_language = nil
  end
end

person = Person.new

person.current_language # => nil
begin
  p person.hello            # => may raise a nice no method error
rescue 
  puts "Don't know hello"
end

person.current_language = Language::English
p person.hello    # => "Hello! (from english instance variable)"
begin
  p person.bonjour  # => may also raise a no method error
rescue
  puts "Don't know bonjour"
end
person.quit

person.current_language = Language::French
p person.bonjour  # => "Bonjour ! (from french instance variable)"

Essentially, all we are doing here is creating a Proxy class to forward Person's unknown messages to the language module stored in the Person's @current_language instance variable. The "trick" I used here is to make hello, bye, etc. module methods, not instance methods. Then, I assigned the actual module into @current_language.

You'll also notice here that the @current_language instance variable from Person is not available in the Language modules. It gets a little more tricky if you need the language methods to access those variables: the quick fix would probably be to just pass them as parameters.

If you really want to use symbols to denote the language, you'll have to do a little magic with Language.const_get.

Output:

C:\temp\ruby>ruby person.rb
Don't know hello
"Hello! (from  instance variable)"
Don't know bonjour
"Bonjour ! (from  instance variable)"
Mark Rushakoff
Thank you Mark for this tip. And that work well! However, I would like that module's methods are merged and unmerged dynamically (with a kind of dynamic include) into the object... and then (for instance) access them with person.public_send(). Thus, the instance variables are accessible from the new methods.
Zag zag..
A: 

Just to show that you could do this (not that you should):

include "A::B::C".split("::").inject{|p,c| (p.class == String ? Kernel.const_get(p) : p).const_get(c)}
jdeseno
Thanks for your answer. But I don't really understand your code.
Zag zag..