strong duck type driven development
TRANSCRIPT
John Cinnamond // panagile ltd
Strong Duck Type Driven Development
strong duck type driven development
Let’s build a duck
How do you build a duck?
What is a duck?
Let’s deconstruct a duck
quacky bit
walking bit
floaty bit
flying bit
quacky bitwalking bitfloaty bitflying bit
This is a pretty rubbish duck
too much negative space
This is still a pretty rubbish duck
The bits don’t fittogether very well
floaty bit
This is still a pretty rubbish duck
quacky bit
walking bit
floaty bit
flying bit
quacky bit
this is worse
floaty bit
something isn’t quite right
I changed the quacky bit
I was required to change the floaty bit too
quacky bit
neck bit
brain bit eye bit
floaty bit
this is going to
hurt
floaty bit
We focus on the bits we’re building
We don’t think about how the bits fit together
this causes
problems
John Cinnamond // panagile ltd
Strong Duck Type Driven Development
Let’s build a duck
class QuackyBit
def quack puts “quack” end
end
class QuackyBit
def soft_quack puts “quack” end
end
class QuackyBit
def soft_quack puts “quack” end
def loud_quack puts “QUACK” end
end
bundle exec rspec ..........................................................................
Finished in 43.32027 seconds 73 examples, 0 failures
bundle exec rspec ..........................................................................
Finished in 43.32027 seconds 73 examples, 0 failures
wat?
either
the code is not used
or
the tests are wrong
class DuckBrain
def say_hello if other_duck.nearby? @quacky_bit.quack end end
end
class DuckBrain
def say_hello if other_duck.nearby? @quacky_bit.quack end end
end
it “greets other ducks” do expect(quacky_bit).to receive(:quack)
duck_brain.say_hello end
it “greets other ducks” do expect(quacky_bit).to receive(:quack)
duck_brain.say_hello end
How would we write it in Go?
type DuckBrain struct { quackyBit Quacker }
func (b DuckBrain) sayHello() { b.quackyBit.quack() }
type DuckBrain struct { quackyBit Quacker }
func (b DuckBrain) sayHello() { b.quackyBit.quack() }
type Quacker interface { quack() }
type Beak struct { }
func (b Beak) quack() { fmt.Println(“quack”) }
beak := Beak{}
brain := DuckBrain.new { quackyBit: beak, }
brain.sayHello()
type Beak struct { }
func (b Beak) quack() { fmt.Println(“quack”) }
type Beak struct { }
func (b Beak) softQuack() { fmt.Println(“quack”) }
type Beak struct { }
func (b Beak) softQuack() { fmt.Println(“quack”) }
func (b Beak) loudQuack() { fmt.Println(“QUACK”) }
./duck.go:4:
cannot use beak (type Beak) as type Quacker in field value:
Beak does not implement Quacker (missing quack method)
type Quacker interface { softQuack() loudQuack() }
./duck_brain.go:6:
db.quackyBit.quack undefined
(type Quacker has no field or method quack)
type DuckBrain struct { quackyBit Quacker }
func (b DuckBrain) sayHello() { b.quackyBit.quack() }
The compiler tells me when I break stuff
The compiler hints about how to fix it
DuckBrain knows nothing about Beak
Beak knows nothing about DuckBrain
Interfaces are no longer intangible
Hey Jakub
wouldn’t it be great if we had
interfaces in ruby?
in rubythis is easy
just write it
word
class DuckBrain
def initialize(quacky_bit) @quacky_bit = quacky_bit end
end
class DuckBrain
def initialize(quacky_bit) if !quacky_bit.respond_to?(:quack) raise InterfaceError end @quacky_bit = quacky_bit end
end
We just invented NoMethodError
There are contracts between objects
One object promises to provide some methods
The other promises to only use these methods
require 'lawyer'
class Quacker < Lawyer::Contract
end
require 'lawyer'
class Quacker < Lawyer::Contract confirm :quack => 0 end
class QuackyBit
def quack puts “quack” end
end
class QuackyBit
def quack puts “quack” end
end
QuackyBit.implements(Quacker)
class QuackyBit
def soft_quack puts “quack” end
end
QuackyBit.implements(Quacker)
class QuackyBit
def soft_quack puts “quack” end
def loud_quack puts “QUACK” end
end
QuackyBit.implements(Quacker)
QuackyBit does not implement <Quacker>
QuackyBit does not implement <Quacker>
(Lawyer::BrokenContract) (1 method missing) [missing] quack
class Quacker < Lawyer::Contract confirm :quack => 0 end
class Quacker < Lawyer::Contract confirm :soft_quack => 0 confirm :loud_quack => 0 end
it “greets other ducks” do
end
it “greets other ducks” do
duck_brain.say_hello expect(<something>). to have_received(:quack) end
it “greets other ducks” do brain = DuckBrain.new(quacker)
duck_brain.say_hello expect(quacker). to have_received(:quack) end
it “greets other ducks” do quacker = contract_double( Quacker )
brain = DuckBrain.new(quacker)
duck_brain.say_hello expect(quacker). to have_received(:quack) end
it “greets other ducks” do quacker = contract_double( Quacker )
brain = DuckBrain.new(quacker)
duck_brain.say_hello expect(quacker). to have_received(:quack) end
Failure/Error: duck_brain.say_hello
Double "Quacker" received unexpected message :quack with (no args)
The compiler tells me when I break stuff
toolchain
This is still ruby
This is just duck typing
But it’s strong duck typing
The toolchain helps enforce the types
We check the type before runtime
We have timeto fix type problems
Is this actually useful?
Initially I was
🙋
“I have more confidence that my production code won’t break”
I’ve only seen interface problems a few times…
…in 8 years of writing ruby code
I’ve solved the wrong problem
#fml
The compiler hints about how to fix it
The object don’t know about each other
Interfaces are no longer intangible
John Cinnamond // panagile ltd
Strong Duck Type Driven Development
Let’s build a duck
Visual Cortex
Brain
Lungs Tongue Beak
duck nearby
describe “Duck.new” do
it “creates a VisualCortex” do
end
end
describe “Duck.new” do
it “creates a VisualCortex” do expect { Duck.new }
end
end
describe “Duck.new” do
it “creates a VisualCortex” do expect { Duck.new }.to create(VisualCortex)
end
end
describe “Duck.new” do
it “creates a VisualCortex” do expect { Duck.new }.to create(VisualCortex). with(no_args) end
end
class Duck
end
class Duck
def initialize VisualCortex.new end
end
Visual Cortex
Brain
duck nearby
it “notifies the brain” do
end
it “notifies the brain” do visual_cortex = VisualCortex.new() visual_cortex.run
end
it “notifies the brain” do visual_cortex = VisualCortex.new() visual_cortex.run
expect(brain). to have_received(:duck_nearby) end
class VisualCortex
def run brain.duck_nearby end
end
class VisualCortex
def run brain.duck_nearby end
end
it “notifies the brain” do visual_cortex = VisualCortex.new() visual_cortex.run
expect(brain). to have_received(:duck_nearby) end
it “notifies the brain” do visual_cortex = VisualCortex.new(brain) visual_cortex.run
expect(brain). to have_received(:duck_nearby) end
it “notifies the brain” do brain = ?
visual_cortex = VisualCortex.new(brain) visual_cortex.run
expect(brain). to have_received(:duck_nearby) end
it “notifies the brain” do brain = contract_double(
)
visual_cortex = VisualCortex.new(brain) visual_cortex.run
expect(brain). to have_received(:duck_nearby) end
it “notifies the brain” do brain = contract_double( Contracts::Greeter )
visual_cortex = VisualCortex.new(brain) visual_cortex.run
expect(brain). to have_received(:duck_nearby) end
module Contracts class Greeter < Lawyer::Contract end end
module Contracts class Greeter < Lawyer::Contract confirm :duck_nearby => 0 end end
it “notifies the brain” do brain = contract_double( Contracts::Greeter )
visual_cortex = VisualCortex.new(brain) visual_cortex.run
expect(brain). to have_received(:duck_nearby) end
it “notifies the brain” do brain = contract_double( Contracts::Greeter )
visual_cortex = VisualCortex.new(brain) visual_cortex.run
expect(brain). to have_received(:duck_nearby) end
class VisualCortex
def run brain.duck_nearby end
end
class VisualCortex
def initialize(brain) @brain = brain end
def run @brain.duck_nearby end
end
VisualCortex when there is duck nearby notifies the brain
Finished in 0.00082 seconds 1 example, 0 failures
We haven’t created the brain yet
describe “Duck.new” do
it “creates a VisualCortex” do expect { Duck.new }.to create(VisualCortex). with(no_args) end
end
describe “Duck.new” do
it “creates a VisualCortex” do expect { Duck.new }.to create(VisualCortex). with(no_args) end
end
ArgumentError: wrong number of arguments (0 for 1)
describe “Duck.new” do
it “creates a VisualCortex” do expect { Duck.new }.to create(VisualCortex). with(no_args) end
end
describe “Duck.new” do
it “creates a VisualCortex” do expect { Duck.new }.to create(VisualCortex). with(Contract::Greeter) end
end
class Duck
def initialize VisualCortex.new(?) end
end
describe “Duck.new” do
it “creates a VisualCortex” do expect { Duck.new }.to create(VisualCortex). with(Contract::Greeter) end
end
describe “Duck.new” do
it “creates a Brain” do expect { Duck.new }.to create(Brain). with(no_args) end
it “creates a VisualCortex” do expect { Duck.new }.to create(VisualCortex). with(Contract::Greeter) end
end
class Duck
def initialize VisualCortex.new(?) end
end
class Duck
def initialize brain = Brain.new VisualCortex.new(brain) end
end
class Brain end
Lawyer::BrokenContract: Brain does not implement<Contracts::Greeter> (1 method missing) [missing] duck_nearby
Lawyer::BrokenContract: Brain does not implement<Contracts::Greeter> (1 method missing) [missing] duck_nearby
describe Brain do
it “greats nearby ducks” do
end
end
describe Brain do
it “greats nearby ducks” do brain = Brain.new brain.duck_nearby
end
end
describe Brain do
it “greats nearby ducks” do brain = Brain.new brain.duck_nearby
expect(quacker).to receive(quack) end
end
describe Brain do
it “greats nearby ducks” do brain = Brain.new brain.duck_nearby
expect(quacker).to receive(quack) end
end
The tests tell us what to write next
Writing new code is easy
Change is hard
We should focus on making change easier
We use TDD to make refactoring easier
We use TDD to make refactoring objects easier
Contracts make refactoring messages easier
Think about the messages
Change the contract
Fix the failing specs
Change the messages between objects
module Contracts class Greeter < Lawyer::Contract confirm :duck_nearby => 0 end end
module Contracts class Greeter < Lawyer::Contract confirm :duck_nearby => [:distance] end end
Lawyer::BrokenContract:
Brain does not implement <Contracts::Greeter> (1 method with the wrong signature) [wrong signature] duck_nearby (missing [:distance])
Failure/Error: visual_cortex.run
Double "Contracts::Greeter" received :duck_nearby with unexpected arguments
expected: ({:distance=>AnyArgMatcher}) got: (no args)
Replace implementations
Replace implementations(as long as they implement the same interface)
class FrontalLobe def duck_nearby(distance:) end end
FrontalLobe.implements(Contracts::Greeter)
class Duck
def initialize brain = Brain.new VisualCortex.new(brain) end
end
class Duck
def initialize fl = FrontalLobe.new VisualCortex.new(fl) end
end
The tests tell us what to write next
We can trust the tests totell us what to write next
We focus on isolated parts of the system
Then we focus on messages
between parts of the system
– Alan Kay
The big idea is "messaging"
http://lists.squeakfoundation.org/pipermail/squeak-dev/1998-October/017019.html
Recap
Built a duck(badly)
Noticed accidental coupling…
…caused by ignoring the connections between objects
…caused by ignoring the messaging between objects
duck typing…Supercharged
…to focus on connections between objects
…to generate errors if we break those connections
Use errors to drive our development
Reduces accidental coupling
Avoidsball-of-mud code
🙋
(strong (duck type)) driven development
Attributions
Picture of Jakub Oboza used with permission and taken from http://lambdacu.be
The following images are all available under a CC Attribution 2.0 Generic licence. Many thanks to the original photographers for sharing these images.
‘~ Duck Dribble ~’ by Stuart Williams https://www.flickr.com/photos/viamoi/3336548665
‘Morgan 8’ Plus by Stewart Cambers https://www.flickr.com/photos/stewc/4392996817
‘Champion Bodybuilders in kuwait 2 June2010’ by Ra'ed Qutena https://www.flickr.com/photos/raedqutena/4665308585
‘duck pair walking’ by Sebastian Ziebell https://www.flickr.com/photos/zebel/2399225416