I've been reading Refactoring (2nd) by Martin Fowler. In the first chapter, he shows an example of refactoring a function where he extracts other functions from it and places them inside that function as nested functions.
Specifically, starting from this function:
function statement(invoice, plays) {
let totalAmount = 0;
let volumeCredits = 0;
let result = `Statement for ${invoice.customer}\n`;
const format = new Intl.NumberFormat("en-US",
{
style: "currency", currency: "USD",
minimumFractionDigits: 2
}).format;
for (let perf of invoice.performances) {
const play = plays[perf.playID];
let thisAmount = 0;
switch (play.type) {
case "tragedy":
thisAmount = 40000;
if (perf.audience > 30) {
thisAmount += 1000 * (perf.audience - 30);
}
break;
case "comedy":
thisAmount = 30000;
if (perf.audience > 20) {
thisAmount += 10000 + 500 * (perf.audience - 20);
}
thisAmount += 300 * perf.audience;
break;
default:
throw new Error(`unknown type: ${play.type}`);
}
volumeCredits += Math.max(perf.audience - 30, 0);
if ("comedy" === play.type) volumeCredits += Math.floor(perf.audience / 5);
result += ` ${play.name}: ${format(thisAmount / 100)} (${perf.audience} seats)\n`;
totalAmount += thisAmount;
}
result += `Amount owed is ${format(totalAmount / 100)}\n`;
result += `You earned ${volumeCredits} credits\n`;
return result;
}
he arrived at this function:
function statement(invoice, plays) {
let result = `Statement for ${invoice.customer}\n`;
for (let perf of invoice.performances) {
result += ` ${playFor(perf).name}: ${usd(amountFor(perf))} (${perf.audience} seats)\n`;
}
result += `Amount owed is ${usd(totalAmount())}\n`;
result += `You earned ${totalVolumeCredits()} credits\n`;
return result;
function totalAmount() {
let result = 0;
for (let perf of invoice.performances) {
result += amountFor(perf);
}
return result;
}
function totalVolumeCredits() {
let result = 0;
for (let perf of invoice.performances) {
result += volumeCreditsFor(perf);
}
return result;
}
function usd(aNumber) {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2
}).format(aNumber / 100);
}
function volumeCreditsFor(aPerformance) {
let result = 0;
result += Math.max(aPerformance.audience - 30, 0);
if ("comedy" === playFor(aPerformance).type) result += Math.floor(aPerformance.audience / 5);
return result;
}
function playFor(aPerformance) {
return plays[aPerformance.playID];
}
function amountFor(aPerformance) {
let result = 0;
switch (playFor(aPerformance).type) {
case "tragedy":
result = 40000;
if (aPerformance.audience > 30) {
result += 1000 * (aPerformance.audience - 30);
}
break;
case "comedy":
result = 30000;
if (aPerformance.audience > 20) {
result += 10000 + 500 * (aPerformance.audience - 20);
}
result += 300 * aPerformance.audience;
break;
default:
throw new Error(`unknown type: ${playFor(aPerformance).type}`);
}
return result;
}
}
Looks simple, but now my question is how to make this refactoring if the statement
function is a method of a class instead, and many of the extracted functions need to call instance properties through this.propertyName
?
In this case, we can't extract them into nested functions because this
in a nested function could refer to a function, not an instance of the object. We can create an arrow function and assign it to the constant in which the meaning of this
will be preserved, but then such functions would not be hoisted, and we'd need to declare them at the beginning of the statement
method, which would be ugly.
So, should we extract all those extra functions into separate (private) methods of this class or is there a better way?
Let's use an example to illustrate this. Imagine we have a simplified class:
class StatementGenerator {
constructor(plays) {
this.plays = plays;
}
public statement(invoice) {
let totalAmount = 0;
for (let perf of invoice.performances) {
const play = this.plays[perf.playID];
// Some business logic
}
return `Amount owed is ${format(totalAmount / 100)}\n`;
}
}
And then want to refactor this line into it's own function/method:
const play = this.plays[perf.playID];
We can either extract it into a method:
class StatementGenerator {
constructor(plays) {
this.plays = plays;
}
public statement(invoice) {
let totalAmount = 0;
for (let perf of invoice.performances) {
const play = this.playFor(perf);
// Some business logic
}
return `Amount owed is ${format(totalAmount / 100)}\n`;
}
private playFor(aPerformance) {
return this.plays[aPerformance.playID]
}
}
Or to extract it into a nested arrow function:
class StatementGenerator {
constructor(plays) {
this.plays = plays;
}
public statement(invoice) {
const playFor = (aPerformance) => {
return this.plays[aPerformance.playID]
}
let totalAmount = 0;
for (let perf of invoice.performances) {
const play = playFor(perf);
// Some business logic
}
return `Amount owed is ${format(totalAmount / 100)}\n`;
}
}
this
being your instance reference inside all methods, I advise not using the method syntax for defining methods but fields that you assign arrow functions to (e.g.public statement = (invoice) => ...
and so on). With this approach the transpiler will bind all of your methods to the this object at the beginning of the constructor, so you don't need to worry about method order as you had to with const arrow functions because methods are bound tothis
in runtime.