SlideShare a Scribd company logo
1 of 88
THE TALE OF THE 
Gilded Rose 
(And working with legacy code) 
me@lee-jon.com 
@tmesis
Introduction
§ 
Lee-Jon 
me@lee-jon.com 
@tmesis 
“To gild refined gold, 
to paint the lily, 
to throw a perfume on the violet, 
to smooth the ice, 
or add another hue unto the 
rainbow, 
or with taper-light to seek the 
beauteous eye of heaven to garnish, 
is wasteful and ridiculous excess.” 
- King John, Shakespeare, iv 2
GILDED ROSE INN
Welcome to Gilded Rose Inn 
Hi and welcome to team Gilded Rose. 
As you know, we are a small inn with 
a prime location in a prominent city 
ran by a friendly innkeeper named 
Allison. We also buy and sell only the 
finest goods. Unfortunately, our 
goods are constantly degrading in 
quality as they approach their sell by 
date. We have a system in place that 
updates our inventory for us. It was 
developed by a no-nonsense type 
named Leeroy, who has moved on to 
new adventures. Your task is to add 
the new feature to our system so that 
we can begin selling a new category 
of items.
THE SYSTEM IS SIMPLE 
• All items have a Sell In value 
which denotes the number of 
days we have to sell the item 
• All items have a Quality value 
which denotes how valuable 
the item is 
• At the end of each day our 
system lowers both values for 
every item 
All of this is in README.md
Some tiny exceptions 
• All items have a Sell In value 
which denotes the number of 
days we have to sell the item 
• All items have a Quality value 
which denotes how valuable 
the item is 
• At the end of each day our 
system lowers both values for 
every item 
• Once the sell by date has passed, Quality 
degrades twice as fast 
• The Quality of an item is never negative 
• "Aged Brie" actually increases in Quality the 
older it gets 
• The Quality of an item is never more than 50 
• "Sulfuras", being a legendary item, never has to 
be sold or decreases in Quality 
• "Backstage passes", like aged brie, increases in 
Quality as it's SellIn value approaches; 
• Quality increases by 2 when there are 10 
days or less and by 3 when there are 5 days 
or less but 
• Quality drops to 0 after the concert 
All of this is in README.md
The requirement 
We are going to stock "Conjured" items: 
• "Conjured" items degrade in Quality twice as fast as 
normal items 
The RULES 
Feel free to make any changes to the UpdateQuality method and add any new code 
as long as everything still works correctly. However, do not alter the Item class or 
@items property as those belong to the goblin in the corner who will insta-rage and 
one-shot you as he doesn't believe in shared code ownership (you can make the 
UpdateQuality method and Items property static if you like, we'll cover for you). 
All of this is in README.md
Leerooooooooy! 
class 
GildedRose 
def 
initialize 
... 
end 
def 
update_quality 
for 
i 
in 
0..(@items.size-­‐1) 
if 
(@items[i].name 
!= 
"Aged 
Brie" 
&& 
@items[i].name 
!= 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert") 
(@items[i].quality 
> 
0) 
if 
if 
(@items[i].name 
!= 
"Sulfuras, 
Hand 
of 
Ragnaros") 
@items[i].quality 
= 
@items[i].quality 
-­‐ 
1 
end 
end 
else 
if 
(@items[i].quality 
< 
50) 
@items[i].quality 
= 
@items[i].quality 
+ 
1 
if 
(@items[i].name 
== 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert") 
if 
(@items[i].sell_in 
< 
11) 
if 
(@items[i].quality 
< 
50) 
@items[i].quality 
= 
@items[i].quality 
+ 
1 
end 
end 
if 
(@items[i].sell_in 
< 
6) 
if 
(@items[i].quality 
< 
50) 
@items[i].quality 
= 
@items[i].quality 
+ 
1 
end 
end 
end 
end 
end
Leerooooooooy! 
if 
(@items[i].name 
!= 
"Sulfuras, 
Hand 
of 
Ragnaros") 
@items[i].sell_in 
= 
@items[i].sell_in 
-­‐ 
1; 
end 
if 
(@items[i].sell_in 
< 
0) 
if 
(@items[i].name 
!= 
"Aged 
Brie") 
if 
(@items[i].name 
!= 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert") 
if 
(@items[i].quality 
> 
0) 
if 
(@items[i].name 
!= 
"Sulfuras, 
Hand 
of 
Ragnaros") 
@items[i].quality 
= 
@items[i].quality 
-­‐ 
1 
end 
end 
else 
@items[i].quality 
= 
@items[i].quality 
-­‐ 
@items[i].quality 
end 
else 
if 
(@items[i].quality 
< 
50) 
@items[i].quality 
= 
@items[i].quality 
+ 
1 
end 
end 
end 
end 
end 
end
ANALYSIS 
Flog: Flog shows you the 
most torturous code you 
wrote. The more painful 
the code, the higher the 
score. 
Flay: Flay analyzes ruby 
code for structural 
similarities. Differences in 
literal values, names, 
whitespace, and 
programming style are 
all ignored. 
> flog gilded_rose.rb 
175.5: flog total 
43.9: flog/method average 
155.1: GildedRose#update_quality 
> flay gilded_rose.rb 
Total score (lower is better) = 144 
1) IDENTICAL code found in :if (mass*2 = 96) 
gilded_rose.rb:21 
gilded_rose.rb:49 
2) Similar code found in :if (mass = 48) 
gilded_rose.rb:30 
gilded_rose.rb:35
<DOJO> 
> 
git 
clone 
git@github.com:lee-­‐jon/gilded_rose.git
MY APPROACH
1. GET BASIC TESTS 
AROUND THE CODE
LET uS TEST_HELPER OURSELVES 
# 
gilded_rose_spec_helper.rb 
class 
GildedRose 
def 
initialize(object=true) 
@items 
= 
[] 
if 
object 
== 
true 
@items 
<< 
Item.new("+5 
Dexterity 
Vest", 
10, 
20) 
@items 
<< 
Item.new("Aged 
Brie", 
2, 
0) 
@items 
<< 
Item.new("Elixir 
of 
the 
Mongoose", 
5, 
7) 
@items 
<< 
Item.new("Sulfuras, 
Hand 
of 
Ragnaros", 
0, 
80) 
@items 
<< 
Item.new("Backstage 
passes 
to 
a…”, 
15, 
20) 
@items 
<< 
Item.new("Conjured 
Mana 
Cake", 
3, 
6) 
else 
@items 
<< 
object 
end 
end 
end
WE CAN NOW BUILD TESTS 
... 
describe 
"Updating 
Normal 
Items" 
do 
describe 
"while 
in 
date" 
before 
do 
@item 
= 
Item.new("Normal 
Item", 
10, 
1) 
@gildedrose 
= 
GildedRose.new(@item) 
@gildedrose.update_quality 
end 
it 
"should 
decrease 
the 
sell 
in" 
do 
expect(@item.sell_in).to 
eq(9) 
end 
it 
"should 
decrease 
the 
quality" 
do 
expect(@item.quality).to 
eq(0) 
end 
it 
"should 
not 
decrease 
the 
quality 
below 
zero" 
do 
@gildedrose.update_quality 
expect(@item.quality).to 
eq(0) 
end 
end 
end
Adding expired items 
... 
describe 
"when 
expired" 
do 
before 
do 
@item 
= 
Item.new("Normal 
Item", 
0, 
10) 
@gildedrose 
= 
GildedRose.new(@item) 
@gildedrose.update_quality 
end 
it 
"should 
have 
a 
negative 
sell 
in" 
do 
expect(@item.sell_in).to 
eq(-­‐1) 
end 
it 
"should 
have 
decreased 
quality 
by 
2" 
do 
expect(@item.quality).to 
eq(8) 
end 
end 
end 
... 
end
Normal
WE CAN NOW BUILD MORE TESTS 
describe 
"for 
legendary 
items" 
do 
before 
do 
@item 
= 
Item.new("Sulfuras, 
Hand 
of 
Ragnaros", 
0, 
80) 
@gildedrose 
= 
GildedRose.new(@item) 
@gildedrose.update_quality 
end 
it 
"should 
not 
change 
sell 
in" 
do 
expect(@item.sell_in).to 
eq(0) 
end 
it 
"should 
not 
change 
quality" 
do 
expect(@item.quality).to 
eq(80) 
end 
end
AGED BRIE INCRESES IN QUALITY 
describe 
"for 
Aged 
Brie" 
do 
before 
do 
@item 
= 
Item.new("Aged 
Brie", 
2, 
0) 
@gildedrose 
= 
GildedRose.new(@item) 
@gildedrose.update_quality 
end 
it 
"should 
decrease 
the 
sell 
in" 
do 
expect(@item.sell_in).to 
eq(1) 
end 
it 
"should 
increase 
in 
quality" 
do 
expect(@item.quality).to 
eq(1) 
end 
it 
"should 
not 
increase 
beyond 
50" 
do 
51.times 
{ 
@gildedrose.update_quality 
} 
expect(@item.quality).to 
eq(50) 
end 
end
Normal, 
Legendary, 
Aged Brie 
I’m suspicious of something…
• Once the sell by date has passed, Quality degrades twice as fast 
• "Aged Brie" actually increases in Quality the older it gets 
WHAT HAPPENS WHEN BRIE 
PASSES ITS SELL_IN DATE? 
Does it degrade now? Twice as fast?
Lets try it 
describe 
"Expired 
Aged 
Brie" 
do 
before 
do 
@item 
= 
Item.new("Aged 
Brie", 
-­‐5, 
0) 
... 
end 
it 
"foo" 
do 
expect(@item.quality).to 
eq(1) 
end 
end
Is THIS RIGHT? 
• Once the sell by date has passed, Quality degrades twice as fast 
• "Aged Brie" actually increases in Quality the older it gets 
OR IS THE CODE RIGHT? 
• We’ll go, this time, with the code. But that may 
not always be the case. We just make the test 
pass by editing the test to match the behaviour.
BACKSTAGE PASSES 
describe 
"for 
Backstage 
Passes" 
do 
before 
do 
@item 
= 
Item.new("Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert", 
15, 
20) 
@gildedrose 
= 
GildedRose.new(@item) 
end 
describe 
"increases 
in 
quality" 
do 
it 
"should 
be 
by 
1 
when 
sell_in 
is 
15-­‐10" 
do 
update 
= 
5 
update.times 
{ 
@gildedrose.update_quality 
} 
expect(@item.quality).to 
eq(20 
+ 
update) 
end 
it 
"should 
be 
by 
2 
when 
sell_in 
< 
10" 
do 
5.times 
{ 
@gildedrose.update_quality 
} 
1.times 
{ 
@gildedrose.update_quality 
} 
expect(@item.quality).to 
eq(20 
+ 
(5*1) 
+ 
(1*2)) 
end 
it 
"should 
be 
by 
3 
when 
sell_in 
< 
5" 
do 
5.times 
{ 
@gildedrose.update_quality 
} 
# 
now 
at 
10 
days 
5.times 
{ 
@gildedrose.update_quality 
} 
# 
now 
at 
5 
days 
1.times 
{ 
@gildedrose.update_quality 
} 
# 
not 
at 
4 
days 
+ 
3 
expect(@item.quality).to 
eq(20 
+ 
(5*1) 
+ 
(5*2) 
+ 
(1*3)) 
end 
it 
"should 
go 
to 
0 
when 
sell_in 
is 
zero" 
do 
@item.sell_in.times 
{ 
@gildedrose.update_quality 
} 
@gildedrose.update_quality 
expect(@item.quality).to 
eq(0) 
end 
end 
end 
Some people may laugh at this format, 
but I wanted to make it more visual that it 
quality increases 5x by 1, 5x by 2, and 
one times by 3. 
11.times {} didn’t seem that clear to me 
that i was hitting a boundary. I could’ve 
used separate objects for each, but I 
wanted to keep it consistent with the Item 
object in production. 
Which reminds me…
OFF-by-1 
Although I don’t know 
what the code is doing 
yet, I’m suspicious about 
this error. Depending on 
what is calculated first, 
we could have different 
outcomes. (I can see in 
the middle of the IFs of 
DOOM the bit to reduce 
sell_in). The spec is 
unclear here. 
I’ll bung a load more 
tests in… 
sell_in = 0 
quality = 1 
sell_in = -1 
quality = 10 
sell_in = -1 
quality = 8 
sell_in = 0 
quality = 1 
sell_in = 0 
quality = 9 
sell_in = -1 
quality = 9
A LOAD MORE TESTS LATER…
<DOJO CHEATCODE> 
> 
git 
clone 
git@github.com:lee-­‐jon/gilded_rose.git 
> 
git 
checkout 
include-­‐tests 
> 
git 
checkout 
-­‐b 
<your-­‐branch-­‐foo>
2. FIRST REFACTOR
GOAL 
We’re going 
to start to 
hack at the 
code to 
understand 
it. But not 
necessary 
make it 
better.
GOAL 2 
“Always leave the 
campground cleaner 
than you found it.” 
- Boy Scout Rule
What I was thinking 
• get control of the IF statements 
• get control of the comparisons != vs ==. 
• find any common methods to get them out of there! 
• it doesn’t matter if the code gets worse provided I understand it 
more 
• don’t worry (yet) about hardcoded variables 
• don’t worry (yet) about syntax
IF a picture paints a thousand words 
The 3. IFs 
1. Not sure….! Think its unexpired logic? 
2. Decrease sell in - move this out! 
3. If expired logic 
There’s lots of conditionals for IF Legendary 
Also… for i in 0 is annoying me. Lets move to .each do
MOVE TO METHOD 
def 
update_quality 
... 
if 
(@items[i].name 
!= 
"Sulfuras, 
Hand 
of 
Ragnaros") 
@items[i].sell_in 
= 
@items[i].sell_in 
-­‐ 
1; 
end 
... 
end 
def 
update_quality 
... 
update_sell_in(@items[i]) 
... 
end 
private 
def 
update_sell_in(item) 
item.sell_in 
-­‐= 
1 
unless 
item.name 
== 
"Sulfuras, 
Hand 
of 
Ragnaros" 
end
s/foo/bar/g 
Move to .each style iterator. 
Tests pass… 
What about that first IF block…
update 
quality 
SKETCH IT 
If code is particularly 
difficult to visualise. 
Especially if its in 
multiple nested blocks 
or statements. If 
indentation is difficult 
you can annotate 
each block so you 
know where in the 
code things are 
happening. 
Is NOT Aged 
Brie 
True True True True 
Is NOT 
Backstage 
passes 
Is QUALITY 
greater than 
zero (0) 
is NOT 
Legendary 
Item 
Reduce quality by 
1 
is QUALITY 
less than 50 
Increase quality 
by 1 
Is it 
BACKSTAG 
E PASS? 
End 
Is SELL IN 
less than 11 
Increase quality 
by 1 
Is SELL IN 
less than 6 
Increase quality 
by 1 
is QUALITY 
less than 50 
is QUALITY 
less than 50 
True True 
True 
True 
True 
Making sense of the first if statement
update 
quality 
SKETCH IT 
Here we see the 
separate 
responsibilities of 
each part of that 
first IF block. The big 
blue stands out. And 
the green is similar, 
a quality bounds 
check followed by a 
decrease in value. 
Is NOT Aged 
Brie 
True True True True 
Is NOT 
Backstage 
passes 
Is QUALITY 
greater than 
zero (0) 
is NOT 
Legendary 
Item 
Reduce quality by 
1 
is QUALITY 
less than 50 
Increase quality 
by 1 
Is it 
BACKSTAG 
E PASS? 
End 
Is SELL IN 
less than 11 
Increase quality 
by 1 
Is SELL IN 
less than 6 
Increase quality 
by 1 
is QUALITY 
less than 50 
is QUALITY 
less than 50 
True True 
True 
True 
True 
Item Type 
Quality < 50 + incrementer 
Quality > 0 + decrementer 
Backstage Passes specific
Next steps: COnditionals 
1. Legendary items don’t update. Why are we 
continually testing for its type? Better - remove. 
2. We bounds check and then increment. We 
can extract this to two methods.
REMOVE LEGENDARY CONDITIONALS 
update_quality 
@items.each 
do 
|item| 
return 
def 
if 
item.name 
== 
"Sulfuras, 
Hand 
of 
Ragnaros" 
(item.name 
!= 
"Aged 
Brie" 
&& 
item.name 
!= 
“Backstage…") 
if 
if 
(item.quality 
> 
0) 
if 
(item.name 
!= 
"Sulfuras, 
Hand 
of 
Ragnaros") 
item.quality 
= 
item.quality 
-­‐ 
1 
end 
end 
else 
... 
update_sell_in(item) 
... 
end 
end 
private 
def 
update_sell_in(item) 
item.sell_in 
-­‐= 
1 
unless 
item.name 
== 
"Sulfuras, 
Hand 
of 
Ragnaros" 
end
? Better run the tests 
update_quality 
@items.each 
do 
|item| 
return 
def 
if 
item.name 
== 
"Sulfuras, 
Hand 
of 
Ragnaros" 
(item.name 
!= 
"Aged 
Brie" 
&& 
item.name 
!= 
“Backstage…") 
if 
if 
(item.quality 
> 
0) 
item.quality 
= 
item.quality 
-­‐ 
1 
end 
else 
... 
update_sell_in(item) 
... 
end 
end 
private 
def 
update_sell_in(item) 
item.sell_in 
-­‐= 
1 
end
MOVE QUALITY METHODs 
(item.sell_in 
< 
0) 
if 
if 
(item.name 
!= 
"Aged 
Brie") 
if 
(item.name 
!= 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert") 
if 
(item.quality 
> 
0) 
item.quality 
= 
item.quality 
-­‐ 
1 
end 
else 
item.quality 
= 
item.quality 
-­‐ 
item.quality 
end 
else 
if 
(item.quality 
< 
50) 
item.quality 
= 
item.quality 
+ 
1 
end 
end 
end 
def 
increase_quality(item) 
item.quality 
+= 
1 
end 
def 
decrease_quality(item) 
item.quality 
-­‐= 
1 
end
REMOVe DUPLICATE QUALITY CONDITIONALS 
update_quality 
@items.each 
do 
|item| 
... 
if 
def 
(item.name 
!= 
"Aged 
Brie" 
&& 
item.name 
!= 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert") 
if 
(item.quality 
> 
0) 
... 
else 
if 
(item.quality 
< 
50) 
... 
... 
if 
(item.quality 
< 
50) 
... 
end 
if 
(item.sell_in 
< 
6) 
if 
(item.quality 
< 
50) 
... 
... 
end 
... 
if 
(item.sell_in 
< 
0) 
if 
(item.name 
!= 
"Aged 
Brie") 
if 
(item.name 
!= 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert") 
if 
(item.quality 
> 
0) 
... 
end 
else 
item.quality 
= 
0 
end 
else 
if 
(item.quality 
< 
50) 
... 
end 
end 
end 
end 
end
23 passing tests… 
update_quality 
@items.each 
do 
|item| 
def 
... 
if 
(item.name 
!= 
"Aged 
Brie" 
&& 
item.name 
!= 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert") 
decrease_quality(item) 
else 
increase_quality(item) 
if 
(item.name 
== 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert") 
if 
(item.sell_in 
< 
11) 
increase_quality(item) 
end 
if 
(item.sell_in 
< 
6) 
increase_quality(item) 
end 
end 
end 
... 
(item.sell_in 
< 
0) 
if 
if 
(item.name 
!= 
"Aged 
Brie") 
if 
(item.name 
!= 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert") 
decrease_quality(item) 
else 
item.quality 
= 
0 
private 
end 
else 
increase_quality(item) 
def 
end 
end 
end 
end 
increase_quality(item) 
item.quality 
+= 
1 
if 
item.quality 
< 
50 
end 
def 
decrease_quality(item) 
item.quality 
-­‐= 
1 
if 
item.quality 
> 
0 
end
ANALYSIS 
How much 
simpler than 
before? 
Flog 
total: 
175.5 
Flog 
max: 
155.1 
Flay: 
144 
OK Pause. We’ve moved the update and conditional 
logic out of the update_quality method. Which has 
reduced the amount of conditionals, and lines. The 
method fits on screen! 
We’ve excluded Legendary items from that code and 
removed the conditionals. 
➜ gilded-rose git:(first-refactor) ✗ rspec gilded_rose_spec.rb 
....................... 
Finished in 0.00708 seconds (files took 0.12844 seconds to load) 
23 examples, 0 failures 
➜ gilded-rose git:(first-refactor) ✗ flog gilded_rose.rb 
69.4: flog total 
9.9: flog/method average 
42.6: GildedRose#update_quality gilded_rose.rb:15 
➜ gilded-rose git:(first-refactor) ✗ flay gilded_rose.rb 
Total score (lower is better) = 0 
➜ gilded-rose git:(first-refactor) ✗
3. SECOND REFACTOR
thoughts 
• We’ve been moving common methods & conditionals 
• We haven’t looked at the item.name conditionals. 
• Having if name != “Brie” in the first and last IF 
blocks seems like it needs refactoring…
Original 
case 
item.name 
when 
"Sulfuras, 
Hand 
of 
Ragnaros" 
return 
end 
if 
(item.name 
!= 
"Aged 
Brie" 
&& 
item.name 
!= 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert") 
decrease_quality(item) 
else 
increase_quality(item) 
if 
(item.name 
== 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert") 
if 
(item.sell_in 
< 
11) 
increase_quality(item) 
end 
if 
(item.sell_in 
< 
6) 
increase_quality(item) 
end 
end 
end
Pull into case statement 
case 
item.name 
when 
"Sulfuras, 
Hand 
of 
Ragnaros" 
return 
when 
"Aged 
Brie" 
increase_quality(item) 
when 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert" 
increase_quality(item) 
if 
(item.sell_in 
< 
11) 
increase_quality(item) 
end 
if 
(item.sell_in 
< 
6) 
increase_quality(item) 
end 
else 
decrease_quality(item) 
end
REFACTOR 
case 
item.name 
when 
"Sulfuras, 
Hand 
of 
Ragnaros" 
return 
when 
"Aged 
Brie" 
increase_quality(item) 
when 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert" 
increase_quality(item) 
increase_quality(item) 
if 
item.sell_in 
< 
11 
increase_quality(item) 
if 
item.sell_in 
< 
6 
else 
decrease_quality(item) 
end
NEXT PROBLEM 
I was going to then bring up the ‘expired’ conditionals. (Third 
block of IFs) Except the process order will change. 
1. increase / decrease quality 
2. decrease sell_in 
3. increase / decrease quality if sell_in < 0 
1. increase / decrease quality 
2. increase / decrease quality if sell_in < 0 
3. decrease sell_in 
So we’ll have to do that first…
I LOVE TESTS! 
Broke Legendary 
(decrease sell in 
before exclude) 
Broke boundaries 
on Backstage Passes
REFACTOR COMPLETE 
update_quality 
@items.each 
do 
|item| 
return 
def 
if 
item.name 
== 
"Sulfuras, 
Hand 
of 
Ragnaros" 
decrease_sell_in(item) 
case 
item.name 
when 
"Aged 
Brie" 
increase_quality(item) 
increase_quality(item) 
if 
item.sell_in 
< 
0 
when 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert" 
increase_quality(item) 
increase_quality(item) 
if 
item.sell_in 
< 
10 
increase_quality(item) 
if 
item.sell_in 
< 
5 
item.quality 
= 
0 
if 
item.sell_in 
< 
0 
else 
decrease_quality(item) 
decrease_quality(item) 
if 
item.sell_in 
< 
0 
end 
end 
end
ANALYSIS 
FLOG: 
0: 175.5 / 155.1 
1: 69.4 / 42.6 
2: 63.8 / 37.0 
TOTAL MAX 
Overall 
complexity is 
reduced, the 
largest method 
has reduced. 
37 is still a 
large score.
4. ARE WE DONE?
GOAL 
We’re going 
to start to 
hack at the 
code to 
understand 
it. But not 
necessary 
make it 
better.
? 
update_quality 
@items.each 
do 
|item| 
return 
def 
if 
item.name 
== 
"Sulfuras, 
Hand 
of 
Ragnaros" 
decrease_sell_in(item) 
case 
item.name 
when 
"Aged 
Brie" 
increase_quality(item) 
increase_quality(item) 
if 
item.sell_in 
< 
0 
when 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert" 
increase_quality(item) 
increase_quality(item) 
if 
item.sell_in 
< 
10 
increase_quality(item) 
if 
item.sell_in 
< 
5 
item.quality 
= 
0 
if 
item.sell_in 
< 
0 
when 
“Conjured 
Mana 
Cake” 
# 
Some 
code 
here 
else 
decrease_quality(item) 
decrease_quality(item) 
if 
item.sell_in 
< 
0 
end 
end 
end
WE VIOLATE THE 
PRINCIPLEs MAN
Single Responsibility 
Gilded Rose knows too much about Items: 
> increase_quality, decrease_quality 
Why can’t an Item know about how to 
update itself? 
BUT we cannot touch the Item class (but 
we can cheat yeah?)
Open / closed principle 
• Open for extension 
• Closed for modification 
To add “Conjured” Item we have to modify the 
GildedRose.update_quality method. 
Key smell to look for is if…else or case / switch 
statements
this is the ONLY code I want 
def 
update_quality 
@items.each 
do 
|item| 
item.update 
end 
end
Approach 
• It would be natural to subclass 
Item with NormalItem, 
LegendaryItem, and so forth. 
• But we cannot. So what can 
we do? 
• We could create an 
ItemUpdater class and 
delegate the resposibilty to 
that. However I prefer a slight 
cheat (that has its drawbacks 
too…)
Replace TYPE CODE WITH 
module EXTENSION 
Its not the only approach… discuss! 
LOOKUP…
INJECT THE MODULE INTO EACH OF THESE 
initialize 
@items 
= 
[] 
@items 
<< 
Item.new("+5 
Dexterity 
Vest", 
10, 
20) 
@items 
<< 
Item.new("Aged 
Brie", 
2, 
0) 
@items 
<< 
Item.new("Elixir 
of 
the 
Mongoose", 
5, 
7) 
@items 
<< 
Item.new("Sulfuras, 
Hand 
of 
Ragnaros", 
0, 
80) 
@items 
<< 
Item.new("Backstage 
passes 
to 
a 
TAFKAL80ETC 
def 
concert", 
15, 
20) 
@items 
<< 
Item.new("Conjured 
Mana 
Cake", 
3, 
6) 
end 
Each of these is a different type, but 
we can’t subclass because: 
1. Angry goblin 
2. We don’t know this is the actual 
interface 
Instead we will inject the right update 
module into each class
5. THIRD REFACTOR
STEP ONE 
Tell each NORMAL Item how to update itself
EXTEND INSTANCE module 
def 
initialize 
# 
... 
extend_items 
end 
def 
extend_items 
@items.each 
do 
|item| 
case 
item.name 
when 
"Aged 
Brie" 
|| 
"Sulfuras, 
Hand 
of 
Ragnaros" 
|| 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert" 
# 
Do 
nothing 
else 
item.extend(NormalUpdate) 
end 
end 
end
update module 
module 
Update 
def 
update 
update_sell_in 
update_quality 
end 
private 
def 
update_sell_in 
self.sell_in 
-­‐= 
1 
end 
def 
update_quality 
increment 
increment 
if 
self.sell_in 
< 
0 
end 
def 
increment 
end 
end
Normal update 
Change the incrementer 
module 
NormalUpdate 
include 
Update 
def 
increment 
self.quality 
+= 
-­‐1 
if 
self.quality 
!= 
0 
end 
end
Legendary update 
We could require but we don’t need to 
module 
LegendaryUpdate 
def 
update 
end 
end
INCREASING UPDATE (Brie) 
update_quality 
@items.each 
do 
|item| 
case 
def 
item.name 
when 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert" 
decrease_sell_in(item) 
increase_quality(item) 
increase_quality(item) 
if 
item.sell_in 
< 
10 
increase_quality(item) 
if 
item.sell_in 
< 
5 
item.quality 
= 
0 
if 
item.sell_in 
< 
0 
else 
item.update 
end 
end 
end 
private 
... 
def 
extend_items 
@items.each 
do 
|item| 
case 
item.name 
when 
"Aged 
Brie" 
item.extend(ImprovingUpdate) 
when 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert" 
# 
Do 
nothing 
when 
"Sulfuras, 
Hand 
of 
Ragnaros" 
item.extend(LegendaryUpdate) 
else 
item.extend(NormalUpdate) 
end 
end 
end 
As we remove 
logic this does all 
the work 
Brie now here
IMPROVING update 
Change the incrementer 
module 
ImprovingUpdate 
include 
Update 
def 
increment 
self.quality 
+= 
1 
if 
self.quality 
!= 
50 
end 
end
ticket update 
Use IncreasingUpdate’s incremented 
Change the logic of update_quality 
module 
TicketsUpdate 
include 
ImprovingUpdate 
def 
update_quality 
increment 
increment 
if 
self.sell_in 
< 
10 
increment 
if 
self.sell_in 
< 
5 
self.quality 
= 
0 
if 
self.sell_in 
< 
0 
end 
end
Remove config 
update_quality 
@items.each 
do 
|item| 
item.update 
end 
end 
private 
def 
def 
extend_items 
@items.each 
do 
|item| 
case 
item.name 
when 
"Aged 
Brie" 
UPDATERS 
= 
{"Aged 
Brie"=> 
ImprovingUpdate, 
item.extend(ImprovingUpdate) 
when 
"Sulfuras, 
Hand 
of 
Ragnaros”=> 
LegendaryUpdate, 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert”=> 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert" 
item.extend(TicketsUpdate) 
when 
"Sulfuras, 
Hand 
of 
Ragnaros" 
item.extend(LegendaryUpdate) 
else 
item.extend(NormalUpdate) 
end 
end 
end 
def 
extend_items 
@items.each 
do 
|item| 
item.extend(UPDATERS[item.name] 
|| 
NormalUpdate) 
end 
end 
TicketsUpdate 
}
Final code 
class 
GildedRose 
UPDATERS 
= 
{ 
"Aged 
Brie" 
=> 
ImprovingUpdate, 
"Sulfuras, 
Hand 
of 
Ragnaros" 
=> 
LegendaryUpdate, 
"Backstage 
passes 
to 
a 
TAFKAL80ETC 
concert" 
=> 
TicketsUpdate 
} 
def 
initialize 
... 
extend_items 
end 
def 
update_quality 
@items.each 
do 
|item| 
item.update 
end 
end 
private 
def 
extend_items 
@items.each 
do 
|item| 
item.extend( 
UPDATERS[item.name] 
|| 
NormalUpdate) 
end 
end 
end
Update 
update 
update_sell_in 
update_quality 
increment 
Normal 
Update 
increment 
Improving 
Update 
increment 
Ticket Update 
update_quality 
Legendary 
Update 
Update 
Conjured 
Update 
increment 
The design has been made easy
ANALYSIS 
FLOG: Total vs Update Quality 
0: 175.5 / 155.1 
1: 69.4 / 42.6 
2: 63.8 / 37.0 
3a: 32.3 / 2.5 
But now we have updater.rb… 
3b: 34.9 
So some total complexity increase 
through indirection, but no single 
method above 10, from 155.
6. ADD THE FAILING TESTS 
& PASS CODE
CONJURED ITEMS 
describe 
"for 
Conjured 
Items" 
do 
describe 
"within 
sell 
in 
date" 
do 
before 
do 
@item 
= 
Item.new("Conjured 
Mana 
Cake", 
10, 
6) 
@gildedrose 
= 
GildedRose.new(@item) 
@gildedrose.update_quality 
end 
it 
"should 
decrease 
twice 
as 
fast 
before 
sign 
in" 
do 
expect(@item.quality).to 
equal(4) 
end 
end 
describe 
"expired" 
do 
before 
do 
@item 
= 
Item.new("Conjured 
Mana 
Cake", 
0, 
6) 
@gildedrose 
= 
GildedRose.new(@item) 
@gildedrose.update_quality 
end 
it 
"should 
decrease 
twice 
as 
fast 
before 
sign 
in" 
do 
expect(@item.quality).to 
equal(2) 
end 
end 
describe 
"at 
the 
quality 
limit" 
do 
before 
do 
@item 
= 
Item.new("Conjured 
Mana 
Cake", 
10, 
1) 
@gildedrose 
= 
GildedRose.new(@item) 
@gildedrose.update_quality 
end 
it 
"should 
not 
exceed 
0" 
do 
expect(@item.quality).to 
equal(0) 
end 
end 
end
FAIL
ADD CODE 
UPDATERS 
= 
{ 
..., 
module 
ConjuredUpdate 
include 
Update 
def 
increment 
self.quality 
+= 
-­‐1 
if 
self.quality 
!= 
0 
self.quality 
+= 
-­‐1 
if 
self.quality 
!= 
0 
end 
end 
"Conjured 
Mana 
Cake" 
=> 
ConjuredUpdate 
}
PASS
THE LEGACY
THOUGHTS 
• We don’t know what is sending the message GR.update_quality. But something is. 
• Refactoring without tests is very dangerous. 
• If you can’t write tests try scratch refactoring 
• “Find the computational core” 
• Sketch the system 
• Extract methods 
• Remove duplication 
• Avoid design patterns and design changes until its first few refactors. 
• Getting to switch(case) statements could’ve been good enough for now? 
• Go have a chat with that Goblin.
DISCUSSION 
• What if the system doesn’t mirror the specification? 
• How responsible is item.extend(FooUpdate)?
WHO SAID I CHEATED? 
• branch alternative-refactor is another approach which doesn’t 
touch the Item class 
• Instead we use Updater.new.update(item) in place of 
item.update. 
• The Updater class finds the right updater strategy. 
• There are similar classes as the modules for each strategy. 
• This interface may be preferable depending on how you 
interpret the specifications (and how brave you are extending 
the item class with that Goblin).
THE TALE OF THE 
Gilded Rose 
Thanks for having me 
me@lee-jon.com 
@tmesis

More Related Content

Recently uploaded

Ahmed Motair CV April 2024 (Senior SW Developer)
Ahmed Motair CV April 2024 (Senior SW Developer)Ahmed Motair CV April 2024 (Senior SW Developer)
Ahmed Motair CV April 2024 (Senior SW Developer)Ahmed Mater
 
How to Track Employee Performance A Comprehensive Guide.pdf
How to Track Employee Performance A Comprehensive Guide.pdfHow to Track Employee Performance A Comprehensive Guide.pdf
How to Track Employee Performance A Comprehensive Guide.pdfLivetecs LLC
 
Automate your Kamailio Test Calls - Kamailio World 2024
Automate your Kamailio Test Calls - Kamailio World 2024Automate your Kamailio Test Calls - Kamailio World 2024
Automate your Kamailio Test Calls - Kamailio World 2024Andreas Granig
 
Software Project Health Check: Best Practices and Techniques for Your Product...
Software Project Health Check: Best Practices and Techniques for Your Product...Software Project Health Check: Best Practices and Techniques for Your Product...
Software Project Health Check: Best Practices and Techniques for Your Product...Velvetech LLC
 
Cloud Data Center Network Construction - IEEE
Cloud Data Center Network Construction - IEEECloud Data Center Network Construction - IEEE
Cloud Data Center Network Construction - IEEEVICTOR MAESTRE RAMIREZ
 
CRM Contender Series: HubSpot vs. Salesforce
CRM Contender Series: HubSpot vs. SalesforceCRM Contender Series: HubSpot vs. Salesforce
CRM Contender Series: HubSpot vs. SalesforceBrainSell Technologies
 
Catch the Wave: SAP Event-Driven and Data Streaming for the Intelligence Ente...
Catch the Wave: SAP Event-Driven and Data Streaming for the Intelligence Ente...Catch the Wave: SAP Event-Driven and Data Streaming for the Intelligence Ente...
Catch the Wave: SAP Event-Driven and Data Streaming for the Intelligence Ente...confluent
 
Introduction Computer Science - Software Design.pdf
Introduction Computer Science - Software Design.pdfIntroduction Computer Science - Software Design.pdf
Introduction Computer Science - Software Design.pdfFerryKemperman
 
Implementing Zero Trust strategy with Azure
Implementing Zero Trust strategy with AzureImplementing Zero Trust strategy with Azure
Implementing Zero Trust strategy with AzureDinusha Kumarasiri
 
Dealing with Cultural Dispersion — Stefano Lambiase — ICSE-SEIS 2024
Dealing with Cultural Dispersion — Stefano Lambiase — ICSE-SEIS 2024Dealing with Cultural Dispersion — Stefano Lambiase — ICSE-SEIS 2024
Dealing with Cultural Dispersion — Stefano Lambiase — ICSE-SEIS 2024StefanoLambiase
 
GOING AOT WITH GRAALVM – DEVOXX GREECE.pdf
GOING AOT WITH GRAALVM – DEVOXX GREECE.pdfGOING AOT WITH GRAALVM – DEVOXX GREECE.pdf
GOING AOT WITH GRAALVM – DEVOXX GREECE.pdfAlina Yurenko
 
Maximizing Efficiency and Profitability with OnePlan’s Professional Service A...
Maximizing Efficiency and Profitability with OnePlan’s Professional Service A...Maximizing Efficiency and Profitability with OnePlan’s Professional Service A...
Maximizing Efficiency and Profitability with OnePlan’s Professional Service A...OnePlan Solutions
 
EY_Graph Database Powered Sustainability
EY_Graph Database Powered SustainabilityEY_Graph Database Powered Sustainability
EY_Graph Database Powered SustainabilityNeo4j
 
How to submit a standout Adobe Champion Application
How to submit a standout Adobe Champion ApplicationHow to submit a standout Adobe Champion Application
How to submit a standout Adobe Champion ApplicationBradBedford3
 
SuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte Germany
SuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte GermanySuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte Germany
SuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte GermanyChristoph Pohl
 
Folding Cheat Sheet #4 - fourth in a series
Folding Cheat Sheet #4 - fourth in a seriesFolding Cheat Sheet #4 - fourth in a series
Folding Cheat Sheet #4 - fourth in a seriesPhilip Schwarz
 
MYjobs Presentation Django-based project
MYjobs Presentation Django-based projectMYjobs Presentation Django-based project
MYjobs Presentation Django-based projectAnoyGreter
 
KnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptx
KnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptxKnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptx
KnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptxTier1 app
 

Recently uploaded (20)

Ahmed Motair CV April 2024 (Senior SW Developer)
Ahmed Motair CV April 2024 (Senior SW Developer)Ahmed Motair CV April 2024 (Senior SW Developer)
Ahmed Motair CV April 2024 (Senior SW Developer)
 
How to Track Employee Performance A Comprehensive Guide.pdf
How to Track Employee Performance A Comprehensive Guide.pdfHow to Track Employee Performance A Comprehensive Guide.pdf
How to Track Employee Performance A Comprehensive Guide.pdf
 
Automate your Kamailio Test Calls - Kamailio World 2024
Automate your Kamailio Test Calls - Kamailio World 2024Automate your Kamailio Test Calls - Kamailio World 2024
Automate your Kamailio Test Calls - Kamailio World 2024
 
Software Project Health Check: Best Practices and Techniques for Your Product...
Software Project Health Check: Best Practices and Techniques for Your Product...Software Project Health Check: Best Practices and Techniques for Your Product...
Software Project Health Check: Best Practices and Techniques for Your Product...
 
Cloud Data Center Network Construction - IEEE
Cloud Data Center Network Construction - IEEECloud Data Center Network Construction - IEEE
Cloud Data Center Network Construction - IEEE
 
CRM Contender Series: HubSpot vs. Salesforce
CRM Contender Series: HubSpot vs. SalesforceCRM Contender Series: HubSpot vs. Salesforce
CRM Contender Series: HubSpot vs. Salesforce
 
Catch the Wave: SAP Event-Driven and Data Streaming for the Intelligence Ente...
Catch the Wave: SAP Event-Driven and Data Streaming for the Intelligence Ente...Catch the Wave: SAP Event-Driven and Data Streaming for the Intelligence Ente...
Catch the Wave: SAP Event-Driven and Data Streaming for the Intelligence Ente...
 
Introduction Computer Science - Software Design.pdf
Introduction Computer Science - Software Design.pdfIntroduction Computer Science - Software Design.pdf
Introduction Computer Science - Software Design.pdf
 
Implementing Zero Trust strategy with Azure
Implementing Zero Trust strategy with AzureImplementing Zero Trust strategy with Azure
Implementing Zero Trust strategy with Azure
 
Dealing with Cultural Dispersion — Stefano Lambiase — ICSE-SEIS 2024
Dealing with Cultural Dispersion — Stefano Lambiase — ICSE-SEIS 2024Dealing with Cultural Dispersion — Stefano Lambiase — ICSE-SEIS 2024
Dealing with Cultural Dispersion — Stefano Lambiase — ICSE-SEIS 2024
 
GOING AOT WITH GRAALVM – DEVOXX GREECE.pdf
GOING AOT WITH GRAALVM – DEVOXX GREECE.pdfGOING AOT WITH GRAALVM – DEVOXX GREECE.pdf
GOING AOT WITH GRAALVM – DEVOXX GREECE.pdf
 
Maximizing Efficiency and Profitability with OnePlan’s Professional Service A...
Maximizing Efficiency and Profitability with OnePlan’s Professional Service A...Maximizing Efficiency and Profitability with OnePlan’s Professional Service A...
Maximizing Efficiency and Profitability with OnePlan’s Professional Service A...
 
EY_Graph Database Powered Sustainability
EY_Graph Database Powered SustainabilityEY_Graph Database Powered Sustainability
EY_Graph Database Powered Sustainability
 
How to submit a standout Adobe Champion Application
How to submit a standout Adobe Champion ApplicationHow to submit a standout Adobe Champion Application
How to submit a standout Adobe Champion Application
 
SuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte Germany
SuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte GermanySuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte Germany
SuccessFactors 1H 2024 Release - Sneak-Peek by Deloitte Germany
 
Folding Cheat Sheet #4 - fourth in a series
Folding Cheat Sheet #4 - fourth in a seriesFolding Cheat Sheet #4 - fourth in a series
Folding Cheat Sheet #4 - fourth in a series
 
MYjobs Presentation Django-based project
MYjobs Presentation Django-based projectMYjobs Presentation Django-based project
MYjobs Presentation Django-based project
 
Hot Sexy call girls in Patel Nagar🔝 9953056974 🔝 escort Service
Hot Sexy call girls in Patel Nagar🔝 9953056974 🔝 escort ServiceHot Sexy call girls in Patel Nagar🔝 9953056974 🔝 escort Service
Hot Sexy call girls in Patel Nagar🔝 9953056974 🔝 escort Service
 
Advantages of Odoo ERP 17 for Your Business
Advantages of Odoo ERP 17 for Your BusinessAdvantages of Odoo ERP 17 for Your Business
Advantages of Odoo ERP 17 for Your Business
 
KnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptx
KnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptxKnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptx
KnowAPIs-UnknownPerf-jaxMainz-2024 (1).pptx
 

Featured

How Race, Age and Gender Shape Attitudes Towards Mental Health
How Race, Age and Gender Shape Attitudes Towards Mental HealthHow Race, Age and Gender Shape Attitudes Towards Mental Health
How Race, Age and Gender Shape Attitudes Towards Mental HealthThinkNow
 
AI Trends in Creative Operations 2024 by Artwork Flow.pdf
AI Trends in Creative Operations 2024 by Artwork Flow.pdfAI Trends in Creative Operations 2024 by Artwork Flow.pdf
AI Trends in Creative Operations 2024 by Artwork Flow.pdfmarketingartwork
 
PEPSICO Presentation to CAGNY Conference Feb 2024
PEPSICO Presentation to CAGNY Conference Feb 2024PEPSICO Presentation to CAGNY Conference Feb 2024
PEPSICO Presentation to CAGNY Conference Feb 2024Neil Kimberley
 
Content Methodology: A Best Practices Report (Webinar)
Content Methodology: A Best Practices Report (Webinar)Content Methodology: A Best Practices Report (Webinar)
Content Methodology: A Best Practices Report (Webinar)contently
 
How to Prepare For a Successful Job Search for 2024
How to Prepare For a Successful Job Search for 2024How to Prepare For a Successful Job Search for 2024
How to Prepare For a Successful Job Search for 2024Albert Qian
 
Social Media Marketing Trends 2024 // The Global Indie Insights
Social Media Marketing Trends 2024 // The Global Indie InsightsSocial Media Marketing Trends 2024 // The Global Indie Insights
Social Media Marketing Trends 2024 // The Global Indie InsightsKurio // The Social Media Age(ncy)
 
Trends In Paid Search: Navigating The Digital Landscape In 2024
Trends In Paid Search: Navigating The Digital Landscape In 2024Trends In Paid Search: Navigating The Digital Landscape In 2024
Trends In Paid Search: Navigating The Digital Landscape In 2024Search Engine Journal
 
5 Public speaking tips from TED - Visualized summary
5 Public speaking tips from TED - Visualized summary5 Public speaking tips from TED - Visualized summary
5 Public speaking tips from TED - Visualized summarySpeakerHub
 
ChatGPT and the Future of Work - Clark Boyd
ChatGPT and the Future of Work - Clark Boyd ChatGPT and the Future of Work - Clark Boyd
ChatGPT and the Future of Work - Clark Boyd Clark Boyd
 
Getting into the tech field. what next
Getting into the tech field. what next Getting into the tech field. what next
Getting into the tech field. what next Tessa Mero
 
Google's Just Not That Into You: Understanding Core Updates & Search Intent
Google's Just Not That Into You: Understanding Core Updates & Search IntentGoogle's Just Not That Into You: Understanding Core Updates & Search Intent
Google's Just Not That Into You: Understanding Core Updates & Search IntentLily Ray
 
Time Management & Productivity - Best Practices
Time Management & Productivity -  Best PracticesTime Management & Productivity -  Best Practices
Time Management & Productivity - Best PracticesVit Horky
 
The six step guide to practical project management
The six step guide to practical project managementThe six step guide to practical project management
The six step guide to practical project managementMindGenius
 
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...RachelPearson36
 
Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...
Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...
Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...Applitools
 
12 Ways to Increase Your Influence at Work
12 Ways to Increase Your Influence at Work12 Ways to Increase Your Influence at Work
12 Ways to Increase Your Influence at WorkGetSmarter
 

Featured (20)

How Race, Age and Gender Shape Attitudes Towards Mental Health
How Race, Age and Gender Shape Attitudes Towards Mental HealthHow Race, Age and Gender Shape Attitudes Towards Mental Health
How Race, Age and Gender Shape Attitudes Towards Mental Health
 
AI Trends in Creative Operations 2024 by Artwork Flow.pdf
AI Trends in Creative Operations 2024 by Artwork Flow.pdfAI Trends in Creative Operations 2024 by Artwork Flow.pdf
AI Trends in Creative Operations 2024 by Artwork Flow.pdf
 
Skeleton Culture Code
Skeleton Culture CodeSkeleton Culture Code
Skeleton Culture Code
 
PEPSICO Presentation to CAGNY Conference Feb 2024
PEPSICO Presentation to CAGNY Conference Feb 2024PEPSICO Presentation to CAGNY Conference Feb 2024
PEPSICO Presentation to CAGNY Conference Feb 2024
 
Content Methodology: A Best Practices Report (Webinar)
Content Methodology: A Best Practices Report (Webinar)Content Methodology: A Best Practices Report (Webinar)
Content Methodology: A Best Practices Report (Webinar)
 
How to Prepare For a Successful Job Search for 2024
How to Prepare For a Successful Job Search for 2024How to Prepare For a Successful Job Search for 2024
How to Prepare For a Successful Job Search for 2024
 
Social Media Marketing Trends 2024 // The Global Indie Insights
Social Media Marketing Trends 2024 // The Global Indie InsightsSocial Media Marketing Trends 2024 // The Global Indie Insights
Social Media Marketing Trends 2024 // The Global Indie Insights
 
Trends In Paid Search: Navigating The Digital Landscape In 2024
Trends In Paid Search: Navigating The Digital Landscape In 2024Trends In Paid Search: Navigating The Digital Landscape In 2024
Trends In Paid Search: Navigating The Digital Landscape In 2024
 
5 Public speaking tips from TED - Visualized summary
5 Public speaking tips from TED - Visualized summary5 Public speaking tips from TED - Visualized summary
5 Public speaking tips from TED - Visualized summary
 
ChatGPT and the Future of Work - Clark Boyd
ChatGPT and the Future of Work - Clark Boyd ChatGPT and the Future of Work - Clark Boyd
ChatGPT and the Future of Work - Clark Boyd
 
Getting into the tech field. what next
Getting into the tech field. what next Getting into the tech field. what next
Getting into the tech field. what next
 
Google's Just Not That Into You: Understanding Core Updates & Search Intent
Google's Just Not That Into You: Understanding Core Updates & Search IntentGoogle's Just Not That Into You: Understanding Core Updates & Search Intent
Google's Just Not That Into You: Understanding Core Updates & Search Intent
 
How to have difficult conversations
How to have difficult conversations How to have difficult conversations
How to have difficult conversations
 
Introduction to Data Science
Introduction to Data ScienceIntroduction to Data Science
Introduction to Data Science
 
Time Management & Productivity - Best Practices
Time Management & Productivity -  Best PracticesTime Management & Productivity -  Best Practices
Time Management & Productivity - Best Practices
 
The six step guide to practical project management
The six step guide to practical project managementThe six step guide to practical project management
The six step guide to practical project management
 
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
 
Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...
Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...
Unlocking the Power of ChatGPT and AI in Testing - A Real-World Look, present...
 
12 Ways to Increase Your Influence at Work
12 Ways to Increase Your Influence at Work12 Ways to Increase Your Influence at Work
12 Ways to Increase Your Influence at Work
 
ChatGPT webinar slides
ChatGPT webinar slidesChatGPT webinar slides
ChatGPT webinar slides
 

Tale of the Gilded Rose

  • 1. THE TALE OF THE Gilded Rose (And working with legacy code) me@lee-jon.com @tmesis
  • 3. § Lee-Jon me@lee-jon.com @tmesis “To gild refined gold, to paint the lily, to throw a perfume on the violet, to smooth the ice, or add another hue unto the rainbow, or with taper-light to seek the beauteous eye of heaven to garnish, is wasteful and ridiculous excess.” - King John, Shakespeare, iv 2
  • 5. Welcome to Gilded Rose Inn Hi and welcome to team Gilded Rose. As you know, we are a small inn with a prime location in a prominent city ran by a friendly innkeeper named Allison. We also buy and sell only the finest goods. Unfortunately, our goods are constantly degrading in quality as they approach their sell by date. We have a system in place that updates our inventory for us. It was developed by a no-nonsense type named Leeroy, who has moved on to new adventures. Your task is to add the new feature to our system so that we can begin selling a new category of items.
  • 6. THE SYSTEM IS SIMPLE • All items have a Sell In value which denotes the number of days we have to sell the item • All items have a Quality value which denotes how valuable the item is • At the end of each day our system lowers both values for every item All of this is in README.md
  • 7. Some tiny exceptions • All items have a Sell In value which denotes the number of days we have to sell the item • All items have a Quality value which denotes how valuable the item is • At the end of each day our system lowers both values for every item • Once the sell by date has passed, Quality degrades twice as fast • The Quality of an item is never negative • "Aged Brie" actually increases in Quality the older it gets • The Quality of an item is never more than 50 • "Sulfuras", being a legendary item, never has to be sold or decreases in Quality • "Backstage passes", like aged brie, increases in Quality as it's SellIn value approaches; • Quality increases by 2 when there are 10 days or less and by 3 when there are 5 days or less but • Quality drops to 0 after the concert All of this is in README.md
  • 8. The requirement We are going to stock "Conjured" items: • "Conjured" items degrade in Quality twice as fast as normal items The RULES Feel free to make any changes to the UpdateQuality method and add any new code as long as everything still works correctly. However, do not alter the Item class or @items property as those belong to the goblin in the corner who will insta-rage and one-shot you as he doesn't believe in shared code ownership (you can make the UpdateQuality method and Items property static if you like, we'll cover for you). All of this is in README.md
  • 9. Leerooooooooy! class GildedRose def initialize ... end def update_quality for i in 0..(@items.size-­‐1) if (@items[i].name != "Aged Brie" && @items[i].name != "Backstage passes to a TAFKAL80ETC concert") (@items[i].quality > 0) if if (@items[i].name != "Sulfuras, Hand of Ragnaros") @items[i].quality = @items[i].quality -­‐ 1 end end else if (@items[i].quality < 50) @items[i].quality = @items[i].quality + 1 if (@items[i].name == "Backstage passes to a TAFKAL80ETC concert") if (@items[i].sell_in < 11) if (@items[i].quality < 50) @items[i].quality = @items[i].quality + 1 end end if (@items[i].sell_in < 6) if (@items[i].quality < 50) @items[i].quality = @items[i].quality + 1 end end end end end
  • 10. Leerooooooooy! if (@items[i].name != "Sulfuras, Hand of Ragnaros") @items[i].sell_in = @items[i].sell_in -­‐ 1; end if (@items[i].sell_in < 0) if (@items[i].name != "Aged Brie") if (@items[i].name != "Backstage passes to a TAFKAL80ETC concert") if (@items[i].quality > 0) if (@items[i].name != "Sulfuras, Hand of Ragnaros") @items[i].quality = @items[i].quality -­‐ 1 end end else @items[i].quality = @items[i].quality -­‐ @items[i].quality end else if (@items[i].quality < 50) @items[i].quality = @items[i].quality + 1 end end end end end end
  • 11. ANALYSIS Flog: Flog shows you the most torturous code you wrote. The more painful the code, the higher the score. Flay: Flay analyzes ruby code for structural similarities. Differences in literal values, names, whitespace, and programming style are all ignored. > flog gilded_rose.rb 175.5: flog total 43.9: flog/method average 155.1: GildedRose#update_quality > flay gilded_rose.rb Total score (lower is better) = 144 1) IDENTICAL code found in :if (mass*2 = 96) gilded_rose.rb:21 gilded_rose.rb:49 2) Similar code found in :if (mass = 48) gilded_rose.rb:30 gilded_rose.rb:35
  • 12. <DOJO> > git clone git@github.com:lee-­‐jon/gilded_rose.git
  • 14. 1. GET BASIC TESTS AROUND THE CODE
  • 15. LET uS TEST_HELPER OURSELVES # gilded_rose_spec_helper.rb class GildedRose def initialize(object=true) @items = [] if object == true @items << Item.new("+5 Dexterity Vest", 10, 20) @items << Item.new("Aged Brie", 2, 0) @items << Item.new("Elixir of the Mongoose", 5, 7) @items << Item.new("Sulfuras, Hand of Ragnaros", 0, 80) @items << Item.new("Backstage passes to a…”, 15, 20) @items << Item.new("Conjured Mana Cake", 3, 6) else @items << object end end end
  • 16. WE CAN NOW BUILD TESTS ... describe "Updating Normal Items" do describe "while in date" before do @item = Item.new("Normal Item", 10, 1) @gildedrose = GildedRose.new(@item) @gildedrose.update_quality end it "should decrease the sell in" do expect(@item.sell_in).to eq(9) end it "should decrease the quality" do expect(@item.quality).to eq(0) end it "should not decrease the quality below zero" do @gildedrose.update_quality expect(@item.quality).to eq(0) end end end
  • 17. Adding expired items ... describe "when expired" do before do @item = Item.new("Normal Item", 0, 10) @gildedrose = GildedRose.new(@item) @gildedrose.update_quality end it "should have a negative sell in" do expect(@item.sell_in).to eq(-­‐1) end it "should have decreased quality by 2" do expect(@item.quality).to eq(8) end end end ... end
  • 19. WE CAN NOW BUILD MORE TESTS describe "for legendary items" do before do @item = Item.new("Sulfuras, Hand of Ragnaros", 0, 80) @gildedrose = GildedRose.new(@item) @gildedrose.update_quality end it "should not change sell in" do expect(@item.sell_in).to eq(0) end it "should not change quality" do expect(@item.quality).to eq(80) end end
  • 20. AGED BRIE INCRESES IN QUALITY describe "for Aged Brie" do before do @item = Item.new("Aged Brie", 2, 0) @gildedrose = GildedRose.new(@item) @gildedrose.update_quality end it "should decrease the sell in" do expect(@item.sell_in).to eq(1) end it "should increase in quality" do expect(@item.quality).to eq(1) end it "should not increase beyond 50" do 51.times { @gildedrose.update_quality } expect(@item.quality).to eq(50) end end
  • 21. Normal, Legendary, Aged Brie I’m suspicious of something…
  • 22. • Once the sell by date has passed, Quality degrades twice as fast • "Aged Brie" actually increases in Quality the older it gets WHAT HAPPENS WHEN BRIE PASSES ITS SELL_IN DATE? Does it degrade now? Twice as fast?
  • 23. Lets try it describe "Expired Aged Brie" do before do @item = Item.new("Aged Brie", -­‐5, 0) ... end it "foo" do expect(@item.quality).to eq(1) end end
  • 24. Is THIS RIGHT? • Once the sell by date has passed, Quality degrades twice as fast • "Aged Brie" actually increases in Quality the older it gets OR IS THE CODE RIGHT? • We’ll go, this time, with the code. But that may not always be the case. We just make the test pass by editing the test to match the behaviour.
  • 25. BACKSTAGE PASSES describe "for Backstage Passes" do before do @item = Item.new("Backstage passes to a TAFKAL80ETC concert", 15, 20) @gildedrose = GildedRose.new(@item) end describe "increases in quality" do it "should be by 1 when sell_in is 15-­‐10" do update = 5 update.times { @gildedrose.update_quality } expect(@item.quality).to eq(20 + update) end it "should be by 2 when sell_in < 10" do 5.times { @gildedrose.update_quality } 1.times { @gildedrose.update_quality } expect(@item.quality).to eq(20 + (5*1) + (1*2)) end it "should be by 3 when sell_in < 5" do 5.times { @gildedrose.update_quality } # now at 10 days 5.times { @gildedrose.update_quality } # now at 5 days 1.times { @gildedrose.update_quality } # not at 4 days + 3 expect(@item.quality).to eq(20 + (5*1) + (5*2) + (1*3)) end it "should go to 0 when sell_in is zero" do @item.sell_in.times { @gildedrose.update_quality } @gildedrose.update_quality expect(@item.quality).to eq(0) end end end Some people may laugh at this format, but I wanted to make it more visual that it quality increases 5x by 1, 5x by 2, and one times by 3. 11.times {} didn’t seem that clear to me that i was hitting a boundary. I could’ve used separate objects for each, but I wanted to keep it consistent with the Item object in production. Which reminds me…
  • 26. OFF-by-1 Although I don’t know what the code is doing yet, I’m suspicious about this error. Depending on what is calculated first, we could have different outcomes. (I can see in the middle of the IFs of DOOM the bit to reduce sell_in). The spec is unclear here. I’ll bung a load more tests in… sell_in = 0 quality = 1 sell_in = -1 quality = 10 sell_in = -1 quality = 8 sell_in = 0 quality = 1 sell_in = 0 quality = 9 sell_in = -1 quality = 9
  • 27. A LOAD MORE TESTS LATER…
  • 28. <DOJO CHEATCODE> > git clone git@github.com:lee-­‐jon/gilded_rose.git > git checkout include-­‐tests > git checkout -­‐b <your-­‐branch-­‐foo>
  • 30. GOAL We’re going to start to hack at the code to understand it. But not necessary make it better.
  • 31. GOAL 2 “Always leave the campground cleaner than you found it.” - Boy Scout Rule
  • 32. What I was thinking • get control of the IF statements • get control of the comparisons != vs ==. • find any common methods to get them out of there! • it doesn’t matter if the code gets worse provided I understand it more • don’t worry (yet) about hardcoded variables • don’t worry (yet) about syntax
  • 33. IF a picture paints a thousand words The 3. IFs 1. Not sure….! Think its unexpired logic? 2. Decrease sell in - move this out! 3. If expired logic There’s lots of conditionals for IF Legendary Also… for i in 0 is annoying me. Lets move to .each do
  • 34. MOVE TO METHOD def update_quality ... if (@items[i].name != "Sulfuras, Hand of Ragnaros") @items[i].sell_in = @items[i].sell_in -­‐ 1; end ... end def update_quality ... update_sell_in(@items[i]) ... end private def update_sell_in(item) item.sell_in -­‐= 1 unless item.name == "Sulfuras, Hand of Ragnaros" end
  • 35. s/foo/bar/g Move to .each style iterator. Tests pass… What about that first IF block…
  • 36. update quality SKETCH IT If code is particularly difficult to visualise. Especially if its in multiple nested blocks or statements. If indentation is difficult you can annotate each block so you know where in the code things are happening. Is NOT Aged Brie True True True True Is NOT Backstage passes Is QUALITY greater than zero (0) is NOT Legendary Item Reduce quality by 1 is QUALITY less than 50 Increase quality by 1 Is it BACKSTAG E PASS? End Is SELL IN less than 11 Increase quality by 1 Is SELL IN less than 6 Increase quality by 1 is QUALITY less than 50 is QUALITY less than 50 True True True True True Making sense of the first if statement
  • 37. update quality SKETCH IT Here we see the separate responsibilities of each part of that first IF block. The big blue stands out. And the green is similar, a quality bounds check followed by a decrease in value. Is NOT Aged Brie True True True True Is NOT Backstage passes Is QUALITY greater than zero (0) is NOT Legendary Item Reduce quality by 1 is QUALITY less than 50 Increase quality by 1 Is it BACKSTAG E PASS? End Is SELL IN less than 11 Increase quality by 1 Is SELL IN less than 6 Increase quality by 1 is QUALITY less than 50 is QUALITY less than 50 True True True True True Item Type Quality < 50 + incrementer Quality > 0 + decrementer Backstage Passes specific
  • 38. Next steps: COnditionals 1. Legendary items don’t update. Why are we continually testing for its type? Better - remove. 2. We bounds check and then increment. We can extract this to two methods.
  • 39. REMOVE LEGENDARY CONDITIONALS update_quality @items.each do |item| return def if item.name == "Sulfuras, Hand of Ragnaros" (item.name != "Aged Brie" && item.name != “Backstage…") if if (item.quality > 0) if (item.name != "Sulfuras, Hand of Ragnaros") item.quality = item.quality -­‐ 1 end end else ... update_sell_in(item) ... end end private def update_sell_in(item) item.sell_in -­‐= 1 unless item.name == "Sulfuras, Hand of Ragnaros" end
  • 40. ? Better run the tests update_quality @items.each do |item| return def if item.name == "Sulfuras, Hand of Ragnaros" (item.name != "Aged Brie" && item.name != “Backstage…") if if (item.quality > 0) item.quality = item.quality -­‐ 1 end else ... update_sell_in(item) ... end end private def update_sell_in(item) item.sell_in -­‐= 1 end
  • 41.
  • 42. MOVE QUALITY METHODs (item.sell_in < 0) if if (item.name != "Aged Brie") if (item.name != "Backstage passes to a TAFKAL80ETC concert") if (item.quality > 0) item.quality = item.quality -­‐ 1 end else item.quality = item.quality -­‐ item.quality end else if (item.quality < 50) item.quality = item.quality + 1 end end end def increase_quality(item) item.quality += 1 end def decrease_quality(item) item.quality -­‐= 1 end
  • 43. REMOVe DUPLICATE QUALITY CONDITIONALS update_quality @items.each do |item| ... if def (item.name != "Aged Brie" && item.name != "Backstage passes to a TAFKAL80ETC concert") if (item.quality > 0) ... else if (item.quality < 50) ... ... if (item.quality < 50) ... end if (item.sell_in < 6) if (item.quality < 50) ... ... end ... if (item.sell_in < 0) if (item.name != "Aged Brie") if (item.name != "Backstage passes to a TAFKAL80ETC concert") if (item.quality > 0) ... end else item.quality = 0 end else if (item.quality < 50) ... end end end end end
  • 44. 23 passing tests… update_quality @items.each do |item| def ... if (item.name != "Aged Brie" && item.name != "Backstage passes to a TAFKAL80ETC concert") decrease_quality(item) else increase_quality(item) if (item.name == "Backstage passes to a TAFKAL80ETC concert") if (item.sell_in < 11) increase_quality(item) end if (item.sell_in < 6) increase_quality(item) end end end ... (item.sell_in < 0) if if (item.name != "Aged Brie") if (item.name != "Backstage passes to a TAFKAL80ETC concert") decrease_quality(item) else item.quality = 0 private end else increase_quality(item) def end end end end increase_quality(item) item.quality += 1 if item.quality < 50 end def decrease_quality(item) item.quality -­‐= 1 if item.quality > 0 end
  • 45. ANALYSIS How much simpler than before? Flog total: 175.5 Flog max: 155.1 Flay: 144 OK Pause. We’ve moved the update and conditional logic out of the update_quality method. Which has reduced the amount of conditionals, and lines. The method fits on screen! We’ve excluded Legendary items from that code and removed the conditionals. ➜ gilded-rose git:(first-refactor) ✗ rspec gilded_rose_spec.rb ....................... Finished in 0.00708 seconds (files took 0.12844 seconds to load) 23 examples, 0 failures ➜ gilded-rose git:(first-refactor) ✗ flog gilded_rose.rb 69.4: flog total 9.9: flog/method average 42.6: GildedRose#update_quality gilded_rose.rb:15 ➜ gilded-rose git:(first-refactor) ✗ flay gilded_rose.rb Total score (lower is better) = 0 ➜ gilded-rose git:(first-refactor) ✗
  • 47. thoughts • We’ve been moving common methods & conditionals • We haven’t looked at the item.name conditionals. • Having if name != “Brie” in the first and last IF blocks seems like it needs refactoring…
  • 48. Original case item.name when "Sulfuras, Hand of Ragnaros" return end if (item.name != "Aged Brie" && item.name != "Backstage passes to a TAFKAL80ETC concert") decrease_quality(item) else increase_quality(item) if (item.name == "Backstage passes to a TAFKAL80ETC concert") if (item.sell_in < 11) increase_quality(item) end if (item.sell_in < 6) increase_quality(item) end end end
  • 49. Pull into case statement case item.name when "Sulfuras, Hand of Ragnaros" return when "Aged Brie" increase_quality(item) when "Backstage passes to a TAFKAL80ETC concert" increase_quality(item) if (item.sell_in < 11) increase_quality(item) end if (item.sell_in < 6) increase_quality(item) end else decrease_quality(item) end
  • 50. REFACTOR case item.name when "Sulfuras, Hand of Ragnaros" return when "Aged Brie" increase_quality(item) when "Backstage passes to a TAFKAL80ETC concert" increase_quality(item) increase_quality(item) if item.sell_in < 11 increase_quality(item) if item.sell_in < 6 else decrease_quality(item) end
  • 51. NEXT PROBLEM I was going to then bring up the ‘expired’ conditionals. (Third block of IFs) Except the process order will change. 1. increase / decrease quality 2. decrease sell_in 3. increase / decrease quality if sell_in < 0 1. increase / decrease quality 2. increase / decrease quality if sell_in < 0 3. decrease sell_in So we’ll have to do that first…
  • 52. I LOVE TESTS! Broke Legendary (decrease sell in before exclude) Broke boundaries on Backstage Passes
  • 53. REFACTOR COMPLETE update_quality @items.each do |item| return def if item.name == "Sulfuras, Hand of Ragnaros" decrease_sell_in(item) case item.name when "Aged Brie" increase_quality(item) increase_quality(item) if item.sell_in < 0 when "Backstage passes to a TAFKAL80ETC concert" increase_quality(item) increase_quality(item) if item.sell_in < 10 increase_quality(item) if item.sell_in < 5 item.quality = 0 if item.sell_in < 0 else decrease_quality(item) decrease_quality(item) if item.sell_in < 0 end end end
  • 54. ANALYSIS FLOG: 0: 175.5 / 155.1 1: 69.4 / 42.6 2: 63.8 / 37.0 TOTAL MAX Overall complexity is reduced, the largest method has reduced. 37 is still a large score.
  • 55. 4. ARE WE DONE?
  • 56. GOAL We’re going to start to hack at the code to understand it. But not necessary make it better.
  • 57. ? update_quality @items.each do |item| return def if item.name == "Sulfuras, Hand of Ragnaros" decrease_sell_in(item) case item.name when "Aged Brie" increase_quality(item) increase_quality(item) if item.sell_in < 0 when "Backstage passes to a TAFKAL80ETC concert" increase_quality(item) increase_quality(item) if item.sell_in < 10 increase_quality(item) if item.sell_in < 5 item.quality = 0 if item.sell_in < 0 when “Conjured Mana Cake” # Some code here else decrease_quality(item) decrease_quality(item) if item.sell_in < 0 end end end
  • 58. WE VIOLATE THE PRINCIPLEs MAN
  • 59. Single Responsibility Gilded Rose knows too much about Items: > increase_quality, decrease_quality Why can’t an Item know about how to update itself? BUT we cannot touch the Item class (but we can cheat yeah?)
  • 60. Open / closed principle • Open for extension • Closed for modification To add “Conjured” Item we have to modify the GildedRose.update_quality method. Key smell to look for is if…else or case / switch statements
  • 61. this is the ONLY code I want def update_quality @items.each do |item| item.update end end
  • 62. Approach • It would be natural to subclass Item with NormalItem, LegendaryItem, and so forth. • But we cannot. So what can we do? • We could create an ItemUpdater class and delegate the resposibilty to that. However I prefer a slight cheat (that has its drawbacks too…)
  • 63. Replace TYPE CODE WITH module EXTENSION Its not the only approach… discuss! LOOKUP…
  • 64. INJECT THE MODULE INTO EACH OF THESE initialize @items = [] @items << Item.new("+5 Dexterity Vest", 10, 20) @items << Item.new("Aged Brie", 2, 0) @items << Item.new("Elixir of the Mongoose", 5, 7) @items << Item.new("Sulfuras, Hand of Ragnaros", 0, 80) @items << Item.new("Backstage passes to a TAFKAL80ETC def concert", 15, 20) @items << Item.new("Conjured Mana Cake", 3, 6) end Each of these is a different type, but we can’t subclass because: 1. Angry goblin 2. We don’t know this is the actual interface Instead we will inject the right update module into each class
  • 66. STEP ONE Tell each NORMAL Item how to update itself
  • 67. EXTEND INSTANCE module def initialize # ... extend_items end def extend_items @items.each do |item| case item.name when "Aged Brie" || "Sulfuras, Hand of Ragnaros" || "Backstage passes to a TAFKAL80ETC concert" # Do nothing else item.extend(NormalUpdate) end end end
  • 68.
  • 69. update module module Update def update update_sell_in update_quality end private def update_sell_in self.sell_in -­‐= 1 end def update_quality increment increment if self.sell_in < 0 end def increment end end
  • 70. Normal update Change the incrementer module NormalUpdate include Update def increment self.quality += -­‐1 if self.quality != 0 end end
  • 71. Legendary update We could require but we don’t need to module LegendaryUpdate def update end end
  • 72. INCREASING UPDATE (Brie) update_quality @items.each do |item| case def item.name when "Backstage passes to a TAFKAL80ETC concert" decrease_sell_in(item) increase_quality(item) increase_quality(item) if item.sell_in < 10 increase_quality(item) if item.sell_in < 5 item.quality = 0 if item.sell_in < 0 else item.update end end end private ... def extend_items @items.each do |item| case item.name when "Aged Brie" item.extend(ImprovingUpdate) when "Backstage passes to a TAFKAL80ETC concert" # Do nothing when "Sulfuras, Hand of Ragnaros" item.extend(LegendaryUpdate) else item.extend(NormalUpdate) end end end As we remove logic this does all the work Brie now here
  • 73. IMPROVING update Change the incrementer module ImprovingUpdate include Update def increment self.quality += 1 if self.quality != 50 end end
  • 74. ticket update Use IncreasingUpdate’s incremented Change the logic of update_quality module TicketsUpdate include ImprovingUpdate def update_quality increment increment if self.sell_in < 10 increment if self.sell_in < 5 self.quality = 0 if self.sell_in < 0 end end
  • 75. Remove config update_quality @items.each do |item| item.update end end private def def extend_items @items.each do |item| case item.name when "Aged Brie" UPDATERS = {"Aged Brie"=> ImprovingUpdate, item.extend(ImprovingUpdate) when "Sulfuras, Hand of Ragnaros”=> LegendaryUpdate, "Backstage passes to a TAFKAL80ETC concert”=> "Backstage passes to a TAFKAL80ETC concert" item.extend(TicketsUpdate) when "Sulfuras, Hand of Ragnaros" item.extend(LegendaryUpdate) else item.extend(NormalUpdate) end end end def extend_items @items.each do |item| item.extend(UPDATERS[item.name] || NormalUpdate) end end TicketsUpdate }
  • 76. Final code class GildedRose UPDATERS = { "Aged Brie" => ImprovingUpdate, "Sulfuras, Hand of Ragnaros" => LegendaryUpdate, "Backstage passes to a TAFKAL80ETC concert" => TicketsUpdate } def initialize ... extend_items end def update_quality @items.each do |item| item.update end end private def extend_items @items.each do |item| item.extend( UPDATERS[item.name] || NormalUpdate) end end end
  • 77. Update update update_sell_in update_quality increment Normal Update increment Improving Update increment Ticket Update update_quality Legendary Update Update Conjured Update increment The design has been made easy
  • 78. ANALYSIS FLOG: Total vs Update Quality 0: 175.5 / 155.1 1: 69.4 / 42.6 2: 63.8 / 37.0 3a: 32.3 / 2.5 But now we have updater.rb… 3b: 34.9 So some total complexity increase through indirection, but no single method above 10, from 155.
  • 79. 6. ADD THE FAILING TESTS & PASS CODE
  • 80. CONJURED ITEMS describe "for Conjured Items" do describe "within sell in date" do before do @item = Item.new("Conjured Mana Cake", 10, 6) @gildedrose = GildedRose.new(@item) @gildedrose.update_quality end it "should decrease twice as fast before sign in" do expect(@item.quality).to equal(4) end end describe "expired" do before do @item = Item.new("Conjured Mana Cake", 0, 6) @gildedrose = GildedRose.new(@item) @gildedrose.update_quality end it "should decrease twice as fast before sign in" do expect(@item.quality).to equal(2) end end describe "at the quality limit" do before do @item = Item.new("Conjured Mana Cake", 10, 1) @gildedrose = GildedRose.new(@item) @gildedrose.update_quality end it "should not exceed 0" do expect(@item.quality).to equal(0) end end end
  • 81. FAIL
  • 82. ADD CODE UPDATERS = { ..., module ConjuredUpdate include Update def increment self.quality += -­‐1 if self.quality != 0 self.quality += -­‐1 if self.quality != 0 end end "Conjured Mana Cake" => ConjuredUpdate }
  • 83. PASS
  • 85. THOUGHTS • We don’t know what is sending the message GR.update_quality. But something is. • Refactoring without tests is very dangerous. • If you can’t write tests try scratch refactoring • “Find the computational core” • Sketch the system • Extract methods • Remove duplication • Avoid design patterns and design changes until its first few refactors. • Getting to switch(case) statements could’ve been good enough for now? • Go have a chat with that Goblin.
  • 86. DISCUSSION • What if the system doesn’t mirror the specification? • How responsible is item.extend(FooUpdate)?
  • 87. WHO SAID I CHEATED? • branch alternative-refactor is another approach which doesn’t touch the Item class • Instead we use Updater.new.update(item) in place of item.update. • The Updater class finds the right updater strategy. • There are similar classes as the modules for each strategy. • This interface may be preferable depending on how you interpret the specifications (and how brave you are extending the item class with that Goblin).
  • 88. THE TALE OF THE Gilded Rose Thanks for having me me@lee-jon.com @tmesis