This is my presentation from Ruby on Ales - March 2014 - Bend, OR
To paraphrase Mark Twain, "I didn't have time to write some small classes, so I wrote a BIG ONE instead." Now what do you do? Refactor! In this talk we'll refactor a large class into a series of smaller classes. We'll learn techniques to identify buried abstractions, what to extract, what to leave behind, and why delegation, composition and dependency inversion are key to writing small things that are easier to test.
"Subclassing and Composition – A Pythonic Tour of Trade-Offs", Hynek Schlawack
Small Code - Ruby on Ales 2014
1. Small Code
Ruby on Ales 2014
Mark Menard
@mark_menard
Enable Labs
Enable Labs
!
@mark_menard
2. ‘The great thing about
writing shitty code that “just
works,” is that it is too risky
and too expensive to
change, so it lives forever.’!
!
!
-Reginald Braithwaite @raganwald
Enable Labs
@mark_menard
8. So, what do I mean by small?
Small methods!
Enable Labs
Small classes
@mark_menard
9. Why should we strive for small code?
•
•
•
•
We don’t know what the future will bring!
Raise the level of abstraction!
Create composable components!
Prefer delegation over inheritance
Enable Labs
@mark_menard
10. Why should we strive for small code?
•
•
•
•
We don’t know what the future will bring!
Raise the level of abstraction!
Create composable components!
Prefer delegation over inheritance
Enable Future Change
Enable Labs
@mark_menard
11. What are the challenges of small code?
• Dependency Management!
• Context Management
Enable Labs
@mark_menard
12. The goal: Small units of
understandable code that
are amenable to change.
Enable Labs
@mark_menard
20. # some_ruby_program -v -sfoo
!
options = CommandLineOptions.new(ARGV) do
option :v
option :s, :string
end
!
if options.valid?
if options.value(:v)
# Do something
end
!
if (s_option = options.value(:s))
# Do something
end
end
Enable Labs
!17
@mark_menard
21. class CommandLineOptions
!
!
!
!
!
!
attr_accessor :argv
attr_reader :options
def initialize (argv = [], &block)
@options = {}
@argv = argv
instance_eval &block
end
def option (option_flag, option_type = :boolean)
options[option_flag] = option_type
end
def valid?
options.each do |option_flag, option_type|
raw_value = argv.find { |arg| arg =~ /^-#{option_flag}/ }
return false if option_type == :string && raw_value && raw_value.length < 3
end
end
def value (option_flag)
raw_option_value = argv.find { |arg| arg =~ /^-#{option_flag}/ }
return nil unless raw_option_value
option_type = options[option_flag]
return true if option_type == :boolean && raw_option_value
return raw_option_value[2..-1] if option_type == :string
end
end
Enable Labs
!18
@mark_menard
22. class CommandLineOptions
!
…
def initialize (argv = [], &block)
@options = {}
@argv = argv
instance_eval &block
end
!
…
!
end
Enable Labs
# In some_ruby_program
options = CommandLineOptions.new(ARGV) do
option :v
option :s, :string
end
!19
@mark_menard
23. class CommandLineOptions
!
…
def option (option_flag, option_type = :boolean)
options[option_flag] = option_type
end
!
…
!
end
Enable Labs
# In some_ruby_program
options = CommandLineOptions.new(ARGV) do
option :v
option :s, :string
end
!20
@mark_menard
24. class CommandLineOptions
!
!
!
!
…
def valid?
options.each do |option_flag, option_type|
raw_value = argv.find { |arg| arg =~ /^-#{option_flag}/ }
return false if option_type == :string && raw_value && raw_value.length < 3
end
end
…
end
Enable Labs
!21
@mark_menard
25. class CommandLineOptions
!
…
!
def value (option_flag)
raw_option_value = argv.find { |arg| arg =~ /^-#{option_flag}/ }
return nil unless raw_option_value
option_type = options[option_flag]
return true if option_type == :boolean && raw_option_value
return raw_option_value[2..-1] if option_type == :string
end
!
…
!
end
Enable Labs
!22
@mark_menard
26. CommandLineOptions
boolean options
are true if present
are false if absent
string options
must have content
are valid if there is content
are valid if not in argv
can return the value
return nil if not in argv
!
Finished in 0.00401 seconds
7 examples, 0 failures
Enable Labs
!23
@mark_menard
28. “The object programs that live
best and longest are those with
short methods.”!
! ! ! ! ! ! -Refactoring by Fields, Harvey, Fowler, Black
Enable Labs
@mark_menard
52. class CommandLineOptions
!
!
!
!
!
!
!
attr_accessor :argv
attr_reader :options
def initialize (argv = [], &block)
@options = {}
@argv = argv
instance_eval &block
end
def option (option_flag, option_type = :boolean)
options[option_flag] = option_type
end
def valid?
options.each do |option_flag, option_type|
raw_option_value = raw_value_for_option(option_flag)
return false if (option_type == :string || option_type == :integer) &&
raw_option_value && raw_option_value.length < 3
return false if option_type == :integer && raw_option_value &&
!(Integer(raw_option_value[2..-1]) rescue false)
end
end
def value (option_flag)
raw_option_value = raw_value_for_option(option_flag)
return nil unless raw_option_value
option_type = options[option_flag]
return true if option_type == :boolean && raw_option_value
return raw_option_value[2..-1] if option_type == :string
return (Integer(raw_option_value[2..-1])) if option_type == :integer
end
private def raw_value_for_option (option_flag)
argv.find { |arg| arg =~ /^-#{option_flag}/ }
end
end
Enable Labs
!39
@mark_menard
53. CommandLineOptions
boolean options
are true if present
are false if absent
string options
must have content
are valid if there is content
are valid if not in argv
can return the value
return nil if not in argv
integer options
must have content
are valid if there is content and it's an integer
are invalid if the content is not an integer
are valid if not in argv
can return the value
return nil if not in argv
!
Finished in 0.00338 seconds
13 examples, 0 failures
Enable Labs
!40
@mark_menard
54. class CommandLineOptions
!
!
!
!
…
def valid?
options.each do |option_flag, option_type|
raw_option_value = raw_value_for_option(option_flag)
return false if (option_type == :string || option_type == :integer) &&
raw_option_value && raw_option_value.length < 3
return false if option_type == :integer && raw_option_value &&
!(Integer(raw_option_value[2..-1]) rescue false)
end
end
…
end
Enable Labs
!41
@mark_menard
55. class CommandLineOptions
!
!
!
!
…
def valid?
options.each do |option_flag, option_type|
raw_option_value = raw_value_for_option(option_flag)
return false if (option_type == :string || option_type == :integer) &&
raw_option_value && raw_option_value.length < 3
return false if option_type == :integer && raw_option_value &&
!(Integer(raw_option_value[2..-1]) rescue false)
end
end
…
end
Enable Labs
!41
@mark_menard
56. class CommandLineOptions
!
!
!
!
…
def valid?
options.each do |option_flag, option_type|
raw_option_value = raw_value_for_option(option_flag)
return false if (option_type == :string || option_type == :integer) &&
raw_option_value && raw_option_value.length < 3
return false if option_type == :integer && raw_option_value &&
!(Integer(raw_option_value[2..-1]) rescue false)
end
end
…
end
Enable Labs
!41
@mark_menard
57. class CommandLineOptions
!
!
!
!
…
def value (option_flag)
raw_option_value = raw_value_for_option(option_flag)
return nil unless raw_option_value
option_type = options[option_flag]
return true if option_type == :boolean && raw_option_value
return raw_option_value[2..-1] if option_type == :string
return (Integer(raw_option_value[2..-1])) if option_type == :integer
end
…
end
Enable Labs
!42
@mark_menard
58. class CommandLineOptions
!
!
!
!
…
def value (option_flag)
raw_option_value = raw_value_for_option(option_flag)
return nil unless raw_option_value
option_type = options[option_flag]
return true if option_type == :boolean && raw_option_value
return raw_option_value[2..-1] if option_type == :string
return (Integer(raw_option_value[2..-1])) if option_type == :integer
end
…
end
Enable Labs
!42
@mark_menard
59. class CommandLineOptions
!
!
!
!
…
def value (option_flag)
raw_option_value = raw_value_for_option(option_flag)
return nil unless raw_option_value
option_type = options[option_flag]
return true if option_type == :boolean && raw_option_value
return raw_option_value[2..-1] if option_type == :string
return (Integer(raw_option_value[2..-1])) if option_type == :integer
end
…
end
Enable Labs
!42
@mark_menard
60. class CommandLineOptions
!
!
!
!
!
…
def valid?
options.each do |option_flag, option_type|
raw_option_value = raw_value_for_option(option_flag)
case(option_type)
when :string
return false if raw_option_value && raw_option_value.length < 3
when :integer
return false if raw_option_value && raw_option_value.length < 3
return false if raw_option_value && !(Integer(raw_option_value[2..-1]) rescue false)
end
end
end
…
end
Enable Labs
!43
@mark_menard
61. class CommandLineOptions
!
…
!
def value (option_flag)
raw_option_value = raw_value_for_option(option_flag)
return nil unless raw_option_value
option_type = options[option_flag]
!
return case(option_type)
when :string
raw_option_value[2..-1]
when :integer
(Integer(raw_option_value[2..-1]))
when :boolean
return true if option_type == :boolean && raw_option_value
end
end
!
…
!
end
Enable Labs
!44
@mark_menard
62. How do we write small classes?
•
•
•
•
•
•
•
Write small methods!
Talk to the class!
Find a good name!
Isolate Responsibilities!
Find cohesive sets of variables/properties!
Extract Class!
Move method
Enable Labs
@mark_menard
63. What are the characteristics of a
well designed small class?
•
•
•
•
•
Single responsibility!
Cohesive properties!
Small public interface (preferably a handful of
methods at the most)!
Implements a single Use Case if possible!
Primary logic is expressed in a composed method!
Enable Labs
@mark_menard
64. class CommandLineOptions
!
!
!
!
!
attr_accessor :argv
attr_reader :options
def initialize (argv = [], &block)
@options = {}
@argv = argv
instance_eval &block
end
def valid?
options.all?(&:valid)
end
def value (option_flag)
options[option_flag].value
end
private
!
!
def option (option_flag, option_type = :boolean)
options[option_flag] = build_option(option_flag, option_type)
end
def build_option
# Need to write this.
end
end
Enable Labs
!47
@mark_menard
67. How do we deal with
Dependencies?
• Dependency Inversion!
• Depend on Stable Abstractions
Enable Labs
@mark_menard
68. private def option (option_flag, option_type = :boolean)
options[option_flag] = case (option_type)
when :boolean
return BooleanOption.new(option_flag, nil)
when :string
return StringOption.new(option_flag, nil)
when :integer
return IntegerOption.new(option_flag, nil)
end
end
Enable Labs
!51
@mark_menard
69. class CommandLineOptions
!
!
!
!
…
def build_option (option_flag, option_type)
"#{option_type}_option".camelize.constantize.new(option_flag, raw_value_for_option(option_flag))
end
…
end
Enable Labs
!51
@mark_menard
70. class Option
!
!
!
def initialize (flag, raw_value)
@flag = flag
@raw_value = raw_value
end
!
class StringOption < Option
!
!
attr_reader :flag, :raw_value
end
!
class IntegerOption < Option
def valid?
return true unless raw_value
raw_value && raw_value.length > 2
end
def value
return nil unless raw_value
raw_value[2..-1]
end
end
Enable Labs
!
def valid?
return true unless raw_value
(raw_value && raw_value.length > 2) &&
(Integer(raw_value[2..-1]) rescue false)
end
def value
return nil unless raw_value
Integer(raw_value[2..-1])
end
end
!
class BooleanOption < Option
!
!
def valid?
true
end
def value
!!raw_value
end
end
!52
@mark_menard
71. !
!
class StringOption < Option
def valid?
options.each do |option_flag, option_type|
raw_option_value = raw_value_for_option(option_flag)
!
case(option_type)
when :string
return false if raw_option_value &&
raw_option_value.length < 3
when :integer
return false if raw_option_value
&& raw_option_value.length < 3
return false if raw_option_value &&
!(Integer(raw_option_value[2..-1]) rescue false)
end
end
end
!
def value
return nil unless raw_value
raw_value[2..-1]
end
end
!
class
!
def value (option_flag)
raw_option_value = raw_value_for_option(option_flag)
return nil unless raw_option_value
option_type = options[option_flag]
return case(option_type)
when :string
raw_option_value[2..-1]
when :integer
(Integer(raw_option_value[2..-1]))
when :boolean
return true if option_type == :boolean && raw_option_value
end
end
Enable Labs
def valid?
return true unless raw_value
raw_value && raw_value.length > 2
end
!
IntegerOption < Option
def valid?
return true unless raw_value
(raw_value && raw_value.length > 2) &&
(Integer(raw_value[2..-1]) rescue false)
end
def value
return nil unless raw_value
Integer(raw_value[2..-1])
end
end
!53
@mark_menard
72. class StringOption < Option
!
class CommandLineOptions
!
…
!
def valid?
options.all?(&:valid?)
end
!
def value (option_flag)
options[option_flag].value
end
!
end
Enable Labs
!
def valid?
return true unless raw_value
raw_value && raw_value.length > 2
end
def value
return nil unless raw_value
raw_value[2..-1]
end
end
!
class
!
!
IntegerOption < Option
def valid?
return true unless raw_value
(raw_value && raw_value.length > 2) &&
(Integer(raw_value[2..-1]) rescue false)
end
def value
return nil unless raw_value
Integer(raw_value[2..-1])
end
end
!53
@mark_menard
73. Option classes
Option
stores it's flag
stores it's raw value
BooleanOption
is true if the raw value is present
is false if the raw value is nil
is valid
StringOption
invalid when there is no content
is valid if there is content
is valid if raw value is nil
can return the value
value is nil if raw value is nil
IntegerOption
is invalid without content
is invalid if the content is not an integer
is valid if there is content and it's an integer
is valid if raw value is nil
can return the value
returns nil if raw value is nil
!
Finished in 0.00495 seconds
22 examples, 0 failures, 6 pending
Enable Labs
!54
@mark_menard
74. How do we isolate abstractions?
Separate the “what”
from the “how”.
Enable Labs
@mark_menard
75. !
def valid?
options.each do |option_flag, option_type|
raw_option_value = raw_value_for_option(option_flag)
case(option_type)
when :string
return false if raw_option_value && raw_option_value.length < 3
when :integer
return false if raw_option_value && raw_option_value.length < 3
return false if raw_option_value && !(Integer(raw_option_value[2..-1]) rescue false)
end
end
end
def valid?
options.values.all?(&:valid?)
end
Enable Labs
!56
@mark_menard
76. def value (option_flag)
raw_option_value = raw_value_for_option(option_flag)
return nil unless raw_option_value
option_type = options[option_flag]
return true if option_type == :boolean && raw_option_value
return raw_option_value[2..-1] if option_type == :string
return (Integer(raw_option_value[2..-1])) if option_type == :integer
end
def value (option_flag)
options[option_flag].value
end
Enable Labs
!57
@mark_menard
77. class CommandLineOptions
!
!
!
!
!
!
!
!
!
attr_accessor :argv
attr_reader :options
def initialize (argv = [], &block)
@options = {}
@argv = argv
instance_eval &block
end
def valid?
options.values.all?(&:valid?)
end
def value (option_flag)
options[option_flag].value
end
private
def option (option_flag, option_type = :boolean)
options[option_flag] = build_option(option_flag, option_type)
end
def build_option (option_flag, option_type)
"#{option_type}_option".camelize.constantize.new(option_flag, raw_value_for_option(option_flag))
end
def raw_value_for_option (option_flag)
argv.find { |arg| arg =~ /^-#{option_flag}/ }
end
end
Enable Labs
!58
@mark_menard
79. CommandLineOptions
builds an option object for each defined option (PENDING: Not yet implemented)
is valid if all options are valid (PENDING: Not yet implemented)
is invalid if any option is invalid (PENDING: Not yet implemented)
option object conventions
uses a StringOption for string options (PENDING: Not yet implemented)
uses a BooleanOption for boolean options (PENDING: Not yet implemented)
uses an IntegerOption for integer options (PENDING: Not yet implemented)
Enable Labs
!60
@mark_menard
80. describe CommandLineOptions do
it "builds an option object for each defined option" do
options = CommandLineOptions.new([ "-v" ]) { option :v }
expect(options.options.values.size).to eq(1)
end
!
!
!
!
!
!
it "is valid if all options are valid" do
options = CommandLineOptions.new([ "-sfoo" ]) { option :s, :string }
expect(options.valid?).to be_true
end
it "is invalid if any option is invalid" do
options = CommandLineOptions.new([ "-s" ]) { option :s, :string }
expect(options.valid?).to be_false
end
describe "option object conventions" do
it "uses a StringOption for string options" do
options = CommandLineOptions.new([ "-s" ]) { option :s, :string }
expect(options.options[:s].class).to eq(StringOption)
end
it "uses a BooleanOption for boolean options" do
options = CommandLineOptions.new([ "-s" ]) { option :v }
expect(options.options[:v].class).to eq(BooleanOption)
end
it "uses an IntegerOption for integer options" do
options = CommandLineOptions.new([ "-s" ]) { option :i, :integer }
expect(options.options[:i].class).to eq(IntegerOption)
end
end
end
Enable Labs
!61
@mark_menard
81. describe CommandLineOptions do
it "builds an option object for each defined option" do
options = CommandLineOptions.new([ "-v" ]) { option :v }
expect(options.options.values.size).to eq(1)
end
!
!
!
!
!
!
it "is valid if all options are valid" do
options = CommandLineOptions.new([ "-sfoo" ]) { option :s, :string }
expect(options.valid?).to be_true
end
it "is invalid if any option is invalid" do
options = CommandLineOptions.new([ "-s" ]) { option :s, :string }
expect(options.valid?).to be_false
end
describe "option object conventions" do
it "uses a StringOption for string options" do
options = CommandLineOptions.new([ "-s" ]) { option :s, :string }
expect(options.options[:s].class).to eq(StringOption)
end
it "uses a BooleanOption for boolean options" do
options = CommandLineOptions.new([ "-s" ]) { option :v }
expect(options.options[:v].class).to eq(BooleanOption)
end
it "uses an IntegerOption for integer options" do
options = CommandLineOptions.new([ "-s" ]) { option :i, :integer }
expect(options.options[:i].class).to eq(IntegerOption)
end
end
end
Enable Labs
!61
@mark_menard
82. describe CommandLineOptions do
it "builds an option object for each defined option" do
options = CommandLineOptions.new([ "-v" ]) { option :v }
expect(options.options.values.size).to eq(1)
end
!
!
!
!
!
!
it "is valid if all options are valid" do
options = CommandLineOptions.new([ "-sfoo" ]) { option :s, :string }
expect(options.valid?).to be_true
end
it "is invalid if any option is invalid" do
options = CommandLineOptions.new([ "-s" ]) { option :s, :string }
expect(options.valid?).to be_false
end
describe "option object conventions" do
it "uses a StringOption for string options" do
options = CommandLineOptions.new([ "-s" ]) { option :s, :string }
expect(options.options[:s].class).to eq(StringOption)
end
it "uses a BooleanOption for boolean options" do
options = CommandLineOptions.new([ "-s" ]) { option :v }
expect(options.options[:v].class).to eq(BooleanOption)
end
it "uses an IntegerOption for integer options" do
options = CommandLineOptions.new([ "-s" ]) { option :i, :integer }
expect(options.options[:i].class).to eq(IntegerOption)
end
end
end
Enable Labs
!61
@mark_menard
83. CommandLineOptions
builds an option object for each defined option
is valid if all options are valid
is invalid if any option is invalid
option object conventions
uses a StringOption for string options
uses a BooleanOption for boolean options
uses an IntegerOption for integer options
Enable Labs
!62
@mark_menard
86. describe "array options" do
it "can return the value as an array" do
expect(CommandLineOptions.new([ "-afoo,bar,baz" ]) { option :a, :array }.value(:a)).to eq(["foo", "bar", "baz"])
end
end
Enable Labs
!65
@mark_menard
87. describe "array options" do
it "can return the value as an array" do
expect(CommandLineOptions.new([ "-afoo,bar,baz" ]) { option :a, :array }.value(:a)).to eq(["foo", "bar", "baz"])
end
end
class ArrayOption < OptionWithContent
def value
return nil if option_unset?
extract_value_from_raw_value.split(",")
end
end
Enable Labs
!65
@mark_menard
88. CommandLineOptions
boolean options
are true if present
are false if absent
string options
must have content
is valid when there is content
can return the value
return nil if not in argv
integer options
must have content
must be an integer
can return the value as an integer
returns nil if not in argv
array options
can return the value as an array
!
OptionWithContent
has a flag
is valid when it has no raw value
is valid when it has a value
can return it's value when present
returns nil if the flag has no raw value
Enable Labs
!66
@mark_menard
89. Now We’re Done!!
Let them implement their own
option classes. It’s easy.
Enable Labs
@mark_menard
90. Credits
•
Syntax highlighting: pbpaste | highlight --syntax=rb --style=edit-xcode -out-format=rtf | pbcopy!
•
Command line option example inspiration Uncle Bob.
Enable Labs
@mark_menard