Refactoring: Improve the design of existing code – buy book links
Step one in refactoring: Make sure your refactoring code has a solid set of tests! Step one in refactoring: Make sure your refactoring code has a solid set of tests! Step one in refactoring: Make sure your refactoring code has a solid set of tests!
The basic concept
Refactoring: an adjustment of the internal structure of software to improve the understandability and reduce the modification cost without changing the observable behavior pairs of software
purpose
- Improve software design
- Make software easier to understand
- Help find hidden bugs
- Speed up programming
Reconstruction time
- Preliminary refactoring: It is easier to add new features
- Refactoring to help you understand: Make your code more understandable
- Garbage pick refactoring: Make your code a little better each time you clean it up
- Planned refactoring
- Substitutability refactoring: replacing some dependent modules
The bad smell of code
- Cryptic naming: a good name that clearly identifies its function and usage (change function declarations, rename variables, rename fields)
- Duplicate code: Seeing the same code in more than one place (refining function, moving statement, function moving up)
- Long functions: The longer the function, the more complicated it is to read (refine functions, replace temporary variables with queries, introduce parameter objects, replace functions with commands)
- Long argument list: Long arguments can be confusing (queries instead of temporary variables, parameter objects)
- Global data: It can be changed anywhere in the project, and there is no way to locate where the change occurred (encapsulated variables)
- Variable data: Data modification is prone to unexpected bugs (encapsulating variables, splitting variables)
- Divergent variations: a function is responsible for only one type of context state (extract function)
- Shotgun change: changes need to be made in different classes (inline functions, inline classes)
- Attachment plot: Reduce interaction between modules (transfer function)
- Data muddle: the way in which data is aggregated and broken down into smaller granularity (refining classes, parameter objects)
- Base-type bias: Processing data with the wrong data type (object instead of base-type)
- Repetitive switch: Find all branch confirmations when you want to add a selected branch (polymorphic substitution conditional expression)
- Loop statements: You need to read through the code to understand the semantics inside the loop (pipes instead of loops)
- Redundant elements: Program elements add code structure to support change, facilitate reuse, but sometimes are simple functions (inline functions, inline classes)
- Talk about commonality: Give up situations you don’t need, such as various hooks, and do limited development (remove dead code)
- Temporary fields: Fields created only for a special case
- Too long message chain: object A accesses object B, object B accesses object C… Until the E object (hides the delegate relationship)
- Middleman: Over-delegating (remove the middleman)
- Insider trading: Exchanging data between modules (moving functions, moving fields, hiding delegate relationships)
- Too big a class: A single class does too much (refining classes)
- Similar classes: Classes of the same type should have the same interface (changing function declarations, moving functions, refining superclasses)
- Pure data class: has some fields and functions to access them (encapsulating records)
- Rejected gift: Subclass does not need most fields/functions of superclass (delegates instead of subclass)
- Comments: When you need to write comments, first try refactoring to make all the comments redundant
Refactoring techniques
-
Extract function (inline function)
Distilling the code into a separate function when you need to spend time looking through a piece of code to figure out what it’s doing
function printOwing() {...// printBanner.// printDetails } Copy the code
function printOwing() { printBanner() printDetails() return function printBanner(){... }function printDetails(){... }}Copy the code
Practice:
- Create a new function, named after the function intent
- Copy the code from the source function into the new function
- Take a close look at the abstracted code, the variables referenced by the scope, and determine if they need to be passed in as arguments
- Compile after all variables are processed
const invoices = [ { customer: 'BigCo'.outstanding: [{amount: 10 }, { amount: 20}]}, {customer: 'Helle'.outstanding: [{amount: 30 }, { amount: 40}}]]function printOwing(invoice) { console.log('-- -- -- -- -- -- --) console.log('--- Owes ---') console.log('-- -- -- -- -- -- --) let amounts = 0 for(let { amount } of invoice.outstanding) { amounts += amount } console.log(`name: ${invoice.customer}`) console.log(`amount: ${amounts}`)}Copy the code
function printOwing(invoice) { printBanner() let outstanding = amountFor(invoice) printDetails(invoice, outstanding) return function printBanner(){ console.log('-- -- -- -- -- -- --) console.log('--- Owes ---') console.log('-- -- -- -- -- -- --)}function amountFor(invoice){ return invoice.outstanding.reduce((prev, item) = > prev + item.amount, 0)}function printDetails(invoice, outstanding){ console.log(`name: ${invoice.customer}`) console.log(`amount: ${outstanding}`) } } invoices.forEach(printOwing) Copy the code
-
Inline function (extract function)
Some of the already readable code can be directly inlined within the source function, reducing the level of indirection
function reportLines(aCustomer){... gatherCustomerData(...)return function gatherCustomerData(){...} } Copy the code
function reportLines(aCustomer){... .// gatherCustomerData return lines } Copy the code
practice
- Check the function to make sure it is not polymorphic
- Find all the call points for this function
- Replace the call point with the function body
- After the replacement, the tests are executed
const customer = { name: 'BigCo'.location: 'sz' } function reportLines(aCustomer){ const lines = [] gatherCustomerData(lines, aCustomer) return lines function gatherCustomerData(out, aCustomer){ out.push(['name', aCustomer.name]) out.push(['location', aCustomer.location]) } } Copy the code
function reportLines(aCustomer){ const lines = [] lines.push(['name', aCustomer.name]) lines.push(['location', aCustomer.location]) return lines } Copy the code
-
Extract variables (inline variables)
Breaking complex, verbose expressions into variables is easier to read
function price(order){ return. }Copy the code
function price(order){ let basePrice return. }Copy the code
practice
- Make sure the expression you want to refine has no side effects
- Declare an immutable variable, make a copy of the expression you want to refine, and assign the result of the expression to the variable
- Replace the expression with a new variable
function price(order){ // price is base price - quantity discount + shipping return order.quantity * order.itemPrice - Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 + Math.min(order.quantity * order.itemPrice * 0.1.100)}Copy the code
function price(order){ // price is base price - quantity discount + shipping let basePrice = order.quantity * order.itemPrice let quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 let shipping = Math.min(basePrice * 0.1.100) return basePrice - quantityDiscount + shipping } Copy the code
-
Inline variables (inline variables)
Local variables can be eliminated by inlining variables
function price(order){ let basePrice return. }Copy the code
function price(order){ return. }Copy the code
practice
- Check whether the expression on the right side of the validation variable assignment statement has side effects
- Find the first use of the variable and replace it with the expression to the right of the assignment statement
function price(order){ let basePrice = order.basePrice; return (basePrice > 1000)}Copy the code
function price(order){ return order.basePrice > 1000 } Copy the code
-
Changing a function declaration
Change functions and variables to semantically better names
// before function circum (x){... }Copy the code
// Simple function circumference (radius){... }// Complex approach function circum (radius){ return circumference(radius) function circumference (radius){...} } Copy the code
A simple approach
- To migrate a parameter, you need to make sure that no parameter is used in the function body
- Modifying a function declaration
- Find the reference and replace it
The migration approach
- If necessary, refactor the inside of the function body first to make the subsequent extraction steps easy to unfold
- The extract function is used to extract the body of a function into a single function
- If the extracted function requires new arguments, follow the simple procedure
- Use inline functions for older functions
function circum (radius){ return 2 * Math.PI * radius } Copy the code
// Simple function circumference (radius){ return 2 * Math.PI * radius } // Complex approach function circum (radius){ return circumference(radius) function circumference (radius){ return 2 * Math.PI * radius } } Copy the code
function isNewEndLand(aCustomer){ return ['MA'.'CT'.'ME'.'VT'.'NH'.'NH'.'RI'].includes(aCustomer.address.state) } Copy the code
function isNewEndLand(aCustomer){ return checkState(aCustomer.address.state) function checkState(state){ return ['MA'.'CT'.'ME'.'VT'.'NH'.'NH'.'RI'].includes(state) } } Copy the code
-
Encapsulation variable
Encapsulate variables into function calls for easy modification and data monitoring
letdefaultOwnerData = {... }function defaultOwner(){ return Object.assign({}, defaultOwnerData) } function setDefaultOwner(newOwner){ return defaultOwnerData = newOwner } Copy the code
practice
- Create a wrapper function in which variables are accessed and updated
- Performing a static check
- Modify the code that uses variables one by one to call the appropriate wrapper function
- Limit the variability of variables
let defaultOwnerData = { firstName: 'Mt'.lastName: 'Fl' } function defaultOwner(){ return Object.assign({}, defaultOwnerData) } function setDefaultOwner(newOwner){ return defaultOwnerData = newOwner } Copy the code
-
The variable name
A good name is a good start
let a = 'xy' a = 'dq' Copy the code
let _name = 'xy' function name(){ return _name } function setName(name){ _name = name } Copy the code
practice
- If a variable is widely referenced, it is encapsulated using encapsulated variables
- Find the code that uses this variable, and pay attention to the modifications
let tpHd = '111'; let result = ' ' result += `title: ${tpHd}\n` tpHd = '222'; result += `title: ${tpHd}` Copy the code
let _title = '111'; let result = ' ' result += `title: ${title()}\n` tpHd = setTitle('222'); result += `title: ${title()}` function title() { return _title } function setTitle(title) { _title = title } Copy the code
-
Import parameter objects
Using parameter objects instead of multiple parameters
function readingsOutsideRange(station, min, max){ return. }Copy the code
class Range {... }function readingsOutsideRange(station, range){ return. }Copy the code
practice
- If you don’t have a data structure in place right now, create one
let station = { name: 'ZB1'.readings: [{temp: 47 }, { temp: 53 }, { temp: 28 }, { temp: 53 }, { temp: 61},],}const min = 30 const max = 60 function readingsOutsideRange(station, min, max){ return station.readings.filter((r) = > r.temp < min || r.temp > max) } let list = readingsOutsideRange(station, min, max) Copy the code
class Range { constructor(min, max) { this.min = min this.max = max } contains(arg){ return arg >= this.min && arg <= this.max } } function readingsOutsideRange(station, range){ return station.readings.filter(r= >! range.contains(r.temp)) }let range = new Range(min, max) let list = readingsOutsideRange(station, range) Copy the code
-
Function groups combine classes
A set of functions that operate on the same block of data can form a class, which can pass fewer arguments and simplify calls
constreading = {... }function baseRate(month, year) {... }const baseCharge = ... const base = ... Copy the code
class Reading {... }const aReading = new Reading(reading) Copy the code
practice
- Encapsulate data shared by multiple functions by encapsulating records
- For each function that uses the record structure, it is moved into the new class by a move function
- The logic used to process this data record can be extracted and moved to the new class using the extract function
const reading = { customer: 'xy'.quantity: 10.month: 5.year: 2017 } function baseRate(month, year) { return month * 0.1 + year * 0.15 } function taxThreshold(year) { return year * 10 } const baseCharge = baseRate(reading.month, reading.year) * reading.quantity const base = baseRate(reading.month, reading.year) * reading.quantity const taxableCharge = Math.max(0, base - taxThreshold(reading.year)) const amount = calculateBaseCharge(reading) function calculateBaseCharge(aReading) { return baseRate(aReading.month, aReading.year) * aReading.quantity } Copy the code
class Reading { constructor(data) { this._customer = data.customer this._quantity = data.quantity this._month = data.month this._year = data.year } get customer() { return this._customer } get quantity() { return this._quantity } get month() { return this._month } get year() { return this._year } get baseCharge() {return baseRate(this.month, this.year) * this.quantity } get taxableCharge() {return Math.max(0.this.baseCharge - taxThreshold(this.year)) } } const aReading = new Reading(reading) const baseCharge = aReading.baseCharge const base = aReading.baseCharge const taxableCharge = aReading.taxableCharge const amount = aReading.baseCharge Copy the code
-
Function combination transformation
Take the source data as input, derive the data, and fill in the input and output data with the derived data as fields
constreading = {... }function baseRate(month, year) {... }const baseCharge = ... const base = ... Copy the code
function clone(obj){... }function enrichReading(aReading){ let result = clone(aReading) ... return result } const aReading = enrichReading(reading) Copy the code
practice
- Create a transformation function that takes the record as input and returns the value of that record (note that deep copying is best here)
- Extract the function and add the result as a field to the enhancement object
const reading = { customer: 'xy'.quantity: 10.month: 5.year: 2017 } function baseRate(month, year) { return month * 0.1 + year * 0.15 } function taxThreshold(year) { return year * 10 } const baseCharge = baseRate(reading.month, reading.year) * reading.quantity const base = baseRate(reading.month, reading.year) * reading.quantity const taxableCharge = Math.max(0, base - taxThreshold(reading.year)) const amount = calculateBaseCharge(reading) function calculateBaseCharge(aReading) { return baseRate(aReading.month, aReading.year) * aReading.quantity } Copy the code
function clone(obj){ return JSON.parse(JSON.stringify(obj)) } function enrichReading(aReading){ let result = clone(aReading) result.baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(aReading.year)) return result } const aReading = enrichReading(reading) const baseCharge = aReading.baseCharge const base = aReading.baseCharge const taxableCharge = aReading.taxableCharge const amount = aReading.baseCharge Copy the code
-
Split phase
A piece of code that is doing two different things at once is broken up into separate modules
function priceOrder() {...return price } Copy the code
function priceOrder() { const priceData = calculatePricingData() return applyShipping(priceData) function calculatePricingData(){ return{... }}function applyShipping(priceData){... }}Copy the code
practice
- Extract the two-phase code into individual functions
- Introduce a relay data structure that is used as an argument to add the argument list that refines the new function
- One by one, “each parameter in Phase 2” is extracted, and if a parameter is used in phase 1, it is moved into the relay data structure
- Apply the extract function to the first phase of the code, and let the extract function return the transfer data structure
/ * * * *@param {{ basePrice: number, discountThreshold: number }} product * @param {number} quantity * @param {{discountThreshold: number, discountedFee: number, feePerCase: number}} shippingMethod */ function priceOrder(product, quantity, shippingMethod) { const basePrice = product.basePrice * quantity const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice const shippingPerCase = (basePrice > shippingMethod.discountThreshold) ? shippingMethod.discountedFee : shippingMethod.feePerCase const shippingCost = quantity * shippingPerCase const price = basePrice - discount + shippingCost return price } Copy the code
/ * * * *@param {{ basePrice: number, discountThreshold: number }} product * @param {number} quantity * @param {{discountThreshold: number, discountedFee: number, feePerCase: number}} shippingMethod */ function priceOrder(product, quantity, shippingMethod) { const priceData = calculatePricingData(product, quantity) return applyShipping(priceData, shippingMethod) function calculatePricingData(product, quantity){ const basePrice = product.basePrice * quantity const discount = Math.max(quantity - product.discountThreshold, 0) * product.basePrice return { basePrice, quantity, discount } } function applyShipping(priceData, shippingMethod){ const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold) ? shippingMethod.discountedFee : shippingMethod.feePerCase const shippingCost = priceData.quantity * shippingPerCase return priceData.basePrice - priceData.discount + shippingCost } } Copy the code