Refactoring: Improved design of existing code – book links
Reorganizing data
-
Split variable
A variable has only one responsibility
function distanceTravelled(){ let acc = 0 acc = ' ' } Copy the code
function distanceTravelled(){ let acc = 0 let str = ' ' } Copy the code
practice
- At the declaration of the variable to be decomposed and its first assignment, change its name
- If possible, declare the new variable as unmodifiable
- The second assignment of the variable is decomposed to modify the previous reference to the variable and refer to the new variable
function distanceTravelled(scenario, time){ let result let acc = scenario.primaryForce / scenario.mass let primaryTime = Math.min(time, scenario.delay) result = 0.5 * acc * primaryTime * primaryTime let secondTime = time - scenario.delay if(secondTime > 0) {let primaryVelocity = acc * scenario.delay acc = (scenario.primaryForce + scenario.secondForce) / scenario.mass result += primaryVelocity * secondTime + 0.5 * acc * secondTime * secondTime } return result } Copy the code
function distanceTravelled(scenario, time){ let result let primaryAcceleration = scenario.primaryForce / scenario.mass let primaryTime = Math.min(time, scenario.delay) result = 0.5 * primaryAcceleration * primaryTime * primaryTime let secondTime = time - scenario.delay if(secondTime > 0) {let primaryVelocity = primaryAcceleration * scenario.delay let secondaryAcceleration = (scenario.primaryForce + scenario.secondForce) / scenario.mass result += primaryVelocity * secondTime + 0.5 * secondaryAcceleration * secondTime * secondTime } return result } Copy the code
-
Field name
Change non-semantically appropriate field names to more appropriate ones
const organization = { name: 'Acme'.country: 'GB' } Copy the code
class Organization { constructor(data){ this._title = data.title || data.name } get title() {return this._title } set title(title) {this._title = title} } Copy the code
practice
- If the record is not encapsulated, it is encapsulated first
- Rename the field and synchronize the update at the reference
const organization = { name: 'Acme'.country: 'GB' } Copy the code
class Organization { constructor(data){ this._title = data.title || data.name this._country = data.country } get title() {return this._title } set title(title) {this._title = title} get country() {return this._country} set country(country) {this._country = country} } const organization = new Organization({ name: 'Acme'.country: 'GB' }) Copy the code
-
Replace derived variables with queries
Mutable pair data is the biggest source of pair errors in software, and pair modification often leads to ugly coupling between parts of code
class ProductionPlan { constructor(adjustments){ this._adjustments = adjustments this._production = this._adjustments.reduce((prev, item) = > prev + item.amount, 0)}get production() {return this._production} applyAdjustment(anAdjustment){ this._adjustments.push(anAdjustment) this._production += anAdjustment.amount } } Copy the code
class ProductionPlan { constructor(adjustments){ this._adjustments = adjustments } get production() {return this._adjustments.reduce((prev, item) = > prev + item.amount, 0)} applyAdjustment(anAdjustment){ this._adjustments.push(anAdjustment) } } Copy the code
practice
- Identify all updates to variables
- Create a new function that evaluates the value of the variable
class ProductionPlan { constructor(adjustments){ this._adjustments = adjustments this._production = this._adjustments.reduce((prev, item) = > prev + item.amount, 0)}get production() {return this._production} applyAdjustment(anAdjustment){ this._adjustments.push(anAdjustment) this._production += anAdjustment.amount } } Copy the code
class ProductionPlan { constructor(adjustments){ this._adjustments = adjustments } get production() {return this._adjustments.reduce((prev, item) = > prev + item.amount, 0)} applyAdjustment(anAdjustment){ this._adjustments.push(anAdjustment) } } Copy the code
-
Changing a reference object to a value object (changing a value object to a reference object)
Updates the property value of a reference object, directly replacing the entire internal object
class Person { constructor(){ this._tele = new TelephoneNumber() } } class Tele{... }Copy the code
class Person { set officeAreaCode(arg) {return this._tele = new Tele() } set officeNumber(arg) {return this._tele = new Tele() } } class Tele{... }Copy the code
practice
- Checks whether the refactoring target is an immutable object or is modified to be immutable
- Modify the set function in the object to create a new object
class Person { constructor(name){ this._name = name this._telephoneNumber = new TelephoneNumber() } get name() {return this._name } get telephoneNumber() {return this._telephoneNumber.toString() } get officeAreaCode() {return this._telephoneNumber.areaCode } set officeAreaCode(arg) {return this._telephoneNumber.areaCode = arg } get officeNumber() {return this._telephoneNumber.number } set officeNumber(arg) {return this._telephoneNumber.number = arg } } class TelephoneNumber{ get number() {return this._number} set number(arg) {return this._number = arg} get areaCode() {return this._areaCode } set areaCode(arg) {return this._areaCode = arg} toString(){return ` (The ${this._areaCode}) The ${this._number}`}}Copy the code
class Person { constructor(name){ this._name = name } get name() {return this._name } get telephoneNumber() {return this._telephoneNumber.toString() } get officeAreaCode() {return this._telephoneNumber.areaCode } set officeAreaCode(arg) {return this._telephoneNumber = new TelephoneNumber(arg, this.officeNumber) } get officeNumber() {return this._telephoneNumber.number } set officeNumber(arg) {return this._telephoneNumber = new TelephoneNumber(this.areaCode, arg) } } class TelephoneNumber{ constructor(areaCode, number){ this._areaCode = areaCode this._number = number } get number() {return this._number} get areaCode() {return this._areaCode } toString(){return ` (The ${this._areaCode}) The ${this._number}`}}Copy the code
-
Change value objects to reference objects (change reference objects to value objects)
Too many copies are difficult to maintain, so create a warehouse for storage
class Order { constructor(data){ this._customer = new Customer(data.customer) } } class Customer {} Copy the code
let _repository = {} _repository.customer = new Map(a)function registerCustomer(id){ if(! _repository.customer.has(id)){ _repository.customer.set(id,new Customer(id)) } return findCustomer(id) } function findCustomer(id){ return _repository.customer.get(id) } class Order { constructor(data){ this._customer = registerCustomer(data.customer) } } class Customer {} Copy the code
practice
- Create a repository for related objects
- Ensure that the constructor has a way of finding the correct instance of the associated object
- Modify the constructor of the host object to get the associated object from the repository
class Order { constructor(data){ this._number = data.number; this._customer = new Customer(data.customer) } get customer() {return this._customer} } class Customer { constructor(id){ this._id = id } get id() {return this._id} } Copy the code
let _repositoryData function initialize(){ _repositoryData = {} _repositoryData.customer = new Map()}function registerCustomer(id){ if(! _repositoryData.customer.has(id)){ _repositoryData.customer.set(id,new Customer(id)) } return findCustomer(id) } function findCustomer(id){ return _repositoryData.customer.get(id) } initialize() class Order { constructor(data){ this._number = data.number; this._customer = registerCustomer(data.customer) } get customer() {return this._customer} } class Customer { constructor(id){ this._id = id } get id() {return this._id} } Copy the code
Reduced conditional logic
-
Decomposition conditional expression
Decomposes complex conditional expressions into independent functions
function price(){...if(aDate.isSummer || aDate.isSpring){ charge = quantity * aPlan.summerRate }else{ charge = quantity * aPlan.rate } ... } Copy the code
function price(aDate, aPlan){...return summerOrSpring() ? summerCharge() : orderCharge() function summerOrSpring(){... }function summerCharge(){... }function orderCharge(){...} } Copy the code
practice
- Extract functions are used for condition judgments and for each condition branch
function price(aDate, aPlan){ let charge let quantity = 100 if(aDate.isSummer || aDate.isSpring){ charge = quantity * aPlan.summerRate }else{ charge = quantity * aPlan.rate } return charge } Copy the code
function price(aDate, aPlan){ let quantity = 100 return summerOrSpring() ? summerCharge() : orderCharge() function summerOrSpring(){ return aDate.isSummer || aDate.isSpring } function summerCharge(){ return quantity * aPlan.summerRate } function orderCharge(){ return quantity * aPlan.rate } } Copy the code
-
Merge conditional expression
Combine the same return conditions into one place
if(state == 1) return 0 if(start == 2) return 0 return 1 Copy the code
if(state == 1 || start == 2) return 0 return 1 Copy the code
practice
- Determine that conditional expressions have no side effects
- Use the appropriate operation to match the union
if(state == 1) return 0 if(start == 2) return 0 if(end == 3) return 0 return 1 Copy the code
if(state == 1 || start == 2 || end == 3) return 0 return 1 Copy the code
-
Replace nested conditional expressions with a guard statement
Multiple layers of nested judgments reduce readability and can be used for early returns
function payAmount(employee){ if(employee.isSeparated){ ... }else{ if(employee.isRetired){ ... }else{... }}}Copy the code
function payAmount(employee){ if(employee.isSeparated) return if(employee.isRetired) return return } Copy the code
practice
- Select the outer conditional logic that needs to be replaced and replace it with a guard statement
function payAmount(employee){ let result if(employee.isSeparated){ result = {amount: 0.reasonCode: 'SEP'}}else{ if(employee.isRetired){ result = {amount: 0.reasonCode: 'RET'}}else{ result = {amount: 1000.reasonCode: ' '}}}return result } Copy the code
function payAmount(employee){ if(employee.isSeparated) return {amount: 0.reasonCode: 'SEP'} if(employee.isRetired) return {amount: 0.reasonCode: 'RET'} return {amount: 1000.reasonCode: ' '}}Copy the code
-
Replace conditional expressions with polymorphism
Class polymorphism is used to improve complex conditional expressions
class Bird { constructor(name, type){ switch (bird.type){ case 'E': this.plumage = 'e' case 'N': this.plumage = 'n' default: this.plumage = 'unknown'}}}Copy the code
class Bird { get plumage() {return 'unknown'}}class E extends Bird{ get plumage() {return 'e'}}class N extends Bird{ get plumage() {return 'n'}}function createBird(. arg){ switch (arg[1]) {case 'E': return newE(... arg);case 'N': return newN(... arg);default: return new Bird(...arg); } } Copy the code
practice
- If an existing class does not have polymorphic behavior, it is created using the factory pattern
- Use engineering functions in caller code to get object instances
- Move a function with conditional logic into a superclass
- Create a function in any subclass that duplicates the function in the superclass that holds the conditional expression
- Copies the conditional expression branch associated with that subclass into the new function
- Declare the functions of the superclass as abstract functions after processing
function plumages(birds){ return new Map(birds.map(b= > [b.name, b.plumage])) } function speeds(birds){ return new Map(birds.map(b= > [b.name, airSpeedVelocity(b)])) } function plumage(bird){ switch (bird.type){ case 'E': return 'a' case 'A': return bird.counts > 2 ? 't' : 'a' case 'N': return bird.voltage > 100 ? 's' : 'b' default: return 'unknown'}}function airSpeedVelocity(bird){ switch (bird.type){ case 'E': return 35 case 'A': return 40 - bird.counts case 'N': return bird.voltage / 10 + 10 default: return null}}class Bird { constructor(name, type, counts, voltage){ this.name = name this.type = type this.counts = counts this.voltage = voltage this.plumage = plumage(this)}}Copy the code
function plumages(birds){ return new Map(birds.map(b= > [b.name, b.plumage])) } function speeds(birds){ return new Map(birds.map(b= > [b.name, b.airSpeedVelocity])) } class Bird { constructor(name, type, counts, voltage){ this.name = name this.type = type this.counts = counts this.voltage = voltage } get plumage() {return 'unknown' } get airSpeedVelocity() {return null}}class E extends Bird{ get plumage() {return 'a' } get airSpeedVelocity() {return 35}}class A extends Bird{ get plumage() {return this.counts > 2 ? 't' : 'a' } get airSpeedVelocity() {return 40 - this.counts } } class N extends Bird{ get plumage() {this.voltage > 100 ? 's' : 'b' } get airSpeedVelocity() {return this.voltage / 10 + 10}}function createBird(. arg){ switch (arg[1]) {case 'E': return newE(... arg);case 'A': return newA(... arg);case 'N': return newN(... arg);default: return new Bird(...arg); } } Copy the code
function rating(voyage, history){ const vpf = voyageProfitFactor(voyage, history) const vr = voyageRisk(voyage) const chr = captainHistoryRisk(voyage, history) if(vpf * 3 > (vr + chr * 2)) return 'A' return 'B' } function voyageRisk(voyage){ let result = 1 if(voyage.length > 4) result += 2 if(voyage.length > 8) result += voyage.length - 8 if(['china'.'east-indies'].includes(voyage.zone)) result += 4 return Math.max(result, 0)}function captainHistoryRisk(voyage, history){ let result = 1 if(history.length < 5) result += 4 result += history.filter(v= > v.profit < 0).length if(voyage.zone === 'china' && hasChina(history)) result -= 2 return Math.max(result, 0)}function hasChina(history) { return history.some(v= > v.zone === 'china')}function voyageProfitFactor(voyage, history){ let result = 2 if(voyage.zone === 'china') result += 1 if(voyage.zone === 'east-indies') result += 1 if(voyage.zone === 'china' && hasChina(history)){ result += 3 if(history.length > 10) result += 1 if(voyage.length > 12) result += 1 if(voyage.length > 18) result -= 1 }else{ if(history.length > 8) result += 1 if(voyage.length > 14) result -= 1 } return result } const voyage = { zone: 'west-indies'.length: 10 } const history = [ { zone: 'east-indies'.profit: 5 }, { zone: 'west-indies'.profit: 15 }, { zone: 'china'.profit: -2 }, { zone: 'west-africa'.profit: 7}]console.log(rating(voyage, history)) Copy the code
function rating(voyage, history){ return createRating(voyage, history).value } function createRating(voyage, history){ if(voyage.zone === 'china' && history.some(v= > v.zone === 'china')) return new ExperienceChinaRating(voyage, history) return new Rating(voyage, history) } class Rating { constructor(voyage, history){ this.voyage = voyage this.history = history } get value() {const vpf = this.voyageProfitFactor const vr = this.voyageRisk const chr = this.captainHistoryRisk if(vpf * 3 > (vr + chr * 2)) return 'A' return 'B' } get voyageProfitFactor() {let result = 2 if(this.voyage.zone === 'china') result += 1 if(this.voyage.zone === 'east-indies') result += 1 result += this.historyLengthFactor result += this.voyageLengthFactor return result } get voyageLengthFactor() {return this.voyage.length > 14 ? 1 : 0 } get historyLengthFactor() {return this.history.length > 8 ? 1 : 0 } get voyageRisk() {let result = 1 if(this.voyage.length > 4) result += 2 if(this.voyage.length > 8) result += this.voyage.length - 8 if(['china'.'east-indies'].includes(this.voyage.zone)) result += 4 return Math.max(result, 0)}get captainHistoryRisk() {let result = 1 if(this.history.length < 5) result += 4 result += this.history.filter(v= > v.profit < 0).length return Math.max(result, 0)}}class ExperienceChinaRating extends Rating{ get captainHistoryRisk() {const result = super.captainHistoryRisk - 2 return result } get voyageProfitFactor() {return super.voyageProfitFactor + 3 } get voyageLengthFactor() {let result = 0 if(this.voyage.length > 12) result += 1 if(this.voyage.length > 18) result -= 1 return result } get historyLengthFactor() {return this.history.length > 10 ? 1 : 0}}const voyage = { zone: 'west-indies'.length: 10 } const history = [ { zone: 'east-indies'.profit: 5 }, { zone: 'west-indies'.profit: 15 }, { zone: 'china'.profit: -2 }, { zone: 'west-africa'.profit: 7}]console.log(rating(voyage, history)) Copy the code
-
The introduction of special case
Group together multiple values of the same special case
function customerName(aCustomer){ if(aCustomer.toString() === 'unknown') return 'occupant' return aCustomer.name } class Customer { toString() { return this.name || 'unknown'}}Copy the code
function customerName(aCustomer){ return aCustomer.name } class Customer { toString() { return this.name || 'unknown'}}const customer = enrichCustomer(new Customer()) function enrichCustomer(aCustomer){ const unknownCustomer = { name: 'occupant',}if(aCustomer.toString() === 'unknown') return unknownCustomer return aCustomer } Copy the code
practice
- Add the check exception attribute to the refactoring target and make it return false
- Creates an exception object that checks only the properties of the exception and returns true
- Refine the “compare to special case” function and make sure the client uses the new function
- New special case objects are introduced into the code, which can be returned from function calls or generated in transformation functions
- Modify the subject of the exception comparison function to use the properties of the check exception directly in it
- Use function group to synthesize class, function to combine into transform, move the general special case processing logic to the new special case object
- Use an inline function for the special case contrast function, inlining it to where it is still needed
function customerName(aCustomer){ if(aCustomer.toString() === 'unknown') return 'occupant' return aCustomer.name } function customerPlan(aCustomer){ if(aCustomer.toString() === 'unknown') return 'basic plan' return aCustomer.plan } class Customer { constructor(name, plan) { this.name = name this.plan = plan } toString() { return this.name || 'unknown'}}Copy the code
function customerName(aCustomer){ return aCustomer.name } function customerPlan(aCustomer){ return aCustomer.plan } class Customer { constructor(name, plan) { this.name = name this.plan = plan } toString() { return this.name || 'unknown'}}const customer = enrichCustomer(new Customer()) function enrichCustomer(aCustomer){ const unknownCustomer = { name: 'occupant'.plan: 'basic plan' } if(aCustomer.toString() === 'unknown') return unknownCustomer return aCustomer } Copy the code
-
Introduction of assertions
Assertions tell the reader what assumptions the program has made about its current state up to this point
- If you find that the code assumes that a condition is always true, you can add an assertion to illustrate the situation
Refactoring API
-
Separate the query and modify functions
Distinguish between functions with and without side effects
function setOffAlarms(){//setTimeout} function alertForMiscreant(people) { for (const p of people) { if(p === 'oo'){ setOffAlarms() return p } } return ' ' } Copy the code
function alertForMiscreant(people) { if(findMiscreant(people) ! = =' ') setOffAlarms() } function findMiscreant(people) {// find } Copy the code
practice
- Copy the entire function and name it as a query
- Remove all statements with side effects from the newly created query
- Find where the function is called, replace
const people = ['xx'.'yy'.'zz'.'oo'.'zzz'.'cc'] function setOffAlarms(){ setTimeout(() = > { console.log('!!!!!! ')},100)}function alertForMiscreant(people) { for (const p of people) { if(p === 'oo'){ setOffAlarms() return p } if(p === 'cc'){ setOffAlarms() return p } } return ' ' } Copy the code
function alertForMiscreant(people) { if(findMiscreant(people) ! = =' ') setOffAlarms() } function findMiscreant(people) { for (const p of people) { if(p === 'oo' || p === 'cc') {return p } } return ' ' } const found = findMiscreant(people) alertForMiscreant(people) Copy the code
-
Function parameterization
If two functions have similar logic but different literal values, you can synthesize a function and pass in different values as arguments to eliminate duplication
function tenPercentRaise(salary){ return salary * 1.1 } function fivePercentRaise(salary){ return salary * 1.05 } Copy the code
function raise(salary, factor=0){ return salary * (factor + 1)}function tenPercentRaise(salary){ return raise(salary, 0.1)}function fivePercentRaise(salary){ return raise(salary, 0.05)}Copy the code
practice
- Select one from a set of similar functions
- Add the literals that need to be passed in as arguments to the argument list
- Modify all calls to the function where the updated parameters are passed in
- Modify the function body to use the new parameters
function baseCharge(usage){ if(usage < 0) return 0 return topBand(usage) * 0.07 + bottomBand(usage) * 0.03 + middleBand(usage) * 0.05 } function bottomBand(usage){ return Math.min(usage, 100)}function topBand(usage){ return usage > 200 ? usage - 200 : 0 } function middleBand(usage){ return usage > 100 ? Math.min(usage, 200) - 100 : 0 } Copy the code
function baseCharge(usage){ if(usage < 0) return 0 return withinBand(usage, 0.100) * 0.03 + withinBand(usage, 100.200) * 0.05 + withinBand(usage, 200.Infinity) * 0.07 } function withinBand(usage, bottom, top){ return usage > bottom ? Math.min(usage, top) - bottom : 0 } Copy the code
-
Remove marker parameter
Marking parameters sometimes makes it difficult to understand which functions can be called
function deliveryDate(anOrder, isRush){ if(isRush){ ... }else{... }}Copy the code
function rushDeliveryDate(anOrder){... }function regularDeliveryDate(anOrder){... }Copy the code
practice
- Create a new function for each possible value of the argument
- For function callers whose literals are arguments, call the newly created explicit function instead
function deliveryDate(anOrder, isRush){ if(isRush){ let deliveryTime if(['MA'.'CT'].includes(anOrder.deliveryState)) deliveryTime = 1 else if(['NY'.'NH'].includes(anOrder.deliveryState)) deliveryTime = 2 else deliveryTime = 3 return deliveryTime }else{ let deliveryTime if(['MA'.'CT'.'NY'].includes(anOrder.deliveryState)) deliveryTime = 2 else if(['ME'.'NH'].includes(anOrder.deliveryState)) deliveryTime = 3 else deliveryTime = 4 return deliveryTime } } Copy the code
function rushDeliveryDate(anOrder){ let deliveryTime if(['MA'.'CT'].includes(anOrder.deliveryState)) deliveryTime = 1 else if(['NY'.'NH'].includes(anOrder.deliveryState)) deliveryTime = 2 else deliveryTime = 3 return deliveryTime } function regularDeliveryDate(anOrder){ let deliveryTime if(['MA'.'CT'.'NY'].includes(anOrder.deliveryState)) deliveryTime = 2 else if(['ME'.'NH'].includes(anOrder.deliveryState)) deliveryTime = 3 else deliveryTime = 4 return deliveryTime } Copy the code
-
Keep objects intact
It is better to pass an object field into several variables and then pass it into a function than to pass it into a function and parse it intact
const low = dayTempRange.low const high = dayTempRange.high function withinRange(low, high){... }Copy the code
function withinRange(aNumberRange){... }Copy the code
practice
- Create an empty function and give it a list of expected arguments
- Call the old function from the new function and map the new parameters to the old parameter list
- Callers who modify the old function one by one to use the new one
- Use an inline function to inline the old function into the new function, rename the new function, and modify all reference points
const dayTempRange = { low: 10.high: 40 } const low = dayTempRange.low const high = dayTempRange.high if(withinRange(low, high)){ console.log('123')}function withinRange(low, high){ return low > 9 && 41 > high } Copy the code
if(withinRange(dayTempRange)){ console.log('123')}function withinRange(aNumberRange){ return aNumberRange.low > 9 && 41 > aNumberRange.high } Copy the code
-
Replace parameters with queries
Passing arguments too often makes the function look complicated
class Order { get finalPrice() {return this.discountedPrice(basePrice, discount) } discountedPrice(price, discount){...} } Copy the code
class Order { get finalPrice() {return this.discountedPrice() } get basePrice() {... }get discount() {... }discountedPrice(){...} } Copy the code
practice
- Use refining functions to extract the calculation of parameters into a separate function
- Change the place in the function body that references this parameter to call the newly created function instead
class Order { constructor(quantity, price){ this.quantity = quantity; this.price = price; } get finalPrice() {const basePrice = this.price * this.quantity let discount if(this.quantity > 100) discount = 2 else discount = 1 return this.discountedPrice(basePrice, discount) } discountedPrice(price, discount){ switch(discount){ case 1: return price * 0.9 case 2: return price * 0.8}}}Copy the code
class Order { constructor(quantity, price){ this.quantity = quantity; this.price = price; } get finalPrice() {return this.discountedPrice() } get basePrice() { return this.price * this.quantity } get discount() { return this.quantity > 100 ? 2 : 1 } discountedPrice(){ switch(this.discount){ case 1: return this.basePrice * 0.9 case 2: return this.basePrice * 0.8}}}Copy the code
-
Replace queries with parameters
When the reference is complex and the caller needs to figure out the meaning of the parameter, the query needs to be replaced by the parameter
const thermostat = {} class HeatingPlan { get targetTemperature() {if(thermostat.t > this.max) return this.max else if(thermostat.t < this.min) return this.min return thermostat.t } } Copy the code
class HeatingPlan { targetTemperature(t){ if(t > this.max) return this.max else if(t < this.min) return this.min return t } } Copy the code
practice
- Perform query operations on the code to extract variables from the function body
- The existing function body no longer performs the query operation
const thermostat = { selectTemperature: 20 } function setToHeat(){ thermostat.selectTemperature += 10 } function setToCool(){ thermostat.selectTemperature -= 10 } class HeatingPlan { constructor(max, min){ this.max = max; this.min = min; } get targetTemperature() {if(thermostat.selectTemperature > this.max) return this.max else if(thermostat.selectTemperature < this.min) return this.min return thermostat.selectTemperature } } Copy the code
function setToHeat(){ thermostat.selectTemperature += 10 } function setToCool(){ thermostat.selectTemperature -= 10 } class HeatingPlan { constructor(max, min){ this.max = max; this.min = min; } targetTemperature(selectTemperature){ if(selectTemperature > this.max) return this.max else if(selectTemperature < this.min) return this.min return selectTemperature } } Copy the code
-
Remove the set function
If immutable data does not expose modified methods
class Person { get name() {return this._name } set name(arg) {return this._name = arg } } Copy the code
class Person { get name() {return this._name } } Copy the code
practice
- Use a private field implementation
- Remove the set function
class Person { constructor(name){ this._name = name; } get name() {return this._name } set name(arg) {return this._name = arg } } Copy the code
class Person { constructor(name){ this._name = name; } get name() {return this._name } } Copy the code
-
Replace constructors with factory functions
Constructors can be changed to factory functions when they are not suitable for some common situations
class Employee{ constructor(name, typeCode){ this. _name = name this._typeCode = typeCode } } Copy the code
function createEngineer(name){ return new Employee(name, 'E')}Copy the code
practice
- Create a new factory function that uses the existing constructor
- Call the factory function instead of calling the constructor
- Modify the visibility of constructors as much as possible
class Employee{ constructor(name, typeCode){ this. _name = name this._typeCode = typeCode } get name() {return this._name } get type() {return Employee.legalTypeCode[this._typeCode] } static get legalTypeCode() {return { 'E': 'Engineer'.'M': 'Manager'.'S': 'Salesman'}}}Copy the code
class Employee{ constructor(name, typeCode){ this. _name = name this._typeCode = typeCode } get name() {return this._name } get type() {return Employee.legalTypeCode[this._typeCode] } static get legalTypeCode() {return { 'E': 'Engineer'.'M': 'Manager'.'S': 'Salesman'}}}function createEngineer(name){ return new Employee(name, 'E')}Copy the code
-
Replace functions with commands
When a function has a lot of complex operations, you can change to command object mode to handle them
function score() {... }Copy the code
function score() { return new Score().execute() } class Scorer{ execute(){ this.scoreSmoking() this.stateWithLowCertification() } scoreSmoking(){} stateWithLowCertification(){}}Copy the code
practice
- Creates an empty class containing its target function
- Create a field for each parameter
function score(candidate, medicalExam, scoringGuide) { let result = 0 let healthLevel = 0 let highMedicalRiskFlag = false if(medicalExam.isSmoker){ healthLevel += 10 highMedicalRiskFlag = true } let certificationGrade = 'regular' if(scoringGuide.stateWithLowCertification(candidate.originState)){ certificateGrade = 'low' result -= 5 } result -= Math.max(healthLevel - 5.0) return result } Copy the code
function score(candidate, medicalExam, scoringGuide) { return new Score(candidate, medicalExam, scoringGuide).execute() } class Scorer{ constructor(candidate, medicalExam, scoringGuide){ this._candidate = candidate this._medicalExam = medicalExam this._scoringGuide = scoringGuide } execute(){ this._result = 0 this._healthLevel = 0 this._highMedicalRiskFlag = false this.scoreSmoking() this.stateWithLowCertification() this._result -= Math.max(this._healthLevel - 5.0) return this._result } scoreSmoking(){ if(this._medicalExam.isSmoker){ this._healthLevel += 10 this._highMedicalRiskFlag = true}}stateWithLowCertification(){ this._certificationGrade = 'regular' if(this._scoringGuide.stateWithLowCertification(this._candidate.originState)){ this._certificationGrade = 'low' this._result -= 5}}}Copy the code
-
Replace commands with functions
Use functions to accomplish simple tasks
class ChargeCalculator{ get basePrice() {}get charge(){} } function charge(){ return new ChargeCalculator().charge } Copy the code
function charge(){... }Copy the code
practice
- Use inline functions for functions used by command objects during execution
- Pass the arguments in the constructor to the executing function
- Find references in the execution function for all fields and change them to parameters
class ChargeCalculator{ constructor(customer, usage, provider){ this.customer = customer this.usage = usage this.provider = provider } get basePrice() {return this.customer.baseRate * this.usage } get charge() {return this.basePrice + this.provider.connectionCharge } } function charge(customer, usage, provider){ return new ChargeCalculator(customer, usage, provider).charge } Copy the code
function charge(customer, usage, provider){ const basePrice = customer.baseRate * usage return basePrice + provider.connectionCharge } Copy the code
Dealing with inheritance
-
The function up
If a function is the same in the function body of each subclass, move the function up
class Party {}class Employee extends Party { annualCost(){... }}class Department extends Party { annualCost(){... }}Copy the code
class Party { annualCost(){... }}class Employee extends Party {}class Department extends Party {}Copy the code
practice
- Check the functions to be promoted to make sure they are identical
- Check that all function calls and fields referenced in the function body can be called from the superclass
- If the signatures to be promoted are different, they need to be changed to the field name of the superclass
- Create a new function in the superclass and copy the code for a function to be promoted into the superclass
- Remove the function to be promoted
class Party { constructor(monthlyCost){ this.monthlyCost = monthlyCost } } class Employee extends Party { get annualCost() {return this.monthlyCost * 12}}class Department extends Party { get totalAnnualCost() {return this.monthlyCost * 12}}Copy the code
class Party { constructor(monthlyCost){ this.monthlyCost = monthlyCost } get annualCost() {return this.monthlyCost * 12}}class Employee extends Party {}class Department extends Party {}Copy the code
-
Field up
If the field is in each subclass, the field can be promoted to the superclass
class Party {}class Employee extends Party { get annualCost() {... }}class Department extends Party { get annualCost() {... }}Copy the code
class Party { get annualCost() {... }}class Employee extends Party {}class Department extends Party {}Copy the code
practice
- For the fields to be promoted, check all their usage points to make sure they are used in the same way
- If the field name is different, rename it with a variable first
- Add fields to the superclass
class Party { constructor(monthlyCost){ this.monthlyCost = monthlyCost } } class Employee extends Party { get annualCost() {return this.monthlyCost * 12}}class Department extends Party { get totalAnnualCost() {return this.monthlyCost * 12}}Copy the code
class Party { constructor(monthlyCost){ this.monthlyCost = monthlyCost } get annualCost() {return this.monthlyCost * 12}}class Employee extends Party {}class Department extends Party {}Copy the code
-
The constructor body moves up
A subclass that has a common instance attribute can be promoted to a superclass
class Party {}class Employee extends Party { constructor(){ super(a)this.name = name } } class Department extends Party { constructor(){ super(a)this.name = name } } Copy the code
class Party { constructor(name){ this.name = name } } class Employee extends Party {}class Department extends Party {}Copy the code
practice
- If the superclass does not have a constructor, define one. Make sure that subclasses call the superclass into the constructor
- Use a move statement to move a common statement in a constructor in a subclass to a statement in a constructor call from the superclass
- Remove common code between subclasses one by one and promote it to the constructor of the superclass
class Party {}class Employee extends Party { constructor(name, id, monthlyCost){ super(a)this.name = name this.id = id this.monthlyCost = monthlyCost } } class Department extends Party { constructor(name, staff){ super(a)this.name = name this.staff = staff } } Copy the code
class Party { constructor(name){ this.name = name } } class Employee extends Party { constructor(name, id, monthlyCost){ super(name) this.id = id this.monthlyCost = monthlyCost } } class Department extends Party { constructor(name, staff){ super(name) this.staff = staff } } Copy the code
-
My function is
If a function is called by only one or more subclasses, move the function down to the subclass
class Party { annualCost(){...} } class Employee extends Party {} class Department extends Party {} Copy the code
class Party {} class Employee extends Party { annualCost(){...} } class Department extends Party {} Copy the code
practice
- Move the function of the superclass to the target subclass
class Party { constructor(monthlyCost){ this.monthlyCost = monthlyCost } annualCost(){ return this.monthlyCost * 12}}class Employee extends Party { get cost() {return this.annualCost() * 10}}class Department extends Party {}Copy the code
class Party { constructor(monthlyCost){ this.monthlyCost = monthlyCost } } class Employee extends Party { annualCost(){ return this.monthlyCost * 12 } get cost() {return this.annualCost() * 10}}class Department extends Party {}Copy the code
-
Field down
If a field is used by only one subclass, move it to the subclass where the field is needed
class Party { get annualCost() {... }}class Employee extends Party {} class Department extends Party {} Copy the code
class Party {} class Employee extends Party { get annualCost() {... }}class Department extends Party {} Copy the code
practice
- Move a field from the superclass to the target subclass
class Party { constructor(monthlyCost){ this.monthlyCost = monthlyCost } get annualCost() {return this.monthlyCost * 12}}class Employee extends Party { get cost() {return this.annualCost() * 10}}class Department extends Party {}Copy the code
class Party { constructor(monthlyCost){ this.monthlyCost = monthlyCost } get annualCost() {return this.monthlyCost * 12}}class Employee extends Party { get cost() {return this.annualCost * 10}}class Department extends Party {}Copy the code
-
Replace type codes with subclasses
Replace type code judgment with multiple subclasses
class Employee { validateType(type){...} } Copy the code
class Employee {} class Engineer extends Employee{ get type(){} } class Salesman extends Employee{ get type(){} } function createEmployee(name, type){ switch(type){ case 'engineer': return new Engineer(name) case 'salesman': return new Salesman(name) } } Copy the code
practice
- Encapsulation type code field
- Create a subclass that overrides the value function of the type code class and returns the literal of the type code
- Creates a selector logic that maps type code arguments to new subclasses
- Subclasses are repeatedly created for each type code
class Employee { constructor(name, type){ this.validateType(type) this._name = name this._type = type } validateType(type){ if(! ['engineer'.'salesman'.'manager'].includes(type)){ throw new Error('Invalid type')}}toString() { return `The ${this._name} (The ${this._type}) `}}Copy the code
class Employee { constructor(name){ this._name = name } toString() { return `The ${this._name} (The ${this.type}) `}}class Engineer extends Employee{ get type() {return 'engineer'}}class Salesman extends Employee{ get type() {return 'salesman'}}class Manager extends Employee{ get type() {return 'manager'}}function createEmployee(name, type){ switch(type){ case 'engineer': return new Engineer(name) case 'salesman': return new Salesman(name) case 'manager': return new Manager(name) default: throw new Error('Invalid type')}}Copy the code
-
Remove the subclass
If the subclass is too useless, you can remove it and replace it with a field of the superclass
class Person{ get genderCode(){} } class Male extends Person{ get genderCode(){} } class female extends Person{ get genderCode(){} } function isMale(aPerson){ return aPerson instanceof Male} Copy the code
class Person{ constructor(){ this.genderCode = genderCode || 'X' } get isMale() {... }}Copy the code
practice
- Wrap the constructor of a subclass in the factory of the superclass
- Create a new field to represent the type of the subclass
- Modify the original judgment function for subclasses without using the new class field
class Person{ constructor(name){ this.name = name } get genderCode() {return 'X'}}class Male extends Person{ get genderCode() {return 'X'}}class female extends Person{ get genderCode() {return 'F'}}function isMale(aPerson){ return aPerson instanceof Male} Copy the code
class Person{ constructor(name, genderCode){ this.name = name this.genderCode = genderCode || 'X' } get isMale() {return this.genderCode === 'X'}}Copy the code
-
Refining the superclass
If two classes are doing similar things, inheritance mechanisms can distill their similarities into superclasses
class Employee {} class Department {} Copy the code
class Party{ constructor(name){ this.name = name } } class Employee extends Party{} class Department extends Party{} Copy the code
practice
- Create a new superclass for the original class
- Tease out the common elements and move them into the superclass
class Employee { constructor(name, id, monthlyCost){ this.name = name this.id = id this.monthlyCost = monthlyCost } get annualCost() {return this.monthlyCost * 12}}class Department { constructor(name, staff){ this.name = name this.staff = staff } get totalMonthlyCost() {return this.staff.map(e= > e.monthlyCost).reduce((prev, cur) = > prev + cur, 0)}get totalAnnualCost() {return this.totalMonthlyCost * 12}}Copy the code
class Party{ constructor(name){ this.name = name } get annualCost() {return this.monthlyCost * 12}}class Employee extends Party{ constructor(name, id, monthlyCost){ super(name) this.id = id this.monthlyCost = monthlyCost } } class Department extends Party{ constructor(name, staff){ super(name) this.staff = staff } get monthlyCost() {return this.staff.map(e= > e.monthlyCost).reduce((prev, cur) = > prev + cur, 0)}}Copy the code
-
Folded inheritance system
Collapse the subclass and superclass directly when the subclass is not much different from its parent
practice
- Select the class to remove
- Move it to the target class
-
Replace subclasses with delegates
Inheritance has limitations, so it should be reformed by combining combination with inheritance
class Booking{} class PremiumBooking extends Booking{} Copy the code
class Booking{ _bePremium(){ this._premium = new PremiumBookingDelegate(this, extra) } } class PremiumBookingDelegate{} function createPremiumBooking(extra){ booking._bePremium(extra) } Copy the code
practice
- If the constructor has more than one caller, wrap it with a factory function first
- Create an empty delegate. The constructor of this class should accept all subclass-specific data items and a reference back to the superclass as an argument
- Add a field to the superclass to place the delegate object
- Modify the subclass creation logic to initialize the delegate field described above and place a delegate object instance
- Select a subclass function and use the move function to move the function into the delegate class
- If the moved function is still called outside the subclass, the delegate code left in the source class is moved from the subclass to the superclass, and the delegate code is preceded by a guard statement to check that the delegate exists.
- If not, the code is simply removed and the process is repeated until all functions are moved to the delegate class
class Booking{ constructor(show, date){ this.show = show this.date = date } get hasTalkBack() {return this.show.talkBack && !this.isPeakDay } get basePrice() {let result = this.show.price if(this.isPeakDay) result *= 1.15 return result } } class PremiumBooking extends Booking{ constructor(show, date, extra){ super(show, date) this.extra = extra } get hasTalkBack() {return this.show.talkBack } get basePrice() {return super.basePrice * this.extra.fee } get hasDinner() {return this.extra.dinner && !this.isPeakDay } } Copy the code
class Booking{ constructor(show, date){ this.show = show this.date = date } get hasTalkBack() {return this._premiumDelegate ? this._premiumDelegate.hasTalkBack : this.show.talkBack && !this.isPeakDay } get basePrice() {let result = this.show.price if(this.isPeakDay) result *= 1.15 return this._premiumDelegate ? this._premiumDelegate.extendBasePrice(result) : result } get hasDinner() {return this._premiumDelegate ? this._premiumDelegate.hasDinner : false } _bePremium(extra){ this._premiumDelegate = new PremiumBookingDelegate(this, extra) } } class PremiumBookingDelegate{ constructor(hostBooking, extra){ this.host = hostBooking this.extra = extra } get hasTalkBack() {return this.host.show.talkBack } get hasDinner() {return this.extra.dinner && !this.host.isPeakDay } extendBasePrice(base){ return base * this.extra.fee } } function createBooking(show, date){ return new Booking(show, date) } function createPremiumBooking(show, date, extra){ let result = new Booking(show, date) result._bePremium(extra) return result } Copy the code
-
Replace superclasses with delegates
Inheritance is preferred, and delegates are used instead of superclasses when inheritance is problematic
class CatalogItem{... }class Scroll extends CatalogItem{... }Copy the code
class CatalogItem{... }class Scroll{ constructor(catalogId, catalog){ this.catalogItem = catalog.get(catalogId) // new CatalogItem()}}Copy the code
practice
- Create a new field in a subclass that refers to an object of the superclass and initialize the delegate reference to an instance of the superclass
- For each function of the superclass, create a forwarding function in the subclass that forwards the request to the delegate reference
- When all superclass functions are overwritten by forwarding functions, the inheritance relationship is removed
class CatalogItem{ constructor(id, title, tags){ this.id = id this.title = title this.tags = tags } hasTag(arg){ return this.tags.includes(arg) } } class Scroll extends CatalogItem{ constructor(id, title, tags, dateLastCleaned){ super(id, title, tags) this.lastCleaned = dateLastCleaned } needsCleaning(targetDate){ const threshold = this.hasTag('revered')?700 : 1500 return this.daysSinceLastCleaning(targetDate) > threshold } daysSinceLastCleaning(targetDate){ return this.lastCleaned.until(targetDate) } } Copy the code
class CatalogItem{ constructor(id, title, tags){ this.id = id this.title = title this.tags = tags } hasTag(arg){ return this.tags.includes(arg) } } class Scroll{ constructor(id, dateLastCleaned, catalogId, catalog){ this.id = id this.catalogItem = catalog.get(catalogId) this.lastCleaned = dateLastCleaned } get id() {return this.catalogItem.id} get title() {return this.catalogItem.title} get tags() {return this.catalogItem.tags} hasTag(arg){ return this.catalogItem.hasTag(arg) } needsCleaning(targetDate){ const threshold = this.hasTag('revered')?700 : 1500 return this.daysSinceLastCleaning(targetDate) > threshold } daysSinceLastCleaning(targetDate){ return this.lastCleaned.until(targetDate) } } Copy the code