ℹ️ This repository is part of my Refactoring catalog based on Fowler's book with the same title. Please see kaiosilveira/refactoring for more details.
Formerly: Replace Method with Method Object
Before | After |
---|---|
function score(candidate, medicalExam, scoringGuide) {
let result = 0;
let healthLevel = 0;
// long body code
} |
class Scorer {
constructor(candidate, medicalExam, scoringGuide) {
this._candidate = candidate;
this._medicalExam = medicalExam;
this._scoringGuide = scoringGuide;
}
execute() {
this._result = 0;
this._healthLevel = 0;
// long body code
}
} |
Inverse of: Replace Command with Function
Nested functions are a useful tool in most cases but, sometimes, we need extra control over variables and readability. That's when this refactoring comes in handy.
Our working example consists of a program that calculates a health score for an insurance company. It goes like this:
export 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)) {
certificationGrade = 'low';
result -= 5;
}
// lots more code like this
result -= Math.max(healthLevel - 5, 0);
return result;
}
Our goal here is to make the score
function into a command, so we can isolate the processing into chunks, making the function more legible and probably more testable as well.
The test suite covers all possible bifurcations of the aforementioned function, and it is somewhat extensive. To see the full implementation, please refer to src/index.test.js.
We start by introducing a Scorer
class:
+export class Scorer {
+ execute(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)) {
+ certificationGrade = 'low';
+ result -= 5;
+ }
+
+ // lots more code like this
+ result -= Math.max(healthLevel - 5, 0);
+
+ return result;
+ }
+}
Then, we inline Scorer.execute
at the body of score
:
export 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)) {
- certificationGrade = 'low';
- result -= 5;
- }
-
- // lots more code like this
- result -= Math.max(healthLevel - 5, 0);
-
- return result;
+ return new Scorer().execute(candidate, medicalExam, scoringGuide);
}
Then, since the command's only raison d'être is to execute the scoring logic, it's somewhat more semantic to have the function's arguments as part of its constructor. We start by moving candidate
:
+++ b/src/index.js
@@ -1,9 +1,13 @@
export function score(candidate, medicalExam, scoringGuide) {
- return new Scorer().execute(candidate, medicalExam, scoringGuide);
+ return new Scorer(candidate).execute(medicalExam, scoringGuide);
}
export class Scorer {
- execute(candidate, medicalExam, scoringGuide) {
+ constructor(candidate) {
+ this._candidate = candidate;
+ }
+
+ execute(medicalExam, scoringGuide) {
let result = 0;
let healthLevel = 0;
let highMedicalRiskFlag = false;
let certificationGrade = 'regular';
- if (scoringGuide.stateWithLowCertification(candidate.originState)) {
+ if (scoringGuide.stateWithLowCertification(this._candidate.originState)) {
certificationGrade = 'low';
result -= 5;
}
Then, we do the same for medicalExam
:
export function score(candidate, medicalExam, scoringGuide) {
- return new Scorer(candidate).execute(medicalExam, scoringGuide);
+ return new Scorer(candidate, medicalExam).execute(scoringGuide);
}
export class Scorer {
- constructor(candidate) {
+ constructor(candidate, medicalExam) {
this._candidate = candidate;
+ this._medicalExam = medicalExam;
}
- execute(medicalExam, scoringGuide) {
+ execute(scoringGuide) {
let result = 0;
let healthLevel = 0;
let highMedicalRiskFlag = false;
- if (medicalExam.isSmoker) {
+ if (this._medicalExam.isSmoker) {
healthLevel += 10;
highMedicalRiskFlag = true;
}
And the last one is scoringGuide
:
export function score(candidate, medicalExam, scoringGuide) {
- return new Scorer(candidate, medicalExam).execute(scoringGuide);
+ return new Scorer(candidate, medicalExam, scoringGuide).execute();
}
export class Scorer {
- constructor(candidate, medicalExam) {
+ constructor(candidate, medicalExam, scoringGuide) {
this._candidate = candidate;
this._medicalExam = medicalExam;
+ this._scoringGuide = scoringGuide;
}
- execute(scoringGuide) {
+ execute() {
let result = 0;
let healthLevel = 0;
let highMedicalRiskFlag = false;
let certificationGrade = 'regular';
- if (scoringGuide.stateWithLowCertification(this._candidate.originState)) {
+ if (this._scoringGuide.stateWithLowCertification(this._candidate.originState)) {
certificationGrade = 'low';
result -= 5;
}
Now, on to the inner refactorings. Since our goal is to break the processing down into smaller chunks, we need to make the internal variables widely accessible, and we can accomplish this by turning them into class members. We start with result
:
execute() {
- let result = 0;
+ this._result = 0;
let healthLevel = 0;
let highMedicalRiskFlag = false;
let certificationGrade = 'regular';
if (this._scoringGuide.stateWithLowCertification(this._candidate.originState)) {
certificationGrade = 'low';
- result -= 5;
+ this._result -= 5;
}
// lots more code like this
- result -= Math.max(healthLevel - 5, 0);
+ this._result -= Math.max(healthLevel - 5, 0);
- return result;
+ return this._result;
}
}
Then healthLevel
:
execute() {
this._result = 0;
- let healthLevel = 0;
+ this._healthLevel = 0;
let highMedicalRiskFlag = false;
if (this._medicalExam.isSmoker) {
- healthLevel += 10;
+ this._healthLevel += 10;
highMedicalRiskFlag = true;
}
// lots more code like this
- this._result -= Math.max(healthLevel - 5, 0);
+ this._result -= Math.max(this._healthLevel - 5, 0);
return this._result;
}
And then highMedicalRiskFlag
:
execute() {
this._result = 0;
this._healthLevel = 0;
- let highMedicalRiskFlag = false;
+ this._highMedicalRiskFlag = false;
if (this._medicalExam.isSmoker) {
this._healthLevel += 10;
- highMedicalRiskFlag = true;
+ this._highMedicalRiskFlag = true;
}
let certificationGrade = 'regular';
And, finally, certificationGrade
:
- let certificationGrade = 'regular';
+ this._certificationGrade = 'regular';
if (this._scoringGuide.stateWithLowCertification(this._candidate.originState)) {
- certificationGrade = 'low';
+ this._certificationGrade = 'low';
this._result -= 5;
}
Now we're able to start the chunking. Our example is scoreSmoking
:
execute() {
- if (this._medicalExam.isSmoker) {
- this._healthLevel += 10;
- this._highMedicalRiskFlag = true;
- }
+ this.scoreSmoking();
}
+ scoreSmoking() {
+ if (this._medicalExam.isSmoker) {
+ this._healthLevel += 10;
+ this._highMedicalRiskFlag = true;
+ }
+ }
}
And that's it for this one! The function is now more ligible and can become more readable as well.
Below there's the commit history for the steps detailed above.
Commit SHA | Message |
---|---|
3310680 | introduce Scorer class |
c982d35 | call Scorer.execute at the body of score |
f8408b4 | move candidate argument to Score 's constructor |
091533c | move medicalExam to Score 's constructor |
20b234e | move scoringGuide to Score 's constructor |
10aa4f7 | make result an instance variable at Score |
eda5940 | make healthLevel an instance variable at Score |
d172cde | make highMedicalRiskFlag an instance variable at Score |
b979164 | make certificationGrade an instance variable at Score |
28a43a9 | extract scoreSmoking function at Score |
For the full commit history for this project, check the Commit History tab.