let's do some upfront design - windycityrails 2014

86
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

Upload: mark-menard

Post on 19-Jan-2015

374 views

Category:

Technology


0 download

DESCRIPTION

My talk from WindyCityRails 2014 in Chicago Sometimes a little time spent upfront on design is worth it. Just because we’re agile doesn’t mean no design upfront. Unfortunately some people have taken “working software over comprehensive documentation” to mean no documentation and no design. In this talk, Mark will present a method for evolving an object oriented design through tests, with a strong separation between collaborator classes and process classes. The emphasis will be on quickly driving toward a tested design before committing to functioning process code to save time commonly spent in the refactoring of operational code and test code. You’ll learn how to write better designed and tested code faster with less refactoring churn.

TRANSCRIPT

Page 1: Let's Do Some Upfront Design - WindyCityRails 2014

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

Page 2: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

Who likes TDD?

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 3: Let's Do Some Upfront Design - WindyCityRails 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

Page 4: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard5

Extract Method

Replace Method with Method Object

Move Method

Extract Class

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 5: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

A Tale of a Refactoring

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 6: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

# some_ruby_program -v -efoo

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 7: Let's Do Some Upfront Design - WindyCityRails 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

Page 8: Let's Do Some Upfront Design - WindyCityRails 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

Page 9: Let's Do Some Upfront Design - WindyCityRails 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

Page 10: Let's Do Some Upfront Design - WindyCityRails 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

Page 11: Let's Do Some Upfront Design - WindyCityRails 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

Page 12: Let's Do Some Upfront Design - WindyCityRails 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

Page 13: Let's Do Some Upfront Design - WindyCityRails 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

Page 14: Let's Do Some Upfront Design - WindyCityRails 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

Page 15: Let's Do Some Upfront Design - WindyCityRails 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

Page 16: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

Oooooof! !!

This class is getting big…

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 17: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

So… let’s extract a class and move some methods.

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 18: Let's Do Some Upfront Design - WindyCityRails 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

Page 19: Let's Do Some Upfront Design - WindyCityRails 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

Page 20: Let's Do Some Upfront Design - WindyCityRails 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

Page 21: Let's Do Some Upfront Design - WindyCityRails 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

Page 22: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

Ow, ow, ow, ow… ow!

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 23: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

The Extract Class and Move Method refactorings are VERY expensive.

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 24: Let's Do Some Upfront Design - WindyCityRails 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

Page 25: Let's Do Some Upfront Design - WindyCityRails 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

Page 26: Let's Do Some Upfront Design - WindyCityRails 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

Page 27: Let's Do Some Upfront Design - WindyCityRails 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

Page 28: Let's Do Some Upfront Design - WindyCityRails 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

Page 29: Let's Do Some Upfront Design - WindyCityRails 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

Page 30: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard34

class BooleanOption < Option ! def valid? true end ! def value !!raw_value end end

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 31: Let's Do Some Upfront Design - WindyCityRails 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

Page 32: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

All code is an impediment to change.

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 33: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

Including your tests!

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 34: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

Can we avoid this churn?

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 35: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

Do some upfront design!

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 36: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

But that’s not agile… right?

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 37: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

Sequence Diagrams!

Collaboration Diagrams!

Mental Experiments!

Playing with Code / Exploratory Tests

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 38: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

Container Cost Calculations

• Bunker Charge (fuel charge)!

• Taxes!

• Carrier Surcharges

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 39: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard43

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 40: Let's Do Some Upfront Design - WindyCityRails 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

Page 41: Let's Do Some Upfront Design - WindyCityRails 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

Page 42: Let's Do Some Upfront Design - WindyCityRails 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

Page 43: Let's Do Some Upfront Design - WindyCityRails 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

Page 44: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard46

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 45: Let's Do Some Upfront Design - WindyCityRails 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

Page 46: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

CoordinatorProcessor

Processor

Processor

Coordinator

Processor

Coordinator Coordinator

Processor

Processor

ProcessorProcessor

Processor

ProcessorCoordinators vs Processors

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 47: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

WhatHow

How

How

What

How

What What

How

How

HowHow

How

HowCoordinators vs Processors

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 48: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

Container Cost Calculations

• Bunker Charge (fuel charge)!

• Taxes!

• Carrier Surcharges

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 49: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard50

What How

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 50: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard51

Coordinator Processor

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 51: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

So, how do I build that without the churn?

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 52: Let's Do Some Upfront Design - WindyCityRails 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

Page 53: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard54

class ShipmentCostCalculator end

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 54: Let's Do Some Upfront Design - WindyCityRails 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

Page 55: Let's Do Some Upfront Design - WindyCityRails 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

Page 56: Let's Do Some Upfront Design - WindyCityRails 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

Page 57: Let's Do Some Upfront Design - WindyCityRails 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

Page 58: Let's Do Some Upfront Design - WindyCityRails 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

Page 59: Let's Do Some Upfront Design - WindyCityRails 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

Page 60: Let's Do Some Upfront Design - WindyCityRails 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

Page 61: Let's Do Some Upfront Design - WindyCityRails 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

Page 62: Let's Do Some Upfront Design - WindyCityRails 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

Page 63: Let's Do Some Upfront Design - WindyCityRails 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

Page 64: Let's Do Some Upfront Design - WindyCityRails 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

Page 65: Let's Do Some Upfront Design - WindyCityRails 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

Page 66: Let's Do Some Upfront Design - WindyCityRails 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

Page 67: Let's Do Some Upfront Design - WindyCityRails 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

Page 68: Let's Do Some Upfront Design - WindyCityRails 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

Page 69: Let's Do Some Upfront Design - WindyCityRails 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

Page 70: Let's Do Some Upfront Design - WindyCityRails 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

Page 71: Let's Do Some Upfront Design - WindyCityRails 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

Page 72: Let's Do Some Upfront Design - WindyCityRails 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

Page 73: Let's Do Some Upfront Design - WindyCityRails 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

Page 74: Let's Do Some Upfront Design - WindyCityRails 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

Page 75: Let's Do Some Upfront Design - WindyCityRails 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

Page 76: Let's Do Some Upfront Design - WindyCityRails 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

Page 77: Let's Do Some Upfront Design - WindyCityRails 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

Page 78: Let's Do Some Upfront Design - WindyCityRails 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

Page 79: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard66

Coordinator ?

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 80: Let's Do Some Upfront Design - WindyCityRails 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

Page 81: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard

I can’t delegate anymore!!!

Now what do I do?

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014

Page 82: Let's Do Some Upfront Design - WindyCityRails 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

Page 83: Let's Do Some Upfront Design - WindyCityRails 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

Page 84: Let's Do Some Upfront Design - WindyCityRails 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

Page 85: Let's Do Some Upfront Design - WindyCityRails 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

Page 86: Let's Do Some Upfront Design - WindyCityRails 2014

Enable Labs @mark_menard73

http://www.enablelabs.com/

[email protected]

866-895-8189

Enable Labs@mark_menard

LetsDoSomeUpfrontDesign-WindyCityRails2014 - September 4, 2014