Enable Labs @mark_menard
Let’s Do Some Upfront Design
Mark Menard
Windy City Rails 2014
@mark_menard !Enable Labs
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
Who likes TDD?
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
Who likes refactoring?
Rename Method
Rename Class
Extract Method
Inline Method
Replace Temp with Query
Replace Method with Method Object
Move Method
Extract Class
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard5
Extract Method
Replace Method with Method Object
Move Method
Extract Class
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
A Tale of a Refactoring
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
# some_ruby_program -v -efoo
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard8
# some_ruby_program -v -efoo !options = CommandLineOptions.new(ARGV) do option :v option :e, :string end !if options.has(:v) # Do something end !if options.has(:e) some_value = options.value(:e) # Do something with the expression end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
describe CommandLineOptions do ! describe "boolean options" do let(:options) { CommandLineOptions.new { option :c } } it "are true if present" do … it "are false if absent" do … end ! describe "string options" do let(:options) { CommandLineOptions.new { option :e, :string } } it "must have content" do … it "can return the value" do … it "return nil if not in argv" do … end end
9
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard10
CommandLineOptions boolean options are true if present are false if absent string options must have content can return the value return nil if not in argv!
Finished in 0.00236 seconds5 examples, 0 failures
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard11
class CommandLineOptions ! attr_accessor :argv attr_reader :options ! def initialize (argv = [], &block) @options = {} @argv = argv instance_eval &block end ! def has (option_flag) options.include?(option_flag) && argv.include?("-#{option_flag}") end ! def option (option_flag, option_type = :boolean) options[option_flag] = option_type end ! def valid? options.each do |option_flag, option_type| return false if option_type == :string && raw_value_for_option(option_flag).length < 3 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 raw_option_value[2..-1] if option_type == :string end ! private def raw_value_for_option (option_flag) argv.find { |arg| arg =~ /-#{option_flag}/ } end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard12
# some_ruby_program -v -efoo -i100 !
options = CommandLineOptions.new(ARGV) do option :v option :e, :string option :i, :integer end !
verbose = options.value(:v) expression = options.value(:e) iteration_count = options.value(:i) || 1
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard13
describe "integer options" do it "must have content" it "must be an integer" it "can return the value as an integer" it "returns nil if not in argv" end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard14
CommandLineOptions boolean options are true if present are false if absent string options must have content can return the value return nil if not in argv integer options must have content (PENDING: No reason given) must be an integer (PENDING: Not yet implemented) can return the value as an integer (PENDING: Not yet implemented) returns nil if not in argv (PENDING: Not yet implemented)
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard15
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 (PENDING: Not yet implemented)!Pending: CommandLineOptions integer options returns nil if not in argv # Not yet implemented # ./spec/command_line_options_spec.rb:61!Finished in 0.00291 seconds10 examples, 0 failures, 1 pending
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard16
class CommandLineOptions ! attr_accessor :argv attr_reader :options ! def initialize (argv = [], &block) @options = {} @argv = argv instance_eval &block end ! def has (option_flag) options.include?(option_flag) && argv.include?("-#{option_flag}") end ! def valid? options.each do |option_flag, option_type| return false unless option_valid?(option_type, raw_value_for_option(option_flag)) 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 extract_value_from_raw_value(raw_option_value) if option_type == :string end ! private def option (option_flag, option_type = :boolean) options[option_flag] = option_type end ! private def option_valid? (option_type, raw_value) send("#{option_type}_option_valid?", raw_value) end ! private def string_option_valid? (raw_value) extract_value_from_raw_value(raw_value).length > 0 end ! private def integer_option_valid? (raw_value) extract_value_from_raw_value(raw_value).length > 0 && (Integer(extract_value_from_raw_value(raw_value)) rescue false) end ! private def boolean_option_valid? (raw_value) true end ! private def extract_value_from_raw_value (raw_value) raw_value[2..-1] end ! private def raw_value_for_option (option_flag) argv.find { |arg| arg =~ /-#{option_flag}/ } end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
Oooooof! !!
This class is getting big…
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
So… let’s extract a class and move some methods.
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard19
describe StringOption do let(:string_option) { StringOption.new('s', '-sfoo') } ! it "has a flag" it "is valid when it has a value" it "can return it's value when present" it "returns nil if the flag has no raw value" end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard20
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 (PENDING: Not yet implemented)!StringOption has a flag (PENDING: No reason given) is valid when it has a value (PENDING: Not yet implemented) can return it's value when present (PENDING: Not yet implemented) returns nil if the flag has no raw value (PENDING: Not yet implemented)
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard21
class StringOption !
attr_reader :flag !
def initialize (flag, raw_value) @flag = flag @raw_value = raw_value end !
end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard22
class CommandLineOptions ! private def string_option_valid? (raw_value) extract_value_from_raw_value(raw_value).length > 0 end !end
class StringOption ! attr_reader :flag, :raw_value ! def initialize (flag, raw_value) @flag = flag @raw_value = raw_value end ! def valid? extract_value_from_raw_value.length > 0 end ! private def extract_value_from_raw_value raw_value[2..-1] end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
Ow, ow, ow, ow… ow!
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
The Extract Class and Move Method refactorings are VERY expensive.
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard28
class StringOption ! attr_reader :flag, :raw_value ! def initialize (flag, raw_value) @flag = flag @raw_value = raw_value end ! def valid? extract_value_from_raw_value.length > 0 end ! private def extract_value_from_raw_value raw_value[2..-1] end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard29
class IntegerOption ! attr_reader :flag, :raw_value ! def initialize (flag, raw_value) @flag = flag @raw_value = raw_value end ! def valid? extract_value_from_raw_value.length > 0 && real_value_is_integer? end ! private def extract_value_from_raw_value raw_value[2..-1] end ! private def real_value_is_integer? (Integer(extract_value_from_raw_value) rescue false) end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard30
class BooleanOption ! attr_reader :flag, :raw_value ! def initialize (flag, raw_value) @flag = flag @raw_value = raw_value end ! def valid? true end ! def value !!raw_value end end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard31
class Option ! attr_reader :flag, :raw_value ! def initialize (flag, raw_value) @flag = flag @raw_value = raw_value end !end !
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard32
class StringOption < Option ! def valid? extract_value_from_raw_value.length > 0 end ! private def extract_value_from_raw_value raw_value[2..-1] end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard33
class IntegerOption < Option ! def valid? extract_value_from_raw_value.length > 0 && real_value_is_integer? end ! private def extract_value_from_raw_value raw_value[2..-1] end ! private def real_value_is_integer? (Integer(extract_value_from_raw_value) rescue false) end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard34
class BooleanOption < Option ! def valid? true end ! def value !!raw_value end end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard35
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!StringOption has a flag is valid when it has a value can return it's value when present returns nil if the flag has no raw value
Duplication!
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
All code is an impediment to change.
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
Including your tests!
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
Can we avoid this churn?
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
Do some upfront design!
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
But that’s not agile… right?
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
Sequence Diagrams!
Collaboration Diagrams!
Mental Experiments!
Playing with Code / Exploratory Tests
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
Container Cost Calculations
• Bunker Charge (fuel charge)!
• Taxes!
• Carrier Surcharges
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard43
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard44
class ShipmentCostCalculator def initialize (type: type, shipment: shipment) @type = type @shipment = shipment end ! def calculate cost = 0 itinerary = shipment.itinerary carrier = shipment.carrier ! # Calculate bunker charge ! distance = itinerary.segments.sum(&:distance_in_km) cost += carrier.bunker_charge_per_km * distance ! # Calculate taxes ! cost += itinerary.segments.select { |s| s.taxable? }.sum(&:tax_fee) ! # Calculate surcharges ! cost += carrier.surcharges.sum(&:charge) cost end end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard44
class ShipmentCostCalculator def initialize (type: type, shipment: shipment) @type = type @shipment = shipment end ! def calculate cost = 0 itinerary = shipment.itinerary carrier = shipment.carrier ! # Calculate bunker charge ! distance = itinerary.segments.sum(&:distance_in_km) cost += carrier.bunker_charge_per_km * distance ! # Calculate taxes ! cost += itinerary.segments.select { |s| s.taxable? }.sum(&:tax_fee) ! # Calculate surcharges ! cost += carrier.surcharges.sum(&:charge) cost end end
What
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard44
class ShipmentCostCalculator def initialize (type: type, shipment: shipment) @type = type @shipment = shipment end ! def calculate cost = 0 itinerary = shipment.itinerary carrier = shipment.carrier ! # Calculate bunker charge ! distance = itinerary.segments.sum(&:distance_in_km) cost += carrier.bunker_charge_per_km * distance ! # Calculate taxes ! cost += itinerary.segments.select { |s| s.taxable? }.sum(&:tax_fee) ! # Calculate surcharges ! cost += carrier.surcharges.sum(&:charge) cost end end
How
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard45
class ShipmentCostCalculator def initialize (type: type, shipment: shipment) @type = type @shipment = shipment end ! def calculate cost = 0 itinerary = shipment.itinerary carrier = shipment.carrier ! # Calculate bunker charge ! distance = itinerary.segments.sum(&:distance_in_km) cost += carrier.bunker_charge_per_km * distance ! # Calculate taxes ! cost += itinerary.segments.select { |s| s.taxable? }.sum(&:tax_fee) ! # Calculate surcharges ! cost += carrier.surcharges.sum(&:charge) cost end end
What
How
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard46
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard47
class ShipmentCostCalculator attr_reader :shipment, :itinerary, :carrier ! def initialize (shipment: shipment) @shipment = shipment @itinerary = shipment.itinerary @carrier = shipment.carrier end ! def calculate calculate_bunker_charge + calculate_tax_charge + calculate_surcharge end ! def calculate_bunker_charge distance = itinerary.segments.sum(&:distance_in_km) carrier.bunker_charge_per_km * distance end ! def calculate_tax_charge itinerary.segments.select { |s| s.taxable? }.sum(&:tax_fee) end ! def calculate_surcharge carrier.surcharges.sum(&:charge) end !end
What
How
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
CoordinatorProcessor
Processor
Processor
Coordinator
Processor
Coordinator Coordinator
Processor
Processor
ProcessorProcessor
Processor
ProcessorCoordinators vs Processors
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
WhatHow
How
How
What
How
What What
How
How
HowHow
How
HowCoordinators vs Processors
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
Container Cost Calculations
• Bunker Charge (fuel charge)!
• Taxes!
• Carrier Surcharges
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard50
What How
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard51
Coordinator Processor
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
So, how do I build that without the churn?
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
So, how do I build that without the churn?
1. Identify a top level Coordinator and give it a descriptive name based on the use case.
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard54
class ShipmentCostCalculator end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
So, how do I build that without the churn?
1. Identify a top level Coordinator and give it a descriptive name based on the use case.!
2. Write a failing top level feature spec that runs everything without test doubles using real data.
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard56
require 'spec_helper' !describe "shipment cost calculation feature" do before(:each) do setup_itineraries setup_carrier_charges end ! describe "basic shipment" do let(:shipment) { build(:basic_shipment) } ! it "should correctly calculate shipment charge" do calculator = ShipmentCostCalculator.new(shipment: shipment) expect(calculator.calculate).to eq(1200) end end ! # more scenarios... !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard56
require 'spec_helper' !describe "shipment cost calculation feature" do before(:each) do setup_itineraries setup_carrier_charges end ! describe "basic shipment" do let(:shipment) { build(:basic_shipment) } ! it "should correctly calculate shipment charge" do calculator = ShipmentCostCalculator.new(shipment: shipment) expect(calculator.calculate).to eq(1200) end end ! # more scenarios... !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard56
require 'spec_helper' !describe "shipment cost calculation feature" do before(:each) do setup_itineraries setup_carrier_charges end ! describe "basic shipment" do let(:shipment) { build(:basic_shipment) } ! it "should correctly calculate shipment charge" do calculator = ShipmentCostCalculator.new(shipment: shipment) expect(calculator.calculate).to eq(1200) end end ! # more scenarios... !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard56
require 'spec_helper' !describe "shipment cost calculation feature" do before(:each) do setup_itineraries setup_carrier_charges end ! describe "basic shipment" do let(:shipment) { build(:basic_shipment) } ! it "should correctly calculate shipment charge" do calculator = ShipmentCostCalculator.new(shipment: shipment) expect(calculator.calculate).to eq(1200) end end ! # more scenarios... !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard57
class ShipmentCostCalculator attr_reader :shipment !
def initialize (shipment:) @shipment = shipment end !
def calculate end end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
So, how do I build that without the churn?
1. Identify a top level Coordinator and give it a descriptive name based on the use case.!
2. Write a failing top level feature spec that runs everything without test doubles using real data.!
3. Write a failing coordinator spec using test doubles for the top level coordinator.!
1. Identify other collaborators and inject them using test doubles.
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard59
require 'spec_helper' !describe "shipment cost calculation" do ! let(:shipment) { double } let(:bunker_charge_calculator) { double("bunker_charge_calculator", :calculate => 1) } ! let(:shipment_cost_calculator) { ShipmentCostCalculator.new(shipment: shipment) } ! before(:each) do expect(BunkerChargeCalculator).to receive(:new).and_return(bunker_charge_calculator) end ! it "uses a bunker charge calculator" do expect(bunker_charge_calculator).to receive(:calculate).once.with(shipment) shipment_cost_calculator.calculate end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard59
require 'spec_helper' !describe "shipment cost calculation" do ! let(:shipment) { double } let(:bunker_charge_calculator) { double("bunker_charge_calculator", :calculate => 1) } ! let(:shipment_cost_calculator) { ShipmentCostCalculator.new(shipment: shipment) } ! before(:each) do expect(BunkerChargeCalculator).to receive(:new).and_return(bunker_charge_calculator) end ! it "uses a bunker charge calculator" do expect(bunker_charge_calculator).to receive(:calculate).once.with(shipment) shipment_cost_calculator.calculate end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard59
require 'spec_helper' !describe "shipment cost calculation" do ! let(:shipment) { double } let(:bunker_charge_calculator) { double("bunker_charge_calculator", :calculate => 1) } ! let(:shipment_cost_calculator) { ShipmentCostCalculator.new(shipment: shipment) } ! before(:each) do expect(BunkerChargeCalculator).to receive(:new).and_return(bunker_charge_calculator) end ! it "uses a bunker charge calculator" do expect(bunker_charge_calculator).to receive(:calculate).once.with(shipment) shipment_cost_calculator.calculate end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard59
require 'spec_helper' !describe "shipment cost calculation" do ! let(:shipment) { double } let(:bunker_charge_calculator) { double("bunker_charge_calculator", :calculate => 1) } ! let(:shipment_cost_calculator) { ShipmentCostCalculator.new(shipment: shipment) } ! before(:each) do expect(BunkerChargeCalculator).to receive(:new).and_return(bunker_charge_calculator) end ! it "uses a bunker charge calculator" do expect(bunker_charge_calculator).to receive(:calculate).once.with(shipment) shipment_cost_calculator.calculate end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard59
require 'spec_helper' !describe "shipment cost calculation" do ! let(:shipment) { double } let(:bunker_charge_calculator) { double("bunker_charge_calculator", :calculate => 1) } ! let(:shipment_cost_calculator) { ShipmentCostCalculator.new(shipment: shipment) } ! before(:each) do expect(BunkerChargeCalculator).to receive(:new).and_return(bunker_charge_calculator) end ! it "uses a bunker charge calculator" do expect(bunker_charge_calculator).to receive(:calculate).once.with(shipment) shipment_cost_calculator.calculate end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard59
require 'spec_helper' !describe "shipment cost calculation" do ! let(:shipment) { double } let(:bunker_charge_calculator) { double("bunker_charge_calculator", :calculate => 1) } ! let(:shipment_cost_calculator) { ShipmentCostCalculator.new(shipment: shipment) } ! before(:each) do expect(BunkerChargeCalculator).to receive(:new).and_return(bunker_charge_calculator) end ! it "uses a bunker charge calculator" do expect(bunker_charge_calculator).to receive(:calculate).once.with(shipment) shipment_cost_calculator.calculate end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard59
require 'spec_helper' !describe "shipment cost calculation" do ! let(:shipment) { double } let(:bunker_charge_calculator) { double("bunker_charge_calculator", :calculate => 1) } ! let(:shipment_cost_calculator) { ShipmentCostCalculator.new(shipment: shipment) } ! before(:each) do expect(BunkerChargeCalculator).to receive(:new).and_return(bunker_charge_calculator) end ! it "uses a bunker charge calculator" do expect(bunker_charge_calculator).to receive(:calculate).once.with(shipment) shipment_cost_calculator.calculate end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
So, how do I build that without the churn?
1. Identify a top level Coordinator and give it a descriptive name based on the use case.!
2. Write a failing top level feature spec that runs everything without test doubles using real data.!
3. Write a failing coordinator spec using test doubles for the top level coordinator.!
1. Identify other collaborators and inject them using test doubles.!
4. Make the top level Coordinator pass the spec.
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard61
class ShipmentCostCalculator attr_reader :shipment, :bunker_charge_calculator ! def initialize (shipment:, bunker_charge_calculator: BunkerChargeCalculator.new) ! @shipment = shipment @bunker_charge_calculator = bunker_charge_calculator end ! def calculate bunker_charge_calculator.calculate(shipment) end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard61
class ShipmentCostCalculator attr_reader :shipment, :bunker_charge_calculator ! def initialize (shipment:, bunker_charge_calculator: BunkerChargeCalculator.new) ! @shipment = shipment @bunker_charge_calculator = bunker_charge_calculator end ! def calculate bunker_charge_calculator.calculate(shipment) end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard61
class ShipmentCostCalculator attr_reader :shipment, :bunker_charge_calculator ! def initialize (shipment:, bunker_charge_calculator: BunkerChargeCalculator.new) ! @shipment = shipment @bunker_charge_calculator = bunker_charge_calculator end ! def calculate bunker_charge_calculator.calculate(shipment) end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard62
require 'spec_helper' !describe "shipment cost calculation" do ! let(:shipment) { double } let(:bunker_charge_calculator) { double("bunker_charge_calculator", :calculate => 1) } ! let(:shipment_cost_calculator) { ShipmentCostCalculator.new(shipment: shipment) } ! before(:each) do expect(BunkerChargeCalculator).to receive(:new).and_return(bunker_charge_calculator) expect(TaxChargeCalculator).to receive(:new).and_return(tax_charge_calculator) expect(SurchargeCalculator).to receive(:new).and_return(surcharge_calculator) end ! it "uses a bunker charge calculator" do expect(bunker_charge_calculator).to receive(:calculate).once.with(shipment) shipment_cost_calculator.calculate end ! it "uses a tax charge calculator" do expect(tax_charge_calculator).to receive(:calculate).once.with(shipment) shipment_cost_calculator.calculate end ! it "uses a surcharge calculator" do expect(surcharge_calculator).to receive(:calculate).once.with(shipment) shipment_cost_calculator.calculate end ! it "returns the sum of bunker charge, tax charge, and shurcharges" do expect(shipment_cost_calculator.calculate).to eq(6) end end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard63
class ShipmentCostCalculator attr_reader :shipment, :bunker_charge_calculator attr_reader :tax_charge_calculator, :surcharge_calculator ! def initialize (shipment:, bunker_charge_calculator: BunkerChargeCalculator.new, tax_charge_calculator: TaxChargeCalculator.new, surcharge_calculator: SurchargeCalculator.new) ! @shipment = shipment @bunker_charge_calculator = bunker_charge_calculator @tax_charge_calculator = tax_charge_calculator @surcharge_calculator = surcharge_calculator end ! def calculate bunker_charge_calculator.calculate(shipment) + tax_charge_calculator.calculate(shipment) + surcharge_calculator.calculate(shipment) end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard63
class ShipmentCostCalculator attr_reader :shipment, :bunker_charge_calculator attr_reader :tax_charge_calculator, :surcharge_calculator ! def initialize (shipment:, bunker_charge_calculator: BunkerChargeCalculator.new, tax_charge_calculator: TaxChargeCalculator.new, surcharge_calculator: SurchargeCalculator.new) ! @shipment = shipment @bunker_charge_calculator = bunker_charge_calculator @tax_charge_calculator = tax_charge_calculator @surcharge_calculator = surcharge_calculator end ! def calculate bunker_charge_calculator.calculate(shipment) + tax_charge_calculator.calculate(shipment) + surcharge_calculator.calculate(shipment) end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard63
class ShipmentCostCalculator attr_reader :shipment, :bunker_charge_calculator attr_reader :tax_charge_calculator, :surcharge_calculator ! def initialize (shipment:, bunker_charge_calculator: BunkerChargeCalculator.new, tax_charge_calculator: TaxChargeCalculator.new, surcharge_calculator: SurchargeCalculator.new) ! @shipment = shipment @bunker_charge_calculator = bunker_charge_calculator @tax_charge_calculator = tax_charge_calculator @surcharge_calculator = surcharge_calculator end ! def calculate bunker_charge_calculator.calculate(shipment) + tax_charge_calculator.calculate(shipment) + surcharge_calculator.calculate(shipment) end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard63
class ShipmentCostCalculator attr_reader :shipment, :bunker_charge_calculator attr_reader :tax_charge_calculator, :surcharge_calculator ! def initialize (shipment:, bunker_charge_calculator: BunkerChargeCalculator.new, tax_charge_calculator: TaxChargeCalculator.new, surcharge_calculator: SurchargeCalculator.new) ! @shipment = shipment @bunker_charge_calculator = bunker_charge_calculator @tax_charge_calculator = tax_charge_calculator @surcharge_calculator = surcharge_calculator end ! def calculate bunker_charge_calculator.calculate(shipment) + tax_charge_calculator.calculate(shipment) + surcharge_calculator.calculate(shipment) end !end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard64
require 'spec_helper' !describe "shipment cost calculation" do ! # . . . it "returns the sum of bunker charge, tax charge, and shurcharges" do expect(shipment_cost_calculator.calculate).to eq(6) end end
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
So, how do I build that without the churn?
1. Identify a top level Coordinator and give it a descriptive name based on the use case.!
2. Write a top level feature spec that runs everything without test doubles using real data.!
3. Write a coordinator spec using test doubles for the top level coordinator.!
1. Identify other collaborators and inject them using test doubles.!
4. Make the top level Coordinator pass the spec.!
5. Repeat 2-3 for the identified collaborators until you can’t delegate anymore.
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard66
Coordinator ?
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard67
Shipment Cost
Calculator Surcharge Calculator
Tax Charge Calculator
Bunker Charge
Calculator
Bunker Rate Repository
Container Weight
Calculator
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard
I can’t delegate anymore!!!
Now what do I do?
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard69
Enable LabsTriage and !Training
Two days on-site at your place of business.!! !First Day: Code Deep Dive!!• Code Review!• OO Design Review!• Testing Strategy Review!• Hands-on Pairing!
!Second Day: Training!!• Directly address issues identified on day one.
Get hands-on mentoring and help with your project.
Two days on-site hands-on.
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard70
Enable LabsSmall Code!WorkshopLearn how to write small beautiful code.
Smaller classes and systems are easier to build and maintain. From fundamentals through system design the Small Code Workshop teaches:!!• Method Design!• Naming Strategies!• Class Design!• Inheritance vs Composition!• Exploratory Testing!• Dependency Management!• Directed Refactorings!• … and more
Two to four days on-site hands-on.
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard71
Enable LabsOn the Project !TrainingWork hand in hand!with Enable Labs!staff on your projects.
You’ll work hands on with Enable Labs developers and trainers on your project using your code. Our developers will focus on teaching the fundamental principles of Small Code and good object oriented design while moving your project forward.!!You get to define your objectives for the two weeks and our team will focus on getting you and your team to a better place.!!We will focus on:!!• Immediate improvements to the design of your most difficult
components.!• Improving your test suite design to assure it’s delivering maximum
value.!• Teaching fundamentals of class naming, class design, separation
of concerns and more.
Level up in two weeks while working in your code base.
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard72
Enable LabsCustom App!DevelopmentFull life-cycle project development
From idea to deployment our team can bring your vision to life. Using a highly interactive process our team will work directly with your stake holders to create your applications.
Fully co-located cross functional team
We use:!!• Test Driven Development!• Client on-site!• Pair Programming
We work with:!!• Ruby!• Rails!• AngularJS!• iOS!• Android!• … and much more
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014
Enable Labs @mark_menard73
http://www.enablelabs.com/
866-895-8189
Enable Labs@mark_menard
LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014