Skip to content

Working example with detailed commit history on the "Introduce Parameter Object" refactoring from the Refactoring book by Fowler

Notifications You must be signed in to change notification settings

kaiosilveira/introduce-parameter-object-refactoring

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 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.


Introduce Parameter Object

Before After
function amountInvoiced(startDate, endDate) {
  /*...*/
}

function amountReceived(startDate, endDate) {
  /*...*/
}

function amountOverdue(startDate, endDate) {
  /*...*/
}
function amountInvoiced(aDateRange) {
  /*...*/
}

function amountReceived(aDateRange) {
  /*...*/
}

function amountOverdue(aDateRange) {
  /*...*/
}

Oftentimes we see a group of parameters being used repeatedly as arguments for multiple functions. These groups are often suggesting a hidden structure inside the project's domain. When this pattern is detected, we can use Introduce Parameter Object to create a class based on these parameters and use it instead. This refactoring helps with this process.

Working example

As our working example for this refactoring, we have the readingsOutsideRange function, responsible for finding the readings that fall outside of a specified range. This function has the max and min parameters, which hide a bigger, NumberRange structure that can be created to aid in this comparison. The core initial code for the refactoring is:

function readingsOutsideRange(station, min, max) {
  return station.readings.filter(r => r.temp < min || r.temp > max);
}

Test suite

A simple test suite with one test was put in place to make sure the readingsOutsideRange function was behaving as expected. Corner cases and validations were left out for simplicity. The test suite looks like this:

describe('readingsOutsideRange', () => {
  it('should return all readings that fall outside of the specified range', () => {
    const min = 10;
    const max = 30;
    const range = new NumberRange(min, max);
    const station = {
      name: 'ZB1',
      readings: [
        { temp: 9, time: '2016-11-10 09:10' },
        { temp: 20, time: '2016-11-10 09:10' },
        { temp: 31, time: '2016-11-10 09:50' },
      ],
    };

    const results = readingsOutsideRange(station, range);

    expect(results).toHaveLength(2);
    expect(results.some(r => r.temp === 9)).toBeTruthy();
    expect(results.some(r => r.temp === 31)).toBeTruthy();
  });
});

Steps

We start by introducing a NumberRange class:

diff --git a/src/number-range/index.js b/src/number-range/index.js
@@ -0,0 +1,15 @@
+class NumberRange {
+  constructor(min, max) {
+    this._data = { min, max };
+  }
+
+  get min() {
+    return this._data.min;
+  }
+
+  get max() {
+    return this._data.max;
+  }
+}
+
+module.exports = { NumberRange };

diff --git a/src/number-range/index.test.js b/src/number-range/index.test.js
@@ -0,0 +1,13 @@
+const { NumberRange } = require('.');
+
+describe('NumberRange', () => {
+  it('should have a min and a max', () => {
+    const min = 10;
+    const max = 20;
+
+    const range = new NumberRange(min, max);
+
+    expect(range.min).toEqual(min);
+    expect(range.max).toEqual(max);
+  });
+});

Then, we add a range as a parameter to readingsOutsideRange...

diff --git a/src/readings-outside-range/index.js b/src/readings-outside-range/index.js
@@ -1,4 +1,4 @@
-function readingsOutsideRange(station, min, max) {
+function readingsOutsideRange(station, min, max, range) {
   return station.readings.filter(r => r.temp < min || r.temp > max);
 }

...and update the caller to instantiate a NumberRange object and pass it down to readingsOutsideRange:

diff --git a/src/caller.js b/src/caller.js
@@ -1,12 +1,14 @@
 const station = require('./data');
+const { NumberRange } = require('./number-range');
 const readingsOutsideRange = require('./readings-outside-range');

 const operatingPlan = { temperatureFloor: 50, temperatureCeiling: 55 };
-
+const range = new NumberRange(operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling);
 const alerts = readingsOutsideRange(
   station,
   operatingPlan.temperatureFloor,
-  operatingPlan.temperatureCeiling
+  operatingPlan.temperatureCeiling,
+  range
 );

 console.log(alerts);

Then, we start using the max field from the range obj at readingsOutsideRange:

diff --git a/src/readings-outside-range/index.js b/src/readings-outside-range/index.js
@@ -1,5 +1,5 @@
 function readingsOutsideRange(station, min, max, range) {
-  return station.readings.filter(r => r.temp < min || r.temp > max);
+  return station.readings.filter(r => r.temp < min || r.temp > range.max);
 }

 module.exports = readingsOutsideRange;

diff --git a/src/readings-outside-range/index.test.js b/src/readings-outside-range/index.test.js
@@ -1,9 +1,11 @@
 const readingsOutsideRange = require('.');
+const { NumberRange } = require('../number-range');

 describe('readingsOutsideRange', () => {
   it('should return all readings that fall outside of the specified range', () => {
     const min = 10;
     const max = 30;
+    const range = new NumberRange(min, max);
     const station = {
       name: 'ZB1',
       readings: [
       ],
     };

-    const results = readingsOutsideRange(station, min, max);
+    const results = readingsOutsideRange(station, min, max, range);

     expect(results).toHaveLength(2);
     expect(results.some(r => r.temp === 9)).toBeTruthy();

And then we can remove the now unused max parameter from readingsOutsideRange:

diff --git a/src/caller.js b/src/caller.js
@@ -4,11 +4,6 @@
 const readingsOutsideRange = require('./readings-outside-range');

 const operatingPlan = { temperatureFloor: 50, temperatureCeiling: 55 };
 const range = new NumberRange(operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling);
-const alerts = readingsOutsideRange(
-  station,
-  operatingPlan.temperatureFloor,
-  operatingPlan.temperatureCeiling,
-  range
-);
+const alerts = readingsOutsideRange(station, operatingPlan.temperatureFloor, range);

 console.log(alerts);

diff --git a/src/readings-outside-range/index.js b/src/readings-outside-range/index.js
@@ -1,4 +1,4 @@
-function readingsOutsideRange(station, min, max, range) {
+function readingsOutsideRange(station, min, range) {
   return station.readings.filter(r => r.temp < min || r.temp > range.max);
 }

diff --git a/src/readings-outside-range/index.test.js b/src/readings-outside-range/index.test.js
@@ -15,7 +15,7 @@ describe('readingsOutsideRange', () => {
       ],
     };

-    const results = readingsOutsideRange(station, min, max, range);
+    const results = readingsOutsideRange(station, min, range);

     expect(results).toHaveLength(2);
     expect(results.some(r => r.temp === 9)).toBeTruthy();

The same happens for the min parameter. We first start using it from the range:

diff --git a/src/readings-outside-range/index.js b/src/readings-outside-range/index.js
@@ -1,5 +1,5 @@
 function readingsOutsideRange(station, min, range) {
-  return station.readings.filter(r => r.temp < min || r.temp > range.max);
+  return station.readings.filter(r => r.temp < range.min || r.temp > range.max);
 }

 module.exports = readingsOutsideRange;

And then we remove it from readingsOutsideRange and update the callers:

diff --git a/src/caller.js b/src/caller.js
@@ -4,6 +4,6 @@
 const readingsOutsideRange = require('./readings-outside-range');

 const operatingPlan = { temperatureFloor: 50, temperatureCeiling: 55 };
 const range = new NumberRange(operatingPlan.temperatureFloor, operatingPlan.temperatureCeiling);
-const alerts = readingsOutsideRange(station, operatingPlan.temperatureFloor, range);
+const alerts = readingsOutsideRange(station, range);

 console.log(alerts);

diff --git a/src/readings-outside-range/index.js b/src/readings-outside-range/index.js
@@ -1,4 +1,4 @@
-function readingsOutsideRange(station, min, range) {
+function readingsOutsideRange(station, range) {
   return station.readings.filter(r => r.temp < range.min || r.temp > range.max);
 }

diff --git a/src/readings-outside-range/index.test.js b/src/readings-outside-range/index.test.js
@@ -15,7 +15,7 @@ describe('readingsOutsideRange', () => {
       ],
     };

-    const results = readingsOutsideRange(station, min, range);
+    const results = readingsOutsideRange(station, range);

     expect(results).toHaveLength(2);
     expect(results.some(r => r.temp === 9)).toBeTruthy();

At this point, the refactoring is done. But we can move forward and make use of our new NumberRange class, adding some behavior to it. We can add a contains method, so it now can tell us whether a number is or isn't inside the range:

diff --git a/src/number-range/index.js b/src/number-range/index.js
@@ -10,6 +10,10 @@
class NumberRange {
   get max() {
     return this._data.max;
   }
+
+  contains(n) {
+    return n >= this.min && n <= this.max;
+  }
 }

 module.exports = { NumberRange };

diff --git a/src/number-range/index.test.js b/src/number-range/index.test.js
@@ -1,13 +1,36 @@
 const { NumberRange } = require('.');

 describe('NumberRange', () => {
-  it('should have a min and a max', () => {
-    const min = 10;
-    const max = 20;
+  const min = 10;
+  const max = 20;

+  it('should have a min and a max', () => {
     const range = new NumberRange(min, max);

     expect(range.min).toEqual(min);
     expect(range.max).toEqual(max);
   });
+
+  describe('contains', () => {
+    it('should return true if a number falls inside of the specified range', () => {
+      const range = new NumberRange(min, max);
+      expect(range.contains(15)).toEqual(true);
+    });
+
+    it('should return false if a number falls outside of the specified range', () => {
+      const range = new NumberRange(min, max);
+      expect(range.contains(9)).toEqual(false);
+      expect(range.contains(21)).toEqual(false);
+    });
+
+    it('should contain the lower end of the range', () => {
+      const range = new NumberRange(min, max);
+      expect(range.contains(min)).toEqual(true);
+    });
+
+    it('should contain the upper end of the range', () => {
+      const range = new NumberRange(min, max);
+      expect(range.contains(max)).toEqual(true);
+    });
+  });
 });

And, finally, we can use the NumberRange.contains method at readingsOutsideRange:

diff --git a/src/readings-outside-range/index.js b/src/readings-outside-range/index.js
@@ -1,5 +1,5 @@
 function readingsOutsideRange(station, range) {
-  return station.readings.filter(r => r.temp < range.min || r.temp > range.max);
+  return station.readings.filter(r => !range.contains(r.temp));
 }

 module.exports = readingsOutsideRange;

And that's it for this refactoring!

Commit history

Commit SHA Message
1fec692 introduce NumberRange class
8b4fed3 add range as a parameter to readingsOutsideRange
58182d4 update caller to instantiate a NumberRange object and pass it down to readingsOutsideRange
c95a67f start using the max field from the range obj at readingsOutsideRange
8db05b1 remove now unused max parameter from readingsOutsideRange
7befc10 start using the min field from the range obj at readingsOutsideRange
2932a1a remove now unused min field from readingsOutsideRange and update callers accordingly
61ec951 add contains method to NumberRange
88378c7 use NumberRange.contains method at readingsOutsideRange

You can also see the full commit history in the Commit History tab.

About

Working example with detailed commit history on the "Introduce Parameter Object" refactoring from the Refactoring book by Fowler

Topics

Resources

Stars

Watchers

Forks

Sponsor this project