-
Notifications
You must be signed in to change notification settings - Fork 0
MR10 Metaprogramming
In this unit, we'll do the following:
- Avoid hardcoding mutant attribute names when possible
We'll learn about the following concepts or tools:
- Constants
- Class methods
- Symbols
String#gsub
- Attribute writer methods
- Metaprogramming
- Splat operator
Object#send
collect_mutant_data
still has a lot of repetition. For each attribute that a mutant has, we are prompting the user and setting the value of the attribute.
If we had a list of the attribute names, we could probably make this much tidier. Let's add such a list to the Mutant
class!
We'll store the array of attribute names as a constant on the class. Then we'll add a class method that returns that array.
class Mutant
ATTRIBUTE_NAMES = [:real_name, :mutant_name, :power]
def self.attribute_names
ATTRIBUTE_NAMES
end
Since attribute_names
is a class method, we call it on the class itself—Mutant.attribute_names
—not on an instance of the class.
You know what? Now that we have that constant, we can use that to set up our attribute accessors!
attr_accessor *ATTRIBUTE_NAMES
The *
is the splat operator. In this case, the splat turns the array into an argument list. Pretty nifty!
How we can make use of this in RosterApplication#collect_mutant_data
? Spitting each attribute name onto the screen is easy enough, and it's not too far off from our prompts.
Mutant.attribute_names.each do |attribute|
print attribute
end
To make that into something that looks more like a prompt, we can replace any underscores with spaces (after converting the name into a string)...
print attribute.to_s.gsub('_', ' ')
...then capitalize it, and add a colon and a space to the end.
print attribute.to_s.gsub('_', ' ').capitalize + ': '
Now for the tricky part. We need to pass the result of gets.chomp
to the appropriate method of the mutant instance.
If you have the name of a method as a symbol, you can invoke that method on an object by passing it as an argument to the object's send
method. Strictly speaking, what we're doing when we call methods in Ruby is sending a message with the name of that method. Most of the time, we can just do that using the .
invocation operator, but when the message (method name) is dynamic, we need to pass it to send
.
In other words, mutant.real_name
is equivalent to mutant.send :real_name
. If the method takes arguments, those arguments are additional arguments to send
. So ['a', 'b', 'c'].send(:join, '-')
is equivalent to ['a', 'b', 'c'].join('-')
, returning "a-b-c"
.
Remember that when we appear to assign an attribute, like mutant.real_name = 'Jill'
, we're actually calling a method named real_name=
with 'Jill'
as an argument: mutant.real_name=('Jill')
. So to do this dynamically, it looks like this:
mutant.send "#{attribute}=", gets.chomp
Here's the whole collect_mutant_data
method at this point:
def collect_mutant_data
mutant = Mutant.new
Mutant.attribute_names.each do |attribute|
print attribute.to_s.gsub('_', ' ').capitalize + ': '
mutant.send "#{attribute}=", gets.chomp
end
@mutants << mutant
end
That's pretty nice, but I'd rather not refer to Mutant
explicitly when getting the array of attribute names. Once we've assigned an instance to mutant
, I'd rather refer to the class as mutant.class
, rather than hardcoding the class name a second time.
def collect_mutant_data
mutant = Mutant.new
mutant.class.attribute_names.each do |attribute|
print attribute.to_s.gsub('_', ' ').capitalize + ': '
mutant.send "#{attribute}=", gets.chomp
end
@mutants << mutant
end
Here's the whole thing:
#!/usr/bin/env ruby
require 'pry'
class Mutant
ATTRIBUTE_NAMES = [:real_name, :mutant_name, :power]
def self.attribute_names
ATTRIBUTE_NAMES
end
attr_accessor *ATTRIBUTE_NAMES
def description
"#{mutant_name} (also known as #{real_name}) has an incredible power: #{power}."
end
end
class RosterApplication
def start
@mutants = []
puts 'Hello, mutant collector!'
end
def collect_mutant_data
mutant = Mutant.new
mutant.class.attribute_names.each do |attribute|
print attribute.to_s.gsub('_', ' ').capitalize + ': '
mutant.send "#{attribute}=", gets.chomp
end
@mutants << mutant
end
def mutant_descriptions
@mutants.map(&:description).join("\n")
end
end
app = RosterApplication.new
app.start
3.times do
app.collect_mutant_data
end
puts app.mutant_descriptions
If we add more attributes to mutants now, we just have to add them to the ATTRIBUTE_NAMES
array! Should we need to add more instance variables that shouldn't be publicly accessible, and shouldn't be set by a user, we just won't include those in the ATTRIBUTE_NAMES
array. It's meant to hold only those attributes which should be accessible from the outside, and it serves that purpose nicely.
The descriptions still refer to specific attributes, which makes sense, given the sentence format. But it might be handy to have a method that returns any and all attributes in a hash. Let's do it!