-
Notifications
You must be signed in to change notification settings - Fork 0
MR13 Monkey Patching
In this unit, we'll do the following:
- Add a convenience method for replacing underscores with spaces
We'll learn about the following concepts or tools:
- Monkey patching
Twice now we've wanted human-readable versions of our attribute names. It seems like something that will pop up again, and we'll just end up copying and pasting the same code... unless we do something about it!
The main thing we did to make them readable was to replace underscores with spaces, .gsub('_', ' ')
. We've also capitalized the attribute name in both cases, but we may not always want to. It seems acceptable to continue to chain the capitalization on the end when calling it.
Let's write a humanize
method that takes a string, and returns a copy of that string with the underscores replaced with spaces. Now then... where do we put this method?
It doesn't really make sense to make it an instance method of Mutant
.
class Mutant
def humanize(str)
str.gsub('_', ' ')
end
end
Think about how strangely that would read when it's called.
mutant.attributes.each do |attribute, value|
report_output += mutant.humanize(attribute.to_s).capitalize + ': '
report_output += "#{value}\n"
end
The operation we're performing doesn't really have anything to do with the mutant instance. Sure we happen to be passing in an attribute name as an argument to the method, but the method doesn't use any instance variables or call any other instance methods. Even if it were using the attribute name without having it passed in as an argument, the names apply to the entire class. They don't change from one instance to another.
We could make it a class method.
class Mutant
def self.humanize(str)
str.gsub('_', ' ')
end
end
mutant.attributes.each do |attribute, value|
report_output += Mutant.humanize(attribute.to_s).capitalize + ': '
report_output += "#{value}\n"
end
That's a little bit better, but it really doesn't have anything to do with mutants, period. It can transform any string.
We could make a module to hold utility methods like this one. That would be a good, clean solution. But it would be more fun if we could make it an instance method on all strings!
[1] pry(main)> 'my_gross_string'.humanize
=> "my gross string"
Ruby makes it very easy to do exactly that. Very, very easy. You can re-open an existing class, as though you were defining it, and add to it as you please.
class String
def humanize
gsub('_', ' ')
end
end
We're not replacing the String
class with a new one. We're re-opening the one that already exists and making modifications to it. Magic!
Changing classes or modules at runtime in this manner is referred to as monkey patching.
We can even replace existing methods with our own implementations. Want to change String#reverse
to behave like capitalize instead? You can!
class String
def reverse
capitalize
end
end
Hopefully that strikes you as a terrible idea. Even if we used a less ridiculous example—like monkey patching capitalize
to capitalize the first letter of every word instead of just the first letter of the entire string—we'd still break any code that was dependent on the original behavior. In a project this small, that might not seem important. But even another built-in method might be calling capitalize
internally, relying on its expected behavior. It's not an issue we'll know about until we run into it, and it could be a nightmare to debug when it happens.
But what about our String#humanize
method? That seems really useful, and it doesn't conflict with anything in standard Ruby: It's a completely new method!
That's certainly much safer than changing the behavior of a pre-existing method, but it can still lead to trouble down the road. If we start to integrate our code into a larger project, or if we include third-party libraries in our project, we could start to create problems. We're not the first people on the block to think that String
could use a humanize
method, and others may be relying on the presence of a monkey patch that behaves differently.
For the moment, convenience trumps hypothetical side effects. Let's do it!
We could do this in one of our existing files, but it feels better to create a new lib/string.rb
file.
lib/string.rb
class String
def humanize
gsub('_', ' ')
end
end
Remember to include it...
roster.rb
require_relative 'lib/string'
require_relative 'lib/mutant'
require_relative 'lib/roster_application'
require 'pry'
...and to use it.
lib/roster_application.rb
report_output += attribute.to_s.humanize.capitalize + ': '
Since our humanize
method is on String
, we have to convert a symbol to a string before we can use it. Why don't we add humanize
to Symbol
too? We'll once again create a new file for it:
lib/symbol.rb
class Symbol
def humanize
to_s.humanize
end
end
Remember to require it:
roster.rb
require_relative 'lib/string'
require_relative 'lib/symbol'
require_relative 'lib/mutant'
require_relative 'lib/roster_application'
require 'pry'
Now we can change both RosterApplication#collect_mutant_data
and RosterApplication#report
to use our fancy new monkey-patched methods.
lib/roster_application.rb
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.humanize.capitalize + ': '
mutant.send "#{attribute}=", gets.chomp
end
@mutants << mutant
end
def mutant_descriptions
@mutants.map(&:description).join("\n")
end
def report
report_output = ''
@mutants.each_with_index do |mutant, i|
report_output += "\nMutant #{i + 1} ================================\n"
mutant.attributes.each do |attribute, value|
report_output += attribute.humanize.capitalize + ': '
report_output += "#{value}\n"
end
end
report_output
end
end
Beautiful!