-
Notifications
You must be signed in to change notification settings - Fork 0
MR09 Mutant Class
In this unit, we'll do the following:
- Extract the concept of a mutant into a separate class
We'll learn about the following concepts or tools:
- Data hiding
attr_accessor
Enumerable#map
Symbol#to_proc
We often create classes for the important nouns in our apps. Mutant is the main noun we're dealing with here, and it seems prudent to extract it into its own class.
To start, we'll include attribute accessors for real name, mutant name, and power.
class Mutant
attr_accessor :real_name, :mutant_name, :power
end
attr_accessor
give us getter and setter methods for the instance variables we specify as symbols.
So attr_accessor :real_name
is equivalent to this:
class Mutant
def real_name=(real_name)
@real_name = real_name
end
def real_name
@real_name
end
end
Direct access to the instance variables stays restricted the class itself, but attr_accessor
gives us methods we can use to set and retrieve their values.
Even the class as simple as this, we can use an instance of Mutant
in place of a hash to hold each mutant's data inside RosterApplication
. We need to update collect_mutant_data
and mutant_data
to use Mutant
instances.
class RosterApplication
def start
@mutants = []
puts 'Hello, mutant collector!'
end
def collect_mutant_data
mutant = Mutant.new
print 'Real name: '
mutant.real_name = gets.chomp
print 'Mutant name: '
mutant.mutant_name = gets.chomp
print 'Power: '
mutant.power = gets.chomp
@mutants << mutant
end
def mutant_data
output = ''
@mutants.each do |mutant|
output += "#{mutant.mutant_name} (also known as #{mutant.real_name}) has an incredible power: #{mutant.power}.\n"
end
output
end
end
At the moment, this doesn't provide any obvious benefits over simply using a hash. We can do more with our new class though.
How about building that descriptive string in mutant_data
within Mutant
itself?
class Mutant
attr_accessor :real_name, :mutant_name, :power
def description
"#{mutant_name} (also known as #{real_name}) has an incredible power: #{power}."
end
end
# ...
def mutant_data
output = ''
@mutants.each do |mutant|
output += "#{mutant.description}\n"
end
output
end
Now mutant_data
simply needs to get the description of each mutant. It's up to Mutant
to provide the actual descriptions. In fact, our each
is just performing an operation on each item in the array, and then collecting the results. We can use Enumerable#map
to collect each description in an array, and then return the result of joining the array with a newline as the separator.
def mutant_data
descriptions = @mutants.map { |mutant| mutant.description }
descriptions.join("\n")
end
@mutants.map { |mutant| mutant.description }
will collect all the descriptions in an array, which we then assign to a local variable, descriptions
.
descriptions.join("\n")
combines those descriptions in a single string, with newlines in between each one. I'm liking this much better.
We can actually make this even more succinct. Since all our block is doing is calling a single method on each item, we can use Symbol#to_proc
:
descriptions = @mutants.map(&:description)
How exactly Symbol#to_proc
works is fairly complex. Suffice to say, if the items in @mutants
all respond to description
, we can throw an ampersand in front of the symbol :description
to create a Proc
that behaves exactly like the block { |mutant| mutant.description }
.
This is now so simple that I think we just chain join
on the end and skip the local variable.
def mutant_data
@mutants.map(&:description).join("\n")
end
mutant_data
no longer seems like a good name for this method. It doesn't really return mutant data, does it? It returns mutant descriptions. Let's change the name accordingly.
def mutant_descriptions
@mutants.map(&:description).join("\n")
end
Don't forget to change the call to that method at the bottom of the script.
puts app.mutant_descriptions
We can improve this some more, but now is a good time to commit if you haven't done so in a while.