4

I wrote the following code:

class Actions
  def initialize
    @people = []
    @commands = {
      "ADD" => ->(name){@people << name },
      "REMOVE" => ->(n=0){ puts "Goodbye" },
      "OTHER" => ->(n=0){puts "Do Nothing" }
    }
  end
  def run_command(cmd,*param)
    @commands[cmd].call param if @commands.key?(cmd)
  end
  def people
    @people
  end
end
act = Actions.new

act.run_command('ADD','joe')
act.run_command('ADD','jack')
puts act.people

This works, however, when the @commands hash is a class variable, the code inside the hash doesn't know the @people array.

How can I make the @commands hash be a class variable and still be able to access the specific object instance variables?

Andrew Grimm
  • 78,473
  • 57
  • 200
  • 338
Srgrn
  • 1,770
  • 16
  • 30
  • 1
    Just curious why not to define methods `add`, `remove` and `other` as instance methods and use `respond_to?` and `send` to call them? – Victor Moroz Jan 03 '12 at 21:42
  • Why do you want `@commands` to be a class variable? – Andrew Grimm Jan 03 '12 at 22:00
  • @Victor: One good reason is that it makes access control easier. If you use `send` and methods you'd need a separate list of which methods `run_command` is allowed to use, using a Hash collects the available commands and their implementations into one nice neat package. – mu is too short Jan 03 '12 at 22:09
  • @mu can't you just ask `respond_to?(cmd)`?, that is, why a separate list of allowed commands? – Marek Příhoda Jan 03 '12 at 22:20
  • @mu `send(cmd) if @allowed_methods.include?(cmd)`, it's still cleaner than using hash of lambdas if you need to access instance variables. – Victor Moroz Jan 03 '12 at 22:29
  • @maprihoda: What if you don't want someone to say `run_command('people')`? You could prefix all the run_command-able things with, say `cmd_` and then add the prefix before checking `respond_to?` to get a similar effect. – mu is too short Jan 03 '12 at 22:31
  • @Victor: That's called an opinion and we all have them :) I've used both approaches (and even the `cmd_` prefix style) and which I use depends on which feels more natural for a given circumstance. If you're looking for the One True and Righteous way to do *X* then you're working with the wrong language. – mu is too short Jan 03 '12 at 22:36

2 Answers2

6

You could use instance_exec to supply the appropriate context for the lambdas when you call them, look for the comments to see the changes:

class Actions
  # Move the lambdas to a class variable, a COMMANDS constant
  # would work just as well and might be more appropriate.
  @@commands = {
    "ADD"    => ->(name)  { @people << name   },
    "REMOVE" => ->(n = 0) { puts "Goodbye"    },
    "OTHER"  => ->(n = 0) { puts "Do Nothing" }
  }
  def initialize
    @people = [ ]
  end
  def run_command(cmd, *param)
    # Use instance_exec and blockify the lambdas with '&'
    # to call them in the context of 'self'. Change the
    # @@commands to COMMANDS if you prefer to use a constant
    # for this stuff.
    instance_exec(param, &@@commands[cmd]) if @@commands.key?(cmd)
  end
  def people
    @people
  end
end
mu is too short
  • 426,620
  • 70
  • 833
  • 800
1

EDIT Following @VictorMoroz's and @mu's recommendations:

class Actions
  def initialize
    @people = []
  end

  def cmd_add(name)
    @people << name
  end

  def cmd_remove
    puts "Goodbye"
  end

  def cmd_other
    puts "Do Nothing"
  end

  def people
    p @people
  end

  def run_command(cmd, *param)
    cmd = 'cmd_' + cmd.to_s.downcase
    send(cmd, *param) if respond_to?(cmd)
  end
end

act = Actions.new

act.run_command('add', 'joe')
act.run_command(:ADD, 'jill')
act.run_command('ADD', 'jack')

act.run_command('people') # does nothing

act.people

Or

class Actions
  ALLOWED_METHODS = %w( add remove other )

  def initialize
    @people = []
  end

  def add(name)
    @people << name
  end

  def remove
    puts "Goodbye"
  end

  def other
    puts "Do Nothing"
  end

  def people
    p @people
  end

  def run_command(cmd, *param)
    cmd = cmd.to_s.downcase
    send(cmd, *param) if ALLOWED_METHODS.include?(cmd)
  end
end

act = Actions.new

act.run_command('add', 'joe')
act.run_command(:add, 'jill')
act.run_command('add', 'jack')

act.run_command('people') # does nothing

act.people
Marek Příhoda
  • 11,108
  • 3
  • 39
  • 53