CS 683 Emerging Technologies: Embracing Change Spring Semester, 2001 Refactoring Intro |
||
---|---|---|
© 2001, All Rights Reserved, SDSU & Roger Whitney San Diego State University -- This page last updated 20-Mar-01 |
Refactoring Intro
We have code that looks like:
at: anInteger put: anObject (smallKey ~= largeKey) ifTrue: [(anInteger < smallKey) ifTrue: [self atLeftTree: anInteger put: anObject] ifFalse: [(smallKey = anInteger) ifTrue: [smallValue := anObject] ifFalse: [(anInteger < largeKey) ifTrue: [self atMiddleTree: anInteger put: anObject] ifFalse: [(largeKey = anInteger) ifTrue: [largeValue := anObject] ifFalse: [(largeKey < anInteger) ifTrue: [self atRightTree: anInteger put: anObject]]]]]] ifFalse:[self addNewKey: anInteger with: anObject].
Now what?
The Broken Window[1]
In inner cities some buildings are:
The Perfect Lawn
A visitor to an Irish castle asked the groundskeeper the secret of the beautiful lawn at the castle
The answer was:
Familiarity verse Comfort
Why don't more programmers/companies continually:
Refactoring
Refactoring is the modifying existing code without adding functionality
Changing existing code is dangerous
Sample Refactoring: Extract Method[2]
You have a code fragment that can be grouped together.
Turn the fragment into a method whose name explains the purpose of the method
Motivation
Short methods:
Mechanics
Mechanics - Continued
Example[3]
No Local Variables
Note I will use Fowler's convention of starting instance variables with "_" even though one can not do this is Squeak.
printOwing | outstanding | outstanding := 0.0. Transcript show: '********************'; cr; show: '***Customer Owes***'; cr; show: '********************'; cr. outstanding := _orders inject: 0 into: [:sum :each | sum + each]. Transcript show: 'Name: '; show: _name; cr; show: 'Amount: '; show: outstanding; cr.
Extracting the banner code we get:
printOwing | outstanding | outstanding := 0.0. self printBanner. outstanding := _orders inject: 0 into: [:sum :each | sum + each]. Transcript show: 'Name: '; show: _name; cr; show: 'Amount: '; show: outstanding; cr.
printBanner Transcript show: '********************'; cr; show: '***Customer Owes***'; cr; show: '********************'; cr
Examples: Using Local Variables
We can extract printDetails: to get
printOwing | outstanding | self printBanner. outstanding := _orders inject: 0 into: [:sum :each | sum + each]. self printDetails: outstanding
printDetails: aNumber Transcript show: 'Name: '; show: _name; cr; show: 'Amount: '; show: aNumber; cr.
Then we can extract outstanding to get:
printOwing self printBanner; printDetails: (self outstanding)
outstanding ^_orders inject: 0 into: [:sum :each | sum + each]
The text stops here, but the code could use more work
Using Add Parameter (275)
printBanner Transcript show: '********************'; cr; show: '***Customer Owes***'; cr; show: '********************'; cr
becomes:
printBannerOn: aStream aStream show: '********************'; cr; show: '***Customer Owes***'; cr; show: '********************'; cr
Similarly we do printDetails and printOwing
printOwingOn: aStream self printBannerOn: aStream. self printDetails: (self outstanding) on: aStream
Perhaps this should be called Replace Constant with Parameter
Simplifying Conditional Expressions
Decompose Conditional[4]
You have a complicated conditional (if-then-else) statement
Extract methods from the condition, then part and else parts
Example[5]
(date before: SummerStart) | (date after: SummerEnd) ifTrue:[ charge := quantity * _winterRate + _winterServiceCharge] ifFalse:[ charge := quantity + _summerRate]
becomes
(self notSummer: date) ifTrue: [ charge := self winterCharge: quantity] ifFalse: [ charge := self summerCharge: quantity]or the more Smalltalk like:
charge := (self notSummer: date) ifTrue: [self winterCharge: quantity] ifFalse: [self summerCharge: quantity]Each method ( notSummer, winterCharge, summerCharge) should be extracted and tested one at a time
Consolidate Conditional Expression[6]
You have a sequence of conditional tests with the same result
Combine them into a single conditional expression and extract it
Example
disabilityAmount _senority < 2 ifTrue: [^0]. _monthDisabled > 12 ifTrue: [^0]. self isPartTime ifTrue: [^0]. "compute the disabilty amount here"becomes
disabilityAmount (_senority < 2) | (_monthDisabled > 12) | (self isPartTime) ifTrue: [^0]. "compute the disabilty amount here"becomes:
disabilityAmount self isNotEliableForDisability ifTrue: [^0]. "compute the disabilty amount here"
Consolidate Duplicate Conditional Fragments[7]
The same fragment of code is in all branches of a conditional expression
Move it outside of the expression
Example
self isSpecialDeal ifTrue: [total := price * 0.95. self send] ifFalse: [total := price * 0.98. self send]
Consolidating we get:
self isSpecialDeal ifTrue:[total := price * 0.95] ifFalse:[total := price * 0.98]. self send
A more Smalltalk like version:
total := self isSpecialDeal ifTrue:[price * 0.95] ifFalse:[price * 0.98]. self send
Example continued
The text stops here, but the code could use more work
Use Introduce Explaining Variable (124) to improve readability
discountRate := self isSpecialDeal ifTrue:[0.95] ifFalse:[0.98]. total := price * discountRate. self sendUsing Replace Temp with Query (120) we get:
total := price * self discountRate. self send
Where we have
discountRate ^self isSpecialDeal ifTrue:[0.95] ifFalse:[0.98]In Java or C++ we could use Replace Magic Number with Symbolic Constant (204) on the 0.95 and 0.98 to improve readability
In Smalltalk we can use Introduce Explaining Variable (124) or Constant Method (Beck)
Remove Control Flag[8]
You have a variable that is acting as a control flag for a series of boolean expressions
Use a break or return instead
Example[9]
checkSecurity: people | found | found := false. 1 to: people size do: [:index | found ifFalse: [((people at: index) = 'Don') ifTrue: [self sendAlert(). found := true]. ((people at: index) = 'John') ifTrue: [self sendAlert(). found := true]]]
Becomes:
checkSecurity: people people containsMiscreant ifTrue:[self sendAlert()]
containsMiscreant 1 to: self size do: [:index | ((self at: index) = 'Don') ifTrue: [^true]. ((self at: index) = 'John') ifTrue: [^true]]. ^false
In Squeak the latter becomes:
containsMiscreant ^self anySatisfy: [:each | (each = 'Don') | (each = 'John')]
Replace Nested Conditional with Guard Clauses[10]
A method has conditional behavior that does not make clear the normal path of execution
Use guard clauses for all the special cases
Example
payAmount | result | _isDead ifTrue: [result := self deadAmount] ifFalse: [_isSeparated ifTrue:[result := self separatedAmount] ifFalse: [_isRetired ifTrue:[result := self retiredAmount] ifFalse:[result := self normalPayAmount]]]. ^ result
becomes
payAmount _isDead ifTrue: [^self deadAmount]. _isSeparated ifTrue:[^self separatedAmount]. _isRetired ifTrue:[^self retiredAmount]. ^self normalPayAmount.
Replace Conditional with Polymorphism[11]
You have a conditional that chooses different behavior depending on the type of an object
Move each leg of the conditional to an overriding method in a subclass. Make the original method abstract
Example
Employee>>payAmount _type = Employee engineer ifTrue:[^ self _monthlySalary]. _type = Employee manager ifTrue:[^ self _monthlySalary * 2]. _type = Employee instructor ifTrue:[^ self _monthlySalary/2]. self error: 'Invalid Employee'
becomes:
Employee>>payAmount ^_type payAmount: self
EmployeeType>>payAmount: anEmployee self subclassResponsibility
Engineer>>payAmount: anEmployee ^anEmployee monthlySalary
Manager>>payAmount: anEmployee ^anEmployee monthlySalary * 2
Instructor>>payAmount: anEmployee ^anEmployee monthlySalary/ 2
Introduce Null Object[12]
You have repeated checks for a null value
Replace the null value with a null object
Example
customer isNil ifTrue: [plan := BillingPlan basic] ifFalse: [plan := customer plan]
becomes:
NullCustomer>>plan ^BillingPlan basic
plan := customer plan
Introduce Assertion[13]
A section of code assumes something about the state of the program
Make the assumption explicit with an assertion
Example
getExpenseLimit "Should have either expense limit or a primary project" ^_expenseLimit isNil ifTrue:[_expenseLimit] ifFalse:[_primaryProject memberExpenseLimit]
Becomes:
getExpenseLimit self assert: [_expenseLimit isNotNil | primaryProject isNotNil]. ^_expenseLimit isNil ifTrue:[_expenseLimit] ifFalse:[_primaryProject memberExpenseLimit]
Recall that $_ is used to indicate an instance variable (_primaryProject)
Squeak does have an assert: method in Object
[1] Pragmatic Programmer, pp. 4-5
[2] Refactoring Text, pp. 110-116
[3] Example code is Squeak version of Fowler's Java example
[4] Refactoring Text, pp. 238-239
[5] Recall that "_" indicates an instance variable
[6] Refactoring Text, pp. 240-242
[7] Refactoring Text, pp. 243-244
[8] Refactoring Text, pp. 245-249
[9] John and Don happen to be the first names of the authors of the Refactoring Browser. Both are excellent Smalltalk programmers. I do not know why Fowler uses those names as examples of miscreants :)
[10] Refactoring Text, pp. 250-254
[11] Refactoring Text, pp. 255-259
[12] Refactoring Text, pp. 260-266
[13] Refactoring Text, pp. 267-270
Copyright ©, All rights reserved.
2001 SDSU & Roger Whitney, 5500 Campanile Drive, San Diego, CA 92182-7700 USA.
OpenContent license defines the copyright on this document.
Previous    visitors since 20-Mar-01    Next