Skip to content

Working example with detailed commit history on the "replace function with command" refactoring based on Fowler's "Refactoring" book

License

Notifications You must be signed in to change notification settings

kaiosilveira/replace-function-with-command-refactoring

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Continuous Integration

ℹ️ This repository is part of my Refactoring catalog based on Fowler's book with the same title. Please see kaiosilveira/refactoring for more details.


Replace Function With Command

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.

Working example

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.

Test suite

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.

Steps

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.

Commit history

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.

About

Working example with detailed commit history on the "replace function with command" refactoring based on Fowler's "Refactoring" book

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

 

Packages

No packages published