Where you and Bill face a problem with duplicated code.
Today, your boss asked you to work on a program for the accounting department. They want a system that flags expenses greater than $99 for computer gear, so they can crack down on developers splurging with company money. (You read that right: $99. The purchasing department isn’t fooling around.)
Some other developers already took a stab at the project, coding a report that lists all the components of each computer in the company and how much each component costs. To date, they haven’t plugged in any real data. Here’s where you and Bill come in.
Right from the start, you have a challenge on your hands: the data you need to load into the already established program is stored in a legacy system stuck behind an awkwardly coded class named DS (for “data source”):
| class DS |
| def initialize # connect to data source... |
| def get_cpu_info(workstation_id) # ... |
| def get_cpu_price(workstation_id) # ... |
| def get_mouse_info(workstation_id) # ... |
| def get_mouse_price(workstation_id) # ... |
| def get_keyboard_info(workstation_id) # ... |
| def get_keyboard_price(workstation_id) # ... |
| def get_display_info(workstation_id) # ... |
| def get_display_price(workstation_id) # ... |
| # ...and so on |
DS#initialize connects to the data system when you create a new DS object. The other methods—and there are dozens of them—take a workstation identifier and return descriptions and prices for the computer’s components. With Bill standing by to offer moral support, you quickly try the class in irb:
| ds = DS.new |
| ds.get_cpu_info(42) # => "2.9 Ghz quad-core" |
| ds.get_cpu_price(42) # => 120 |
| ds.get_mouse_info(42) # => "Wireless Touch" |
| ds.get_mouse_price(42) # => 60 |
It looks like workstation number 42 has a 2.9GHz CPU and a luxurious $60 mouse. This is enough data to get you started.
You have to wrap DS into an object that fits the reporting application. This means each Computer must be an object. This object has a single method for each component, returning a string that describes both the component and its price. Remember that price limit set by the purchasing department? Keeping this requirement in mind, you know that if the component costs $100 or more, the string must begin with an asterisk to draw people’s attention.
You kick off development by writing the first three methods in the Computer class:
| class Computer |
| def initialize(computer_id, data_source) |
| @id = computer_id |
| @data_source = data_source |
| end |
| |
| def mouse |
| info = @data_source.get_mouse_info(@id) |
| price = @data_source.get_mouse_price(@id) |
| result = "Mouse: #{info} ($#{price})" |
| return "* #{result}" if price >= 100 |
| result |
| end |
| |
| def cpu |
| info = @data_source.get_cpu_info(@id) |
| price = @data_source.get_cpu_price(@id) |
| result = "Cpu: #{info} ($#{price})" |
| return "* #{result}" if price >= 100 |
| result |
| end |
| |
| def keyboard |
| info = @data_source.get_keyboard_info(@id) |
| price = @data_source.get_keyboard_price(@id) |
| result = "Keyboard: #{info} ($#{price})" |
| return "* #{result}" if price >= 100 |
| result |
| end |
| |
| # ... |
| end |
At this point in the development of Computer, you find yourself bogged down in a swampland of repetitive copy and paste. You have a long list of methods left to deal with, and you should also write tests for each and every method, because it’s easy to make mistakes in duplicated code.
“I can think of two different ways to remove this duplication,” Bill says. “One is a spell called Dynamic Methods. The other is a special method called method_missing. We can try both solutions and decide which one we like better.” You agree to start with Dynamic Methods and get to method_missing after that.