Skip to content

Working example with detailed commit history on the "remove subclass" refactoring based on Fowler's "Refactoring" book

License

Notifications You must be signed in to change notification settings

kaiosilveira/remove-subclass-refactoring

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

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


Remove Subclass

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.

Working example

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.

Test suite

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.

Steps

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.

Commit history

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.

About

Working example with detailed commit history on the "remove subclass" refactoring based on Fowler's "Refactoring" book

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project