ℹ️ This repository is part of my Refactoring catalog based on Fowler's book with the same title. Please see kaiosilveira/refactoring for more details.
Before | After |
---|---|
class Clz {
get discountedTotal() {
return this._discountedTotal;
}
set discount(aNumber) {
const old = this._discount;
this._discount = aNumber;
this._discountedTotal += old - aNumber;
}
} |
class Clz {
get discountedTotal() {
return this._baseTotal - this._discount;
}
set discount(aNumber) {
this._discount = aNumber;
}
} |
In exact sciences, we build new things on top of truth, there's the only way we can be sure our work is correct. Therefore, the source of the truth itself is paramount for all programs, and it's not difficult to understand why having multiple sources of truth brings additional confusion and complexity. This refactoring helps remove these multiple sources.
We have two working examples, both extracted from the book, representing the same scenario: a program that holds a production plan. The first example looks into the case where there's only one source for the derived variable, while the second looks into the case where there are multiple sources.
For this scenario, the ProductionPlan
class looks like this:
export class ProductionPlan {
constructor() {
this._adjustments = [];
this._production = 0;
}
get production() {
return this._production;
}
applyAdjustment(anAdjustment) {
this._adjustments.push(anAdjustment);
this._production += anAdjustment.amount;
}
}
We want to get rid of the private _production
variable, transforming it into a getter that's dynamically calculated based on the adjustments
.
The test suite for the ProductionPlan
class covers the edge case where no adjustments were made, as well as a regular case where an adjustment was made, making sure that production
reflects this fact.
import { ProductionPlan } from './index';
describe('ProductionPlan', () => {
it('should return product as zero if no adjustments were made', () => {
const productionPlan = new ProductionPlan();
expect(productionPlan.production).toBe(0);
});
it('should return product as 10 if an adjusment of 10 monetary units were made', () => {
const productionPlan = new ProductionPlan();
productionPlan.applyAdjustment({ amount: 10 });
expect(productionPlan.production).toBe(10);
});
});
With these tests in place, we're safe to proceed.
We don't want to break the code that's currently working, so we start by introducing a calculatedProduction
getter. This getter will be used as a bridge between the current behavior and our envisioned behavior (where the derived _production
field no longer exists):
+++ b/src/single-source/index.js
@@ -8,6 +8,10 @@ export class ProductionPlan {
return this._production;
}
+ get calculatedProduction() {
+ return this._adjustments.reduce((sum, a) => sum + a.amount, 0);
+ }
+
applyAdjustment(anAdjustment) {
this._adjustments.push(anAdjustment);
this._production += anAdjustment.amount;
Then, as a sanity check, we introduce an assertion to make sure production
has the same value as calculatedProduction
:
+++ b/src/single-source/index.js
@@ -1,3 +1,5 @@
+import assert from 'node:assert';
+
export class ProductionPlan {
constructor() {
this._adjustments = [];
@@ -5,6 +7,7 @@ export class ProductionPlan {
}
get production() {
+ assert(this._production === this.calculatedProduction);
return this._production;
}
Our tests are still passing, so we can move forward with confidence. We can now return the value of calculatedProduction
at the production
getter:
+++ b/src/single-source/index.js
@@ -8,7 +8,7 @@ export class ProductionPlan {
get production() {
assert(this._production === this.calculatedProduction);
- return this._production;
+ return this.calculatedProduction;
}
get calculatedProduction() {
The assertion is no longer needed, so we remove it:
+++ b/src/single-source/index.js
@@ -1,5 +1,3 @@
-import assert from 'node:assert';
-
export class ProductionPlan {
constructor() {
this._adjustments = [];
@@ -7,7 +5,6 @@ export class ProductionPlan {
}
get production() {
- assert(this._production === this.calculatedProduction);
return this.calculatedProduction;
}
Finally, we can inline calculatedProudction
into production
getter:
+++ b/src/single-source/index.js
@@ -5,10 +5,6 @@ export class ProductionPlan {
}
get production() {
- return this.calculatedProduction;
- }
-
- get calculatedProduction() {
return this._adjustments.reduce((sum, a) => sum + a.amount, 0);
}
And then, we can remove the _production
field:
+++ b/src/single-source/index.js
@@ -1,7 +1,6 @@
export class ProductionPlan {
constructor() {
this._adjustments = [];
- this._production = 0;
}
get production() {
@@ -10,6 +9,5 @@ export class ProductionPlan {
applyAdjustment(anAdjustment) {
this._adjustments.push(anAdjustment);
- this._production += anAdjustment.amount;
}
}
And that's it!
Below there's the commit history for the steps detailed above.
Commit SHA | Message |
---|---|
fb1df73 | introduce calculatedProduction getter |
be30da4 | assert production has the same value as calculatedProduction |
e873ca2 | return calculatedProduction at production getter |
53d995a | remove assertion for production === calculatedProduction |
c14966c | inline calculatedProudction into production getter |
af4e704 | remove _production field |
For the full commit history for this project, check the Commit History tab.
This scenario has the same ProductionPlan
class but is a bit more involved since the class' constructor now also accepts an initial value for production
. The class looks like this:
export class ProductionPlan {
constructor(production) {
this._adjustments = [];
this._production = production;
}
get production() {
return this._production;
}
applyAdjustment(anAdjustment) {
this._adjustments.push(anAdjustment);
this._production += anAdjustment.amount;
}
}
The test suite for ProductionPlan
is fairly similar to the one in the previous example, with the only difference being a test that covers the case where we provide an initial value to production
. It looks like this:
import { ProductionPlan } from './index';
describe('multiple sources', () => {
describe('ProductionPlan', () => {
it('should return the predefined production if no adjustments were made', () => {
const productionPlan = new ProductionPlan(10);
expect(productionPlan.production).toBe(10);
});
it('should return the pre-defined proudction plus the adjustments made', () => {
const productionPlan = new ProductionPlan(10);
productionPlan.applyAdjustment({ amount: 10 });
expect(productionPlan.production).toBe(20);
});
});
});
With these tests in place, we can move forward.
We start by splitting the production
variable:
+++ b/src/multiple-sources/index.js
@@ -1,15 +1,16 @@
export class ProductionPlan {
constructor(production) {
+ this._initialProduction = production;
+ this._productionAccumulator = 0;
this._adjustments = [];
- this._production = production;
}
get production() {
- return this._production;
+ return this._initialProduction + this._productionAccumulator;
}
applyAdjustment(anAdjustment) {
this._adjustments.push(anAdjustment);
- this._production += anAdjustment.amount;
+ this._productionAccumulator += anAdjustment.amount;
}
}
Then we can proceed with the same approach used in the previous example. We introduce a calculatedProduction
getter:
+++ b/src/multiple-sources/index.js
@@ -9,6 +9,10 @@ export class ProductionPlan {
return this._initialProduction + this._productionAccumulator;
}
+ get calculatedProduction() {
+ return this._adjustments.reduce((sum, a) => sum + a.amount, 0);
+ }
+
applyAdjustment(anAdjustment) {
this._adjustments.push(anAdjustment);
this._productionAccumulator += anAdjustment.amount;
Then, we add an assertion to make sure productionAccumulator
equals calculatedProduction
:
+++ b/src/multiple-sources/index.js
@@ -1,3 +1,5 @@
+import assert from 'node:assert';
+
export class ProductionPlan {
constructor(production) {
this._initialProduction = production;
@@ -6,6 +8,7 @@ export class ProductionPlan {
}
get production() {
+ assert(this._productionAccumulator === this.calculatedProduction);
return this._initialProduction + this._productionAccumulator;
}
Then we start using calculatedProduction
at the production
getter:
+++ b/src/multiple-sources/index.js
@@ -9,7 +9,7 @@ export class ProductionPlan {
get production() {
assert(this._productionAccumulator === this.calculatedProduction);
- return this._initialProduction + this._productionAccumulator;
+ return this._initialProduction + this.calculatedProduction;
}
get calculatedProduction() {
And we can now safely remove the assertion:
+++ b/src/multiple-sources/index.js
@@ -1,5 +1,3 @@
-import assert from 'node:assert';
-
export class ProductionPlan {
constructor(production) {
this._initialProduction = production;
@@ -8,7 +6,6 @@ export class ProductionPlan {
}
get production() {
- assert(this._productionAccumulator === this.calculatedProduction);
return this._initialProduction + this.calculatedProduction;
}
Finally, we can inline calculatedProduction
:
+++ b/src/multiple-sources/index.js
@@ -6,11 +6,7 @@ export class ProductionPlan {
}
get production() {
- return this._initialProduction + this.calculatedProduction;
- }
-
- get calculatedProduction() {
- return this._adjustments.reduce((sum, a) => sum + a.amount, 0);
+ return this._initialProduction + this._adjustments.reduce((sum, a) => sum + a.amount, 0);
}
applyAdjustment(anAdjustment) {
And that's it!
Below there's the commit history for the steps detailed above.
Commit SHA | Message |
---|---|
0f0eae1 | split production variable |
5c2466b | introduce calculatedProduction getter |
744a4a1 | assert productionAccumulator equals calculatedProduction |
8c5d077 | return calculatedProduction at production getter |
20129d6 | remove assertion for calculatedProduction |
a31ea87 | inline calculatedProduction |
4a362f1 | remove _productionAccumulator field |
For the full commit history for this project, check the Commit History tab.