This exercise covers the following topics.
In four different places the executable bin/statowl
converts arrays of strings into arrays of floating-point numbers.
That's quadruplication!
With green tests protecting you from mistakes, extract the logic
in bin/statowl to a separate
method:
...
desc 'Computes the mean'
command :mean do |c|
c.action do |global_options,options,args|
puts array_from_STDIN.mean
end
end
desc 'Computes the median'
command :median do |c|
c.action do |global_options,options,args|
puts array_from_STDIN.median
end
end
desc 'Computes the variance'
command :variance do |c|
c.action do |global_options,options,args|
puts array_from_STDIN.variance
end
end
desc 'Computes the standard deviation'
command :deviation do |c|
c.action do |global_options,options,args|
puts array_from_STDIN.standard_deviation
end
end
...
def array_from_STDIN
STDIN.read.strip.split(',').map{ |number| number.to_f }
end
Still green?
Delegate the conversion of strings to floats to a single location in the
business logic by adding the mapping to lib/statowl/extensions.rb:
...
def as_f
map{ |number_as_string| number_as_string.to_f }
end
...
Then in bin/statowl shorten
STDIN.read.strip.split(',').map{ |number| number.to_f }
to
STDIN.read.strip.split(',').as_f
Still green?
Take a look at features/text.feature.
Remove the redundant setups by creating a Background for the scenarios:
Feature: Get statistics from numbers via the command line
If I have some numbers
I want to easily compute statistics on them
Background:
Given a file named "infile" with:
"""
1,2,3,4,5
"""
Scenario: Get Help
When I run `bundle exec statowl help`
Then the output should contain:
"""
statowl [global options] command
"""
Scenario: Get the mean
When I run `bundle exec statowl mean` interactively
And I pipe in the file "infile"
Then the output should contain:
"""
3.0
"""
Scenario: Get the median
When I run `bundle exec statowl median` interactively
And I pipe in the file "infile"
Then the output should contain:
"""
3.0
"""
Scenario: Get the variance
When I run `bundle exec statowl variance` interactively
And I pipe in the file "infile"
Then the output should contain:
"""
2.0
"""
Scenario: Get the standard deviation
When I run `bundle exec statowl deviation` interactively
And I pipe in the file "infile"
Then the output should contain:
"""
1.414
"""
Still green? Automated tests make refactoring production code and test code safe and easy.
StatOwl's project description states that the output format can be
plain text, HTML, XML, or JSON.
Add another scenario to features/text.feature that tests
your command line app's ability to format "all" statistics for plain text:
...
Scenario: Get nicely formatted statistics
When I run `bundle exec statowl all` interactively
And I pipe in the file "infile"
Then the output should contain:
"""
Mean: 3.0
Median: 3.0
Variance: 2.0
Standard Deviation: 1.414
"""
The scenario should fail.
Add production code to bin/statowl to make it pass:
...
desc 'Computes all statistics'
command :all do |c|
c.action do |global_options,options,args|
array = array_from_STDIN
puts %{Mean: #{array.mean}
Median: #{array.median}
Variance: #{array.variance}
Standard Deviation: #{array.standard_deviation}}
end
end
...
Try out the new feature from the command line:
echo "1,2,3,4,5" | bundle exec bin/statowl all
This works OK, but look at those terrible indent spasms
in bin/statowl
to make the output flush with the left margin.
This occurs frequently in multi-line strings and makes
code difficult to read.
The Facets gem adds a convenient unindent method
to correct this problem.
Add a Facets dependency to statowl.gemspec:
...
s.add_runtime_dependency('facets')
...
Now you can clean up your margins in bin/statowl:
require 'facets'
...
desc 'Computes all statistics'
command :all do |c|
c.action do |global_options,options,args|
array = array_from_STDIN
puts """
Mean: #{array.mean}
Median: #{array.median}
Variance: #{array.variance}
Standard Deviation: #{array.standard_deviation}
""".unindent.lstrip
end
end
...
Verify that all of your tests are passing. Then take a look at the unindented left margin for yourself:
echo "1,2,3,4,5" | bundle exec bin/statowl all
bin/statowl
This file will look really ugly when you add HTML, XML, and JSON capability.
Some major refactoring is needed before you implement the next feature.
Using your automated tests to keep bugs from arising, pull out knowledge of the
various types of statistics and put it where it belongs by adding the following to
lib/statowl/extensions.rb:
...
def statistics
keys = [ :mean, :median, :variance, :standard_deviation ]
values = keys.map{ |key| self.send(key) }
Hash[ keys.zip(values) ]
end
...
Now you can get the statistics as a generic hash.
With the following modification bin/statowl
has no knowledge of what the hash actually contains:
...
desc 'Computes all statistics'
command :all do |c|
c.action do |global_options,options,args|
array_from_STDIN.statistics.each do |key, value|
label = key.to_s.split("_").map{ |word| word.capitalize }.join(" ")
puts "#{label}: #{value}"
end
end
end
...
Still green?
This represents an improvement, because bin/statowl
no longer needs to know about specific statistics.
It merely formats a generic hash.
Nevertheless, it still does too much.
Move the formatting to lib/statowl/extensions.rb:
...
def statistics_as_text
statistics.map do |key, value|
"#{key.to_s.split('_').map{ |word| word.capitalize }.join(' ')}: #{value}\n"
end
end
...
Now your command line executable can delegate all formatting:
...
desc 'Computes all statistics'
command :all do |c|
c.action do |global_options,options,args|
puts array_from_STDIN.statistics_as_text
end
end
...
More refactoring? Take a look at this line in bold:
...
def statistics_as_text
statistics.map do |key, value|
"#{key.to_s.split('_').map{ |word| word.capitalize }.join(' ')}: #{value}\n"
end
end
...
This is a prime candidate for Extract Method refactoring. When the purpose of a line of code is to replace underscores with spaces and capitalize the results, then the code should say that. Extract Method refactoring is great for code that is too long or needs comments. Control your refactoring urges for now, however. You will extract the method in the next exercise.
You have implemented the Single Responsibility Principle.
The executable bin/statowl has no knowledge
of how to compute a complete set of mathematical statistics,
nor does in know how to format them as plain text.
lib/statowl/extensions.rb does not know
the source of its numbers nor what is done with the results.
The files now have completely separate concerns.
Automated tests facilitate aggressive refactoring. They create a safety net to keep delivery risks low and Ruby elegance high.
In this exercise you delegated business logic to keep your executable file short. You learned how to eliminate redundancy using Cucumber background statements. Finally, you refactored your code to conform with the Single Responsibility Principle. Along the way you gained additional practice with the Test-Code-Refactor mantra of Test Driven Development.
In the next exercise you will learn how to implement Ruby "Convention over Configuration" for a clean design.
Copyright © 2005-2026 Amp Books LLC All Rights Reserved