ℹ️ 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 Subclass with Fields
Before | After |
---|---|
class Person {
get genderCode() {
return 'X';
}
}
class Male extends Person {
get genderCode() {
return 'M';
}
}
class Female extends Person {
get genderCode() {
return 'F';
}
} |
class Person {
get genderCode() {
return this._genderCode;
}
} |
Inverse of: Replace Type Code with Subclasses
Sublcasses are useful and often provide a good degree of code separation, encapsulation, and isolation. Sometimes, though, as our understanding about the problem domain grows, we need to distil the model a bit, and this may imply pruning some leaves (a.k.a. removing some subclasses and / or cutting down some class hierarchies). This refactoring helps with that.
Our working example is straightfoward: we have a gender-based class hierarchy built around Person
. In the current state of the program, though, these subclasses (Male
and Female
) do nothing but overriding a genderCode
getter. Our goal here is to move this logic into the superclass and get rid of the subclasses.
Testing is quite simple here - we cover the gender code for each subclass:
describe('Person', () => {
it('should have genderCode as X', () => {
const person = new Person('Alex Doe');
expect(person.genderCode).toBe('X');
});
});
describe('Male', () => {
it('should have genderCode as M', () => {
const male = new Male('John Doe');
expect(male.genderCode).toBe('M');
});
});
describe('Female', () => {
it('should have genderCode as F', () => {
const female = new Female('Jane Doe');
expect(female.genderCode).toBe('F');
});
});
It also covers some basic behavior around loading data from a given input, which is omitted for brevity.
That's all we need to get started.
Instantiation control is one of the biggest concerns when it comes to removing something. So, to ensure we know where every subclass is created, we start by introducing a factory function to create them:
+export function createPerson(aRecord) {
+ let p;
+ switch (aRecord.gender) {
+ case 'M':
+ p = new Male(aRecord.name);
+ break;
+ case 'F':
+ p = new Female(aRecord.name);
+ break;
+ default:
+ p = new Person(aRecord.name);
+ break;
+ }
+ return p;
+}
We then update loadFromInput
to use our recently created factory:
export function loadFromInput(data) {
const result = [];
data.forEach(aRecord => {
- let p;
- switch (aRecord.gender) {
- case 'M':
- p = new Male(aRecord.name);
- break;
- case 'F':
- p = new Female(aRecord.name);
- break;
- default:
- p = new Person(aRecord.name);
- break;
- }
- return result.push(p);
+ return result.push(createPerson(aRecord));
});
return result;
}
The code is functional, bug ugly, so we inline some variables at createPerson
...
export function createPerson(aRecord) {
- let p;
switch (aRecord.gender) {
case 'M':
- p = new Male(aRecord.name);
- break;
+ return new Male(aRecord.name);
case 'F':
- p = new Female(aRecord.name);
- break;
+ return new Female(aRecord.name);
default:
- p = new Person(aRecord.name);
- break;
+ return new Person(aRecord.name);
}
- return p;
}
and inline the loop with a pipeline at loadFromInput
:
export function loadFromInput(data) {
- const result = [];
- data.forEach(aRecord => {
- return result.push(createPerson(aRecord));
- });
- return result;
+ return data.map(aRecord => createPerson(aRecord));
}
And as a last step before start the fun part of deleting things, we extract the isMale
logic into a function:
const numberOfMales = people.filter(p => p instanceof Male).length;
console.log(`Number of males: ${numberOfMales}`);
+
+export function isMale(person) {
+ return person instanceof Male;
+}
Now, on to the core work. We first move isMale
to Person
, as a getter:
export class Person {
+
+ get isMale() {
+ return this instanceof Male;
+ }
}
Just so we can use Person.isMale
instead of the isolated implementation (which can be safely removed):
-const numberOfMales = people.filter(p => p instanceof Male).length;
+const numberOfMales = people.filter(p => p.isMale).length;
console.log(`Number of males: ${numberOfMales}`);
-
-export function isMale(person) {
- return person instanceof Male;
-}
Moving on, we now add a genderCode
field as part of the base Person
class:
export class Person {
- constructor(name) {
+ constructor(name, genderCode) {
this._name = name;
+ this._genderCode = genderCode ?? 'X';
}
get genderCode() {
- return 'X';
+ return this._genderCode;
}
This will be the basis for us to remove the subclasses.
We can now return a Person
with gender M
to represent males in the createPerson
factory function:
export function createPerson(aRecord) {
switch (aRecord.gender) {
case 'M':
- return new Male(aRecord.name);
+ return new Person(aRecord.name, 'M');
case 'F':
return new Female(aRecord.name);
default:
But we also need to update the internal logic of Person.isMale
to look at the genderCode
field:
export class Person {
get isMale() {
- return this instanceof Male;
+ return this.genderCode === 'M';
}
}
And, finally, we can remove the Male
subclass:
-export class Male extends Person {
- get genderCode() {
- return 'M';
- }
-}
We repeat the process for Female
. First returning a Person
with genderCode F
:
export function createPerson(aRecord) {
case 'M':
return new Person(aRecord.name, 'M');
case 'F':
- return new Female(aRecord.name);
+ return new Person(aRecord.name, 'F');
default:
return new Person(aRecord.name);
}
And then removing the subclass:
-import { Person } from '../person/index.js';
-
-export class Female extends Person {
- get genderCode() {
- return 'F';
- }
-}
As a last touch, we can provide a X
as the default gender for a Person
in the createPerson
factory function:
export function createPerson(aRecord) {
case 'F':
return new Person(aRecord.name, 'F');
default:
- return new Person(aRecord.name);
+ return new Person(aRecord.name, 'X');
}
}
And remove the gender fallback at Person
's initialization:
export class Person {
constructor(name, genderCode) {
this._name = name;
- this._genderCode = genderCode ?? 'X';
+ this._genderCode = genderCode;
}
And that's all for this one. There's now only a base Person
class, with the gender code correctly implemented.
Below there's the commit history for the steps detailed above.
Commit SHA | Message |
---|---|
6f5a7cc | introduce function to create a person from a given record |
f40088c | update loadFromInput to use createPerson |
fc3841e | simplify implementation of createPerson |
2bb61a7 | simplify implementation of loadFromInput |
8cdf6dd | encapsulate check for male as isMale |
9dc30f4 | implement isMale getter at Person |
bbe54f1 | update client code to use Person.isMale instead of isolated implementation |
ce204a3 | add genderCode as part of Person 's initialization |
1ca3c84 | return Person with gender M for males at createPerson factory function |
90276b8 | update Person.isMale internal logic to check genderCode |
37df6bc | remove Male subclass |
a87da9f | return Person with gender F for females at createPerson factory function |
9ce8b40 | remove Female subclass |
b0abe9e | provide X as default gender at createPerson factory function |
14ec949 | remove gender fallback at Person initialization |
For the full commit history for this project, check the Commit History tab.