3

I've made a constructor like this:

class Foo
  def initialize(p1, p2, opts={})
    #...Initialize p1 and p2
    opts.each do |k, v|
      instance_variable_set("@#{k}", v)
    end
  end
end

I'm wondering if it's a good practice to dynamically set instance variables like this or if I should better set them manually one by one as in most of the libs, and why.

2
  • 1
    What are you going to do with all these instance variables later on? Commented Feb 15, 2016 at 15:29
  • Basically they'll impact my objects behavior depending if they are defined and what their values are. I don't know if it really answers to your question :) Commented Feb 15, 2016 at 15:32

4 Answers 4

4

Diagnosing the problem

What you're doing here is a fairly simple example of metaprogramming, i.e. dynamically generating code based on some input. Metaprogramming often reduces the amount of code you need to write, but makes the code harder to understand.

In this particular case, it also introduces some coupling concerns: the public interface of the class is directly related to the internal state in a way that makes it hard to change one without changing the other.

Refactoring the example

Consider a slightly longer example, where we make use of one of the instance variables:

class Foo
  def initialize(opts={})
    opts.each do |k, v|
      instance_variable_set("@#{k}", v)
    end
  end

  def greet(name)
    greeting = @greeting || "Hello"
    puts "#{greeting}, name"
  end
end

Foo.new(greeting: "Hi").greet

In this case, if someone wanted to rename the @greeting instance variable to something else, they'd possibly have a hard time understanding how to do that. It's clear that @greeting is used by the greet method, but searching the code for @greeting wouldn't help them find where it was first set. Even worse, to change this bit of internal state they'd also have to change any calls to Foo.new, because the approach we've taken ties the internal state to the public interface.

Remove the metaprogramming

Let's look at an alternative, where we just store all of the opts and treat them as state:

class Foo
  def initialize(opts={})
    @opts = opts
  end

  def greet(name)
    greeting = @opts.fetch(:greeting, "Hello")
    puts "#{greeting}, name"
  end
end

Foo.new(greeting: "Hi").greet

By removing the metaprogramming, this clarifies the situation slightly. A new team member who's looking to change this code for the first time is going to have a slightly easier time of things, because they can use editor features (like find-and-replace) to rename the internal ivars, and the relationship between the arguments passed to the initialiser and the internal state is a bit more explicit.

Reduce the coupling

We can go even further, and decouple the internals from the interface:

class Foo
  def initialize(opts={})
    @greeting = opts.fetch(:greeting, "Hello")
  end

  def greet(name)
    puts "#{@greeting}, name"
  end
end

Foo.new(greeting: "Hi").greet

In my opinion, this is the best implementation we've looked at:

  1. There's no metaprogramming, which means we can find explicit references to variables being set and used, e.g. with an editor's search features, grep, git log -S, etc.
  2. We can change the internals of the class without changing the interface, and vice-versa.
  3. By calling opts.fetch in the initialiser, we're making it clear to future readers of our class what the opts argument should look like, without making them read the whole class.

When to use metaprogramming

Metaprogramming can sometimes be useful, but those situations are rare. As a rough guide, I'd be more likely to use metaprogramming in framework or library code which typically needs to be more generic (e.g. the ActiveModel::AttributeAssignment module in Rails), and to avoid it in application code, which is typically more specific to a particular problem or domain.

Even in library code, I'd prefer the clarity of a few lines of repetition.

Sign up to request clarification or add additional context in comments.

1 Comment

Best answer so far as it helped me a lot to have a clear picture in my mind. I don' think there's much to say over this, so I set it as the accepted answer
2

Answers to this question are always going to be based on someone's personal opinion so here's mine.

Clarity v Brevity

If you cannot know the set of options ahead of time then you have no real choice but to do as you have. However if the options are drawn from a known set then I would favour clarity over brevity and have explicit methods to set the options. These would also be a good place to add any rdoc etc.

Safety

From a safety perspective, having methods to handle the setting of an option would allow you to perform validation as required.

1 Comment

Makes sense and I now tend to set options explicitly. I will just wait for other points of views before setting this as resolved. Thanks
1

When you need to do such thing, the inventory of the parameters varies. In such case, there is already a handy structure within Ruby (as well as most modern languages): array and hash. In this case, you should just save the entire option as a single hash. That would make things simpler.

5 Comments

You mean saving my options hash as an instance variable ? That doesn't look simpler to me, I think I'll end up with a non-readable verbose code like @opts[:my_opt] instead of @my_opt ?
What's wrong with @opts[:my_opt]? If that seems too long, you can just make the name a single letter: @h[:my_opt].
@Logar it's more like @opts[:my_opt] vs. instance_variable_get(:my_opt), i.e. you can't use @my_opt because it is not known in advance, is it?
It is known in advance and in that way I tend to agree with @Chris answer and explicitly set them. And yes, if I can't know the set of options I agree that saving the entire hash is an acceptable solution, but if I do it's still bugging me to call my hash instead of defining a variable at initialization. I don't see the point of calling a hash when i can call a variable with a few lines in an initializer. Considering Sawa's experience vs mine I must be wrong, but I can't get why
@Logar It depends on the particular use case. Since I do not know the whole picture of your code, probably you can tell better.
1

Instead of creating instance variables dynamically, you could use attr_accessor to declare the available instance variables and just call the setters dynamically:

class Foo
  attr_accessor :bar, :baz, :qux

  def initialize(opts = {})
    opts.each do |k, v|
      public_send("#{k}=", v)
    end
  end
end

Foo.new(bar: 1, baz: 2) #=> #<Foo:0x007fa8250a31e0 @bar=1, @baz=2>
Foo.new(qux: 3)         #=> #<Foo:0x007facbc06ed50 @qux=3>

This approach also shows an error if an unknown option is passed:

Foo.new(quux: 4) #=> undefined method `quux=' for #<Foo:0x007fd71483aa20> (NoMethodError)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.