Slides from my presentation to Alliants Coding Club (www.alliants.com), including the dojo on the Gilded Rose Kata, my thoughts as I went through, and some thoughts on working with Legacy / other peoples code.
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
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
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
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.
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
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
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.
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
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