Adventures
in
Refactoring Ben Lavender
About me and the talk •
@bhuga on github, twitter, TLDs
!
About me and the talk •
@bhuga on github, twitter, TLDs, but not snapchat
!
About me and the talk •
@bhuga on github, twitter, TLDs, but not snapchat
•
Work at GitHub on internal tools
About me and the talk •
@bhuga on github, twitter, TLDs, but not snapchat
•
Work at GitHub on internal tools
•
Has refactored a lot of “quick one offs” that turned important
if foo if bar if fum puts "yeah!" end end end !
-> !
return unless foo && bar && fum puts "yeah!"
What is Refactoring?
What is Refactoring? Changing code without changing behavior.
What is Refactoring? trying to
Changing code without ^ change behavior.
Bad reasons to Refactor
Bad reasons to Refactor
•
Increase consistency
x = "foo" y = 'bar' !
-> !
x = "foo" y = "bar"
Bad reasons to Refactor
•
Increase consistency
•
DRY things up
user = load_user widgets = user.widgets !
user = load_user widgets = user.widgets !
-> load_user_widgets
Bad reasons to Refactor •
Increase consistency
•
DRY things up
•
Improve abstractions
user.get_first_address_city !
-> user.addresses.first.city
Why were those
Bad
Reasons
!
?
Good software projects
Measure
Success
Measuring
Refactoring
Success
Measuring Success:
Maintain
Correctness
user.get_first_address_city !
-> user.addresses.first.city
user.get_first_address_city # "Toledo" !
-> user.addresses.first.city # "Akron"
Measuring Success:
The
Almighty
Diffstat
🔪 :hocho:
Measuring Success:
Improved
Test
Coverage
# 100% test coverage! if foo puts "foo!" end !
if bar explode if foo puts "bar!" end
Measuring Success:
Improved
Test
Expressability
describe "deployment" do context "when master needs merging" do context "when CI is green" do # test context "when CI is not green" do # test context "when branch is up-to-date" do context "when CI is green" do # test context "when CI is not green" do # test
class ChecklistItem ... end class MasterMerged < ChecklistItem ... end class CIGreen < ChecklistItem ... end
class Deployment def run_checklist! checklist.each do |checklist_item| # checklist_item is # a class like MasterMerged checklist_item.new(self).check
describe MasterMerged do it "merges master if required" it "does nothing if master merge not required"
!
describe CIGreen do it "adds a blocker if CI is not green" it "lets the deployment continue if CI is green"
Measuring Success:
Improved
Performance
Measuring Success:
Developer
Happiness
Measuring Success:
Developer
Happiness
😁
Focus on Metrics
Reasons
to
Refactor
Reason 1:
Developer
Happiness
Reason 2:
Increase
Performance
Reason 3:
Gain Confidence
for
Future Work
Reason 3:
Gain Confidence
for
Future Work
AKA paying off technical debt
Reason 4:
Developer Education
Part 2:
Techniques
Technique 1: Little
bits
of
Happy
if foo if bar if fum puts "yeah!" end end end !
-> !
return unless foo && bar && fum puts "yeah!"
user = load_user widgets = user.widgets !
user = load_user widgets = user.widgets !
-> load_user_widgets
user.get_first_address_city !
-> user.addresses.first.city
x = "foo" y = 'bar' !
-> !
x = "foo" y = "bar"
if some_pretty_long_condition ? "foo string" : "bar string" -> return "foo string" if some_pretty_long_condition "bar string"
object.some_method "foo", :fi_fo_fum => widget, :baz => fum, :free => fallin !
-> object.some_method "foo", :fi_fo_fum => widget, :baz => fum, :free => fallin
Get
a
Styleguide
!
$ go fmt
Technique 2:
Types
for
Verbs
class Account def transfer(amount, other_account) other_account.add(amount) self.subtract(amount) end end
class Account def transfer(amount, other_account, at) if at BackgroundJob.schedule(at, self, :transfer, amount, other_account) return end other_account.add(amount) self.subtract(amount) end end
def Transaction def initialize(@from, @to, @amount, @at) BackgroundJob.schedule(self, @at) end def run(from, to, amount) from.subtract(amount) to.add(amount) end end
class Account def transfer(amount, other_account, at) Transaction.new(self, other_account, amount, at) end end
transaction.successful? transaction.finished_at transaction.failure_reason
Technique 3:
Add
useful
Abstractions
pull.branch_valid? pull.branch_exists?
pull.branch.valid? pull.branch.exists?
pull.git_mergeable? pull.status_mergeable?
if pull.git_mergeable? show_merge_button else show_disabled_merge_button end
if pull.stable? show_merge_button else show_disabled_merge_button end
pull.merge_status.okay? pull.merge_status.git_okay? pull.merge_status.ci_okay?
class PullRequest def git_mergeable? end def status_mergeable? end def stable? git_mergeable? && status_mergeable? end end
class MergeState def initialize(pull) @pull = pull end def git_okay? end def ci_okay? end def stable? git_okay? && ci_okay? end end
Technique 4:
Remove
unused
Abstractions
class CampfireAdapter < ChatAdapter #... end !
class ChatAdapter def post(room, message) room = lookup_room(room) room.post(message) end end
chat = ChatAdapter.new(:campfire) chat.post(room, message)
class Campfire def self.post(room_id, message) RestClient.post "rooms/room_id", {:message => message } end end
Campfire.post room, message
Problems
Problem 1:
Existing
Bugs
Problem 2:
Performance
Changes
Problem 3:
Context
gitrpc.client do |c| c.fetch(file) end
gitrpc.client do |c| c.fetch(file, 4096) end
class TreeEntry def data ... end end
fetch(file) find_file_by_name(file) setup_gitrpc_client(…) rpc_client_connect(…) entry = rpc_get_current_tree(…) entry.data
module GitRPC def head_bytes(file) fetch(file).head_bytes end def tail_bytes(file) fetch(file).tail_bytes end end
module GitRPC def head_bytes(file) fetch(file, 4096).head_bytes end def tail_bytes(file) fetch(file).tail_bytes end end
Big
Refactorings
are a
Risk
Break
them
Down
Breaking it down 1:
Deprecate
module GitRPC # deprecated def fetch(file) end def fetch_with_limit(file, size) end end
module GitRPC def fetch(file) end deprecate :fetch, "Migrate to fetch_with_limit plz" def fetch_with_limit(file, size) end end
module GitRPC def head_bytes(file) fetch(file).head_bytes end def tail_bytes(file) fetch(file).tail_bytes end end
module GitRPC def fetch(file) end deprecate :fetch, "Migrate to fetch_with_limit plz" def tail_bytes(file) # Not deprecated here, really fetch(file).tail_bytes end end
Breaking it down 2:
Backwards
Compatible
Design
-------------- id | current | -------------- 1 | false | 2 | true | --------------
-------------- id | current | -------------- 1 | true | 2 | true | --------------
class Widget def current_widget find_by(:current => true) end -> def current_widget find_all(:current => true).last end end
!
->
Big
Refactorings
are a
Risk
Write
Tools
Backscatter
def data backscatter_trace 72 load_data end
Science
def get_widgets widgetify end
def get_widgets widgetify end !
def get_widgets_new load_widgets_from_database end
def get_widgets science "widgets.loading" do |e| e.use { get_widgets_old } e.try { get_widgets_new } end end
GitHub.subscribe "science.mismatch" do |payload| puts "old value returned #{payload.control}" puts "new value returned #{payload.candidate}" end
https://rubygems.org/gems/scientist
Part 3: Complaining
Editor-level facilities
Editor-level facilities
•
“Really good search/replace”
Editor-level facilities
•
“Really good search/replace”
•
“Extract resource”
Language Facilities
Language Facilities
•
Java @deprecate annotation
/** * @deprecated As of release 1.3, replaced by {@link #getPreferredSize()} */ @Deprecated public Dimension preferredSize() { return getPreferredSize(); }
Language Facilities
•
Java @deprecate annotation
•
C deprecation pragma
[[deprecated("Replaced by bar")]] void foo(int);
Compile-time deprecation sucks
$x = 1 | 2 x == 1 # true x == 2 # true $x = $x * 3 x == 3 && x == 6 # true
Example: nothing!
Go refactor I’m all done