preface
In this article I will try to demonstrate how refactoring can be done correctly through an actual project
What is Refactoring
The word “reconstruction” can be used as a noun or as a verb.
1.1 the noun
An adjustment to the internal structure of software intended to improve readability and reduce the cost of modification without changing the observable behavior of the software.
Take a chestnut
If you have a bug in your program, it should still exist after a proper refactoring. This is not changing the external behavior.
1.2 the verb
A series of refactoring techniques are used to adjust the structure of the software without changing its observable behavior.
1.3 Key Points
The key to refactoring is to use a large number of small but consistent software behavior steps to achieve large-scale changes. Each individual refactoring is either small or composed of several small steps. Code will rarely go into a non-working state, and with the perfect combination of small steps, the entire refactoring process can be debugged without spending any time.
Why refactoring
2.1 Refactoring and improving software design
The internal design of a program deteriorates over time, and refactoring is needed to help the code maintain its state. An important way to improve design is to eliminate repetition, which is the root of all evil!
2.2 Refactoring makes software easier to understand
Make the code express itself better — say what I want to do more clearly. Master programmers tell systems as stories, and readability is key!
2.3 Refactoring helps find bugs
Refactoring allows you to gain insight into what your code is doing and reflect that new understanding into your code. When you understand the structure of the program, it’s hard not to pick out the bugs.
When to refactor
3.1 Preparatory Refactoring: Make it easier to add new features
Effective refactoring makes it easier to change logic, add logic, and reduce duplication of existing code through refactoring.
3.2 Understandable refactoring: Make code easier to understand
You need to understand what the code is doing before you can start making changes. Use refactoring to make this code self-explanatory.
3.3 Garbage pickup reconstruction
I already understand what the code is doing, but if it’s not doing well, I can refactor.
Professional programmers follow the Scout code of “make camp cleaner than you came.”
Start refactoring
4.1 Project Introduction
First, let’s talk about the Length of the project we’re refactoring.
Americans are used to the odd British unit of measurement. Conversion of English units of measure is often not decimal, for example:
• 1 foot = 12 inches
• 1 yard = 3 foot
Please write a program to handle the conversion between inches, feet and yards. Such as:
• 1 foot should make 12 inches
• A yard should equal 36 inches
An inch should equal 1/36 of a yard
4.2 Implementation Code
export class Length {
constructor(val, uint) {
this.value = val
this.unit = uint
}
getVal() {
return this.value
}
getUint() {
return this.unit
}
parseTo(u) {
let len = this
if (this.unit === 'yard') {
if (u === 'f') {
len = new Length(this.value * 3, u)
} else if (u === 'inch') {
len = new Length(this.value * 36, u)
}
}
if (this.unit === 'inch') {
if (u === 'yard') {
len = new Length(this.value / 36, u)
} else if (u === 'f') {
len = new Length(this.value / 12, u)
}
}
if (this.unit === 'f') {
if (u === 'yard') {
len = new Length(this.value / 3, u)
} else if (u === 'inch') {
len = new Length(this.value * 12, u)
}
}
return len
}
}
Copy the code
4.3 Recognize bad taste
How does it feel to see the code above? Feel it first and look down.
Do you feel bad about readability?
-
Variable names are not clear;
-
Warning parseTo() function logic is too long
-
Repeated strings;
Do some partners feel that the problem is not very big ah. I don’t care if I don’t have 50 lines of code.
That’s when it happens, every big problem is piled up with small problems, and less and less clear code will cause maintenance costs to skyrocket.
Always beware of broken Windows theory:
The theory is that undesirable phenomena in the environment, if allowed to exist, will induce people to imitate or even exacerbate them. Take a building with a few broken Windows. If those Windows are not repaired, vandals will probably destroy more Windows. Eventually they will even break into buildings and, if they find them empty, perhaps settle there or set fire to them. A wall, if some graffiti is not washed off, soon, the wall is covered with messy, ugly things. A few scraps of paper on a sidewalk, and soon there will be more, and eventually people will take it for granted and throw it on the ground.
You may be tempted to change the code at this point, but don’t worry, there is one more important thing to do before you change the code.
4.4 Adding a Security Network
Refactoring without testing is going rogue. How can we be sure that the refactored logic is consistent with the previous behavior?
In order to be consistent with the previous behavior, then we need to verify.
That’s why we need to add testing, in other words you can’t guarantee that your refactoring is safe and correct without testing.
In my experience, 80 percent of projects don’t have tests, so let’s show you how to add tests.
4.4.1 introduced jest
1, install,
Yarn add --dev jest // Babel is needed to compile es6+ syntax YARN add --dev babel-jest @babel/ core@babel /preset-envCopy the code
2. Add to package.json
Add test to package.json
// package.json
{
"script":{
"test":"jest"
}
}
Copy the code
Create babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
],
};
Copy the code
4. Create the first test
We’ll create an index.test.js file and add code to test if jest is installed successfully.
import {Length} from "./index"
descript("Length",()=>{
test("getVal",()=>{
const length = new Length(100);
expect(length.getVal()).toBe(100)
})
})
Copy the code
Ok Now jEST has been successfully introduced.
4.5 Test Improvement
First, we need to read the project requirements in detail, and then increase the test coverage and improve the safety net.
4.5.1 One foot should make twelve inches
First complete this test:
Test ('1 inch should equal 12 inches ', () => {const length = new length (1,"f").parseto ("inch"); expect(length.getVal()).toBe(12) })Copy the code
When I read the code, I realized that this parseTo should be used as a conversion unit.
Tip: Read the code with questions or questions
Then write the corresponding test code with our question to verify that it is correct.
Run tests:
yarn test
Copy the code
PASS./index.test.js Length ✓ getVal (3ms) ✓ 1 foot should equal 12 inches (1ms) test Suites: 1 passed, 1 total Tests: 2 passed, 2 total Snapshots: 0 Total Time: 2.756s Ran all test suites. ✨ Done in 5.29s.Copy the code
Ok to go through.
The test helped us verify the previous question, and from the test results, we should be able to reach a very clear conclusion:
After initializing a unit, the parseTo function converts the unit to the corresponding value, which is just feet to inches and can be extended to feet to yards.
Nine feet should equal three yards
Test ('9 feet should equal 3 yards ', () => {const length = new length (9,"f").parseto ("yard"); expect(length.getVal()).toBe(3) })Copy the code
And just to explain why we know that 9 feet is equal to 3 yards?
Because I looked at the logic of the source code.
// index.js
if (this.unit === 'f') {
if (u === 'yard') {
len = new Length(this.value / 3, u)
} else if (u === 'inch') {
len = new Length(this.value * 12, u)
}
}
Copy the code
When u === yard is converted into code logic.
So the conditional branch tests are covered, and again, we’ll write the rest of the tests quickly.
Twenty-four inches should equal two feet
Test ('24 inches should equal 2 feet ', () => {const length = new length (24,"inch").parseto ("f"); expect(length.getVal()).toBe(2) })Copy the code
36 inches should equal 1 yard
Const length = new length (36,"inch").parseto ("yard"); const length = new length (36,"inch").parseto ("yard"); expect(length.getVal()).toBe(1) })Copy the code
A yard should make three feet
Const length = new length (1, "yard").parseto ("f"); const length = new length (1, "yard").parseto ("f"); expect(length.getVal()).toBe(3); });Copy the code
A yard should equal 36 inches
Test ("1 yard = 36 inches ", () => {const length = new length (1, "yard").parseto ("inch"); expect(length.getVal()).toBe(36); });Copy the code
4.5.7 The current unit should be returned if there is no corresponding conversion unit
Observe the parseTo function:
Function parseTo(){let len = this... return len }Copy the code
The last line returns itself.
This logic is the easiest to ignore, so tests are needed to ensure consistent behavior.
Test (' return current unit if there is no corresponding conversion unit ', () => {const val = 1; const unit = "yard" const length = new Length(1,"yard").parseTo('mi'); expect(length.getVal()).toBe(val) expect(length.getUint()).toBe(unit) })Copy the code
How do you tell if it’s returning itself. Check whether the unit and value of the returned length are the same as those of the new length.
This logic also validates the getVal() and getUnit() functions.
4.5.8 refactoring
After writing the test, we need to pause to see if the code is legible and non-repetitive. The test logic above is to check whether the two lengths are equal. But it doesn’t make a lot of sense in the code, so let’s refactor it.
// index.test.js test(" Return current unit if there is no corresponding conversion unit ", () => {const val = 1; const unit = "yard" - const length = new Length(1,"yard").parseTo('mi'); - expect(length.getVal()).toBe(val) - expect(length.getUint()).toBe(unit) + const length = new Length(val,unit); + const newLength = length.parseTo("mi"); + expect(length.equal(newLength)).toBeTruthy() });Copy the code
// index.js + equal(length){ + return this.value === length.getVal() && this.unit === length.getUnit(); +}Copy the code
Add the equal function so that we can see the intent of the code at a glance.
4.5.9 Test coverage
With our safety net built, we can take a look at our current test coverage.
yarn test --coverage
Copy the code
You can see:
----------|---------|----------|---------|---------|------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered the Line # s -- -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- - | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- All files | 100 | | 100 88.89 100 | | index. Js | 100 | | 100 | | 100 28 88.89 4 ----------|---------|----------|---------|---------|-------------------Copy the code
Almost 100%!
Now it’s time to refactor!
4.6 Extracting String Constants
By now, we should have noticed a particularly obvious overlap: the string for “units” is in multiple places, and here is the obvious “duplicate code”.
It’s important to emphasize here: test code is just as important as production code.
The dirtier the test, the dirtier the code will get and eventually the tests will be lost and the code will start to rot, so the test code is also within our refactoring scope.
Let’s get started
Remembering that refactoring is all about small steps, I decided to change the “yard”, “inch” and “F” to constants first.
Steps:
- So let’s create a constant
// index.js const YARD = "YARD";Copy the code
- Run down the test
yarn test
Copy the code
In particular, some students may wonder if the operation here is so small. Is it necessary to run the test?
It is necessary to
If we make sure that every step is problem-free, we can do this without debugging the code. When you find that the test doesn’t work after making changes, immediately back up the code. Because each of our steps are small enough, and we can guarantee that we will pass the test when we pull back.
- Make use of global search
Open the search bar with Shift + Command + F;
Type ‘yard’ in the search bar;
Enter YARD in the replace field;
Vscode development
You can see that the red circle indicates that you can replace all files. We click, replace all ‘yards’ in index.js.
It is much safer to modify through IDE than to modify one by one by hand. Another point is that we must be skilled in using shortcut keys, which can help us improve efficiency.
- Run down the test
yarn test
Copy the code
Yeah, we’ll run the test after we fix it, make sure everything is safe. Similarly, we refactor the remaining “inch” and “f” using the above steps, and our final code should look like this:
// index.js const YARD = "yard"; const F = "f"; Const INCH = "INCH"...... parseTo(u) { let len = this if (this.unit === YARD) { if (u === F) { len = new Length(this.value * 3, u) } else if (u === INCH) { len = new Length(this.value * 36, u) } } if (this.unit === INCH) { if (u === YARD) { len = new Length(this.value / 36, u) } else if (u === F) { len = new Length(this.value / 12, u) } } if (this.unit === F) { if (u === YARD) { len = new Length(this.value / 3, u) } else if (u === INCH) { len = new Length(this.value * 12, u) } } return len }Copy the code
At this point we have changed the repeated strings in production code to constants. Next we apply the same refactoring to the test file index.test.js. The first problem should be identified when we start refactoring. The constants we declared earlier are in index.js, and we need to reuse them here, so we create a consts.js file to declare constants uniformly.
// consts.js
export const YARD = "yard";
export const F = "f";
export const INCH = "inch"
Copy the code
Then modify the index.js file
+ import { YARD, F, INCH } from "./consts";
- const YARD = "yard";
- const F = "f";
- const INCH = "inch"
Copy the code
Run down the test
yarn test
Copy the code
Ok test passed Let’s move on to index.test.js.
// index.test.js
+ import { YARD, F, INCH } from "./consts";
Copy the code
Index.test.js after refactoring:
import { Length } from "./index"; import { YARD, F, INCH } from "./consts"; describe("Length", () => { test("getVal", () => { const length = new Length(100); expect(length.getVal()).toBe(100); }); Const length = new length (1, F).parseto (INCH); const length = new length (1, F).parseto (INCH); expect(length.getVal()).toBe(12); }); Test ("9 feet should equal 3 yards ", () => {const length = new length (9, F).parseto (YARD); expect(length.getVal()).toBe(3); }); Test ("24 inches should equal 2 feet ", () => {const length = new length (24, INCH).parseto (F); expect(length.getVal()).toBe(2); }); Test ("36 inches should equal 1 YARD ", () => {const length = new length (36, INCH).parseto (YARD); expect(length.getVal()).toBe(1); }); Const length = new length (1, YARD).parseto (F); const length = new length (1, YARD).parseto (F); expect(length.getVal()).toBe(3); }); > const length = new length (1, YARD).parseto (INCH); expect(length.getVal()).toBe(36); }); Test (" Return current unit if there is no corresponding conversion unit ", () => {const val = 1; const unit = YARD; const length = new Length(1, YARD).parseTo("mi"); expect(length.getVal()).toBe(val); expect(length.getUint()).toBe(unit); }); });Copy the code
At this point, we have replaced all repeated strings in our production and test code with constants. This should give you a taste of refactoring. We refactor it little by little with each safe step. If there’s only one thing you can remember, make small changes to the code and run the tests immediately!
4.7 Changing a Name
You can see that the name “F” has no clear meaning. Meaningful naming is an important prerequisite for readable code. This refactoring will be very simple because of what we’ve done before.
Open the consts.js file;
Select F and press F2 global rename shortcut key;
Enter the new name FOOT in the input box that pops up.
// consts.js
+ const FOOT = "f"
- const F = "f"
Copy the code
Run down the test
yarn test
Copy the code
Ok, no problem
And then we noticed that all the F’s in all the files had changed to FOOT.
Now let’s change the ‘f’ as well:
- const FOOT = "f"
+ const FOOT = "foot"
Copy the code
Run down the test
yarn test
Copy the code
4.8 summarize
With the use of shortcuts, our refactoring can take off efficiently!
4.9 Refining functions
4.9.1 refining isUnit
Let’s focus on the parseTo() function:
If (this.unit === YARD) {if (u === FOOT) {...... }else if(u === INCH){ }Copy the code
It can be found that a large number of if statements are used to determine whether it is a certain unit, which lacks expression power. We refactor to make the code more readable.
It is already clear from the previous add test that there are three different units that can be converted to each other, so first add a function to determine whether a specific unit is available:
// index.js
isYard(unit){
return unit === YARD;
}
Copy the code
We can tell at a glance from the function name that this is a yard check.
Run down the test
yarn test
Copy the code
Next, we replace the first if statement
-if (this.unit === YARD) {+ if (this.isYard(this.unit)) if (u === FOOT) {... }else if(u === INCH){ }Copy the code
Run down the test
yarn test
Copy the code
Again, let’s replace the rest of the if statements, and be sure to run the test every step of the way.
Take a look at our refactored code:
isYard(unit){
return unit === YARD;
}
isFoot(unit){
return unit === FOOT;
}
isInch(unit){
return unit === INCH;
}
parseTo(u) {
……
}
Copy the code
Do you feel like our code is getting better, and it’s important that you stop at any time.
Because your code is always running. So some people say I need 2 days to refactor the code, sorry, you are not called refactor, it is called rewrite -#
4.9.2 Refining the calculation function
It should be clear by now that the parseTo function handles all three conversions at once.
As mentioned in the functions chapter of Clean Code, good functions should be as small as possible, and a function should only do one thing.
So here we are distilling the logic of each unit conversion into the corresponding function.
Let’s start by creating a new function:
parseYard(u){ if (this.isFoot(u)) { len = new Length(this.value * 3, u); } else if (this.isInch(u)) { len = new Length(this.value * 36, u); }}Copy the code
Then run the quiz:
yarn test
Copy the code
To continue, replace the previous logic with calling the new function:
if (this.isYard(this.unit)) {
// if (this.isFoot(u)) {
// len = new Length(this.value * 3, u);
// } else if (this.isInch(u)) {
// len = new Length(this.value * 36, u);
// }
+ return this.parseYard(u)
}
Copy the code
Run down the test
yarn test
Copy the code
The test failed!
If there is no corresponding conversion unit, the current unit TypeError should be returned: Cannot read property 'getVal' of undefined 7 | 8 | equal(length){ > 9 | return this.value === length.getVal() && this.unit === length.getUnit(); | ^ 10 | } Test Suites: 1 failed, 1 total Tests: 1 failed, 7 passed, 8 totalCopy the code
Through the feedback we received from the test, we realized that the problem should be that the returned object is not length.
Because our steps are small, we can quickly lock the problem down to the newly added function parseYard().
A closer look at the logic shows that if neither if matches, undefined is returned. Our tests suggest that the current unit should be returned if there is no corresponding conversion unit, so here we should return the current Length object.
parseYard(u){
if (this.isFoot(u)) {
return new Length(this.value * 3, u);
} else if (this.isInch(u)) {
return new Length(this.value * 36, u);
}
+ return this;
}
Copy the code
Run down the test
yarn test
Copy the code
Through!!
Now we can begin to appreciate the benefits of small steps and safety nets. If the test fails, we can quickly locate the problem.
See, we don’t need to debug code at all.
Similarly, refactor the rest of the logic.
The final code looks like this:
ParseYard (u) {... {} parseInch (u)... {} parseFoot (u)... } parseTo(u) { if (this.isYard(this.unit)) { return this.parseYard(u); } if (this.isInch(this.unit)) { return this.parseInch(u); } if (this.isFoot(this.unit)) { return this.parseFoot(u); }}}Copy the code
The refactoring is now very readable. If more unit conversions need to be added later, they can be solved by refactoring to extend polymorphism.
Rule of three: Do something the first time. The second time you do something similar, you’ll be offended, but you can do it anyway. The third time you do something like this, you should refactor.
As the old saying goes: Three things, three things.
I think refactoring is a good way to balance design overkill, we just follow the rule of three.
Because we have enough tests, we can refactor as soon as the code starts to smell bad. This avoids both design overkill and code rot.
conclusion
I showed you how to do this with a real project. First we build safety nets, then quickly and safely refactor by identifying bad smells + small steps + frequent running tests + IDE.
We can always stop because our code is always working.
Hopefully this article has given you a feel for “how to refactor”.
The most important thing this example tells us is the pace of refactoring.
Take small steps, and make sure that each step is compiled and tested in a working state.
The key to efficient and orderly refactoring is that small steps make faster progress, keep your code always working, and small changes can add up to a big improvement in your system design.
Of course, there is still room for improvement in this example, but for now the tests all pass and the code is a huge improvement from when I first saw it, so I’m satisfied.
reference
-
Refactoring – Improving the Design of Existing Code
-
The Code Clean Way
Vscode refactoring shortcut keys
-
Find all references: Shift+F12;
-
Modify all matches in this file at the same time: Ctrl+F12;
-
Rename: For example, to change the name of a method, you can select and press F2, type the new name, press Enter, and you will find that all files have been changed.
-
Jump to the next Error or Warning: When there are multiple errors, press F8 to jump to one by one.