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-2022 Amp Books LLC All Rights Reserved