Skip to content

Working example with detailed commit history on the "replace type code with" refactoring based on Fowler's "Refactoring" book

License

Notifications You must be signed in to change notification settings

kaiosilveira/replace-type-code-with-subclasses-refactoring

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

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


Replace Type Code With Subclasses

Subsumes: Replace Type Code with State / Strategy Subsumes: Extract Subclass

Before After
function createEmployee(name, type) {
  return new Employee(name, type);
}
function createEmployee(name, type) {
  switch (type) {
    case 'engineer':
      return new Engineer(name);
    case 'salesman':
      return new Salesman(name);
    case 'manager':
      return new Manager(name);
  }
}

Inverse of: Remove Subclass

Many class hierarchies fall short from their idiomacy (is that a word?) and intended goal of abstracting away details at the moment they start including hints to the client code regarding what will happen. This is specially true when you have enum-like flags passed around during construction. This refactoring helps bringing abstraction back to its best.

Example: subclassing by employee type

Our first working example is a program that contains a base Employee class, that receives a type when it's constructed. To introduce a degree of polymorphism, therefore allowing for more flexible, particular behaviors in the future, we want to introduce a series of subclasses of Employee, creating the following class hierarchy:

classDiagram
    class Employee
    Employee <|-- Engineer
    Employee <|-- Salesman
    Employee <|-- Manager

    class Employee {
        get name
    }

    class Engineer {
        get type # "engineer"
    }

    class Salesman {
        get type # "salesman"
    }

    class Manager {
        get type # "manager"
    }
Loading

Test suite

The starting test suite for this example covers the basic behavior of Employee, but receives considerable expansion as we go. Please check the source code directly for details.

Steps

We start by exposing the type field as a getter at Employee. This will serve as a basis for overriding behavior in the subclasses:

export class Employee {
+  get type() {
+    return this._type;
+  }

Then, we update Employee.toString to use the type getter instead of private field:

export class Employee {
   toString() {
-    return `${this._name} (${this._type})`;
+    return `${this._name} (${this.type})`;
   }

And now we can start introducing the subclasses. We start with Engineer:

+export class Engineer extends Employee {
+  constructor(name) {
+    super(name, `engineer`);
+  }
+
+  get type() {
+    return `engineer`;
+  }
+}

From this moment onwards, we need a way to return a subclass based on the type argument. This logic would make the constructor a bit messy, so we introduce a createEmployee factory function:

+export const createEmployee = (name, type) => {
+  return new Employee(name, type);
+};

We, then, start returning an Engineer subclass when the type is engineer:

 export const createEmployee = (name, type) => {
+  switch (type) {
+    case 'engineer':
+      return new Engineer(name);
+  }
   return new Employee(name, type);
 };

And now we can generalize the case. We do the same steps for Salesman, first creating it:

+export class Salesman extends Employee {
+  constructor(name) {
+    super(name, `salesman`);
+  }
+
+  get type() {
+    return `salesman`;
+  }
+}

and then returning it:

 export const createEmployee = (name, type) => {
   switch (type) {
     case 'engineer':
       return new Engineer(name);
+    case 'salesman':
+      return new Salesman(name);
   }
   return new Employee(name, type);
 };

And then for Manager. Creating it first:

+export class Manager extends Employee {
+  constructor(name) {
+    super(name, `manager`);
+  }
+
+  get type() {
+    return `manager`;
+  }
+}

and then returning it at createEmployee:

export const createEmployee = (name, type) => {
  switch (type) {
     case 'engineer':
       return new Engineer(name);
     case 'salesman':
       return new Salesman(name);
+    case 'manager':
+      return new Manager(name);
   }
   return new Employee(name, type);
}

Finally, since all subclasses already have their type getters well defined, we can remove this getter from the base Employee superclass:

export class Employee {
   constructor(name, type) {
     this.validateType(type);
     this._name = name;
-    this._type = type;
-  }
-
-  get type() {
-    return this._type;
   }

We can also get rid of the type validation at the superclass level, since it's now handled by the createEmployee factory function:

 export class Employee {
   constructor(name, type) {
-    this.validateType(type);
     this._name = name;
   }
-  validateType(arg) {
-    if (![`engineer`, `manager`, `salesman`].includes(arg))
-      throw new Error(`Employee cannot be of type ${arg}`);
-  }

And since we want to preserve the "invalid type" guard clause, we implement it at createEmployee (please note that we first removed the validation and then reintroduced it somewhere else, which for real-life scenarios would be a mistake and would have to happen in the reverse other instead):

export const createEmployee = (name, type) => {
    switch (type) {
     case 'engineer':
       return new Engineer(name);
     case 'salesman':
       return new Salesman(name);
     case 'manager':
       return new Manager(name);
+    default:
+      throw new Error(`Employee cannot be of type ${type}`);
   }
-  return new Employee(name, type);
 };

Last, but not least, we can remove the type argument altogether from the base Employee superclass and all subclasses:

diff --git Employee...
 export class Employee {
-  constructor(name, type) {
+  constructor(name) {
     this._name = name;
   }

diff --git Engineer...
 export class Engineer extends Employee {
   constructor(name) {
-    super(name, `engineer`);
+    super(name);
   }

diff --git Manager...
 export class Manager extends Employee {
   constructor(name) {
-    super(name, `manager`);
+    super(name);
   }

diff --git Salesman...
 export class Salesman extends Employee {
   constructor(name) {
-    super(name, `salesman`);
+    super(name);
   }

And that's it!

Commit history

Below there's the commit history for the steps detailed above.

Commit SHA Message
2dd4c4e expose type field as a getter at Employee
1e641f6 update Employee.toString to use type getter instead of private field
33bf59f introduce Engineer subclass
2b6627d introduce createEmployee factory function
798421f return Engineer subclass when type is engineer at createEmployee
593bf84 introduce Salesman subclass
1e74c99 return Salesman subclass when type is salesman at createEmployee
963371d introduce Manager subclass
c3fecd9 return Manager subclass when type is manager at createEmployee
9b13d25 remove type getter from base Employee superclass
f2cbc5e remove type validation at base Employee superclass
1765d29 throw error if employee type is invalid at createEmployee
813c8c3 remove type argument from base Employee superclass

For the full commit history for this project, check the Commit History tab.

Example: Using indirect inheritance

This example is similar to the first one, but our approach here is different: instead of subclassing Employee, we're going to do it only for type. This allows for the same flexible, particular behaviors mentioned before, but only for the portion that touches the type aspect of employees. The target class hierarchy is:

classDiagram
    class EmployeeType
    EmployeeType <|-- Engineer
    EmployeeType <|-- Salesman
    EmployeeType <|-- Manager

    class Engineer {
        toString() # "engineer"
    }

    class Salesman {
        toString() # "salesman"
    }

    class Manager {
        toString() # "manager"
    }
Loading

Test suite

The starting test suite for this example covers the basic behavior of Employee, but receives considerable expansion as we go. Please check the source code directly for details.

Steps

We start by introducing the EmployeeType superclass:

+export class EmployeeType {
+  constructor(value) {
+    this.value = value;
+  }
+
+  toString() {
+    return this.value;
+  }
+}

Now, since we want to continue using the Employee.type class the same way as before, we need to update the inner workings of Employee so it contains a string representation of type:

class Employee {
+  get typeString() {
+    return this._type.toString();
+  }
+
   get capitalizedType() {
-    return this._type.charAt(0).toUpperCase() + this._type.substr(1).toLowerCase();
+    return this.typeString.charAt(0).toUpperCase() + this.typeString.substr(1).toLowerCase();
   }

We need to do the same for the input of validateType in the constructor:

class Employee {
   constructor(name, type) {
-    this.validateType(type);
+    this.validateType(type.toString());
     this._name = name;
     this._type = type;
   }

And now we're ready to start the subclassing work, which is pretty much identical to what we did in the previous example. We start by introducing a createEmployeeType factory function:

+function createEmployeeType(value) {
+  if (!['engineer', 'manager', 'sales'].includes(value)) {
+    throw new Error(`Employee cannot be of type ${value}`);
+  }
+
+  return new EmployeeType(value);
+}

Then we start introducing the subclasses. We start with Engineer:

+export class Engineer extends EmployeeType {
+  toString() {
+    return 'engineer';
+  }
+}

and start returning it it at createEmployeeType:

function createEmployeeType(value) {
+  switch (value) {
+    case 'engineer':
+      return new Engineer();
+  }
 }

Then Manager:

+export class Manager extends EmployeeType {
+  toString() {
+    return 'manager';
+  }
+}

and start returning it:

function createEmployeeType(value) {
   switch (value) {
     case 'engineer':
       return new Engineer();
+    case 'manager':
+      return new Manager();
   }

And, finally, Salesman:

+export class Salesman extends EmployeeType {
+  toString() {
+    return 'salesman';
+  }
+}

and start returning it:

function createEmployeeType(value) {
       return new Engineer();
     case 'manager':
       return new Manager();
+    case 'sales':
+      return new Salesman();
   }

Now on to bit embellishments, we move the type validation to the default clause of the switch statement:

function createEmployeeType(value) {
-  if (!['engineer', 'manager', 'sales'].includes(value)) {
-    throw new Error(`Employee cannot be of type ${value}`);
-  }
-
   switch (value) {
     case 'engineer':
       return new Engineer();
     case 'manager':
       return new Manager();
     case 'sales':
       return new Salesman();
+    default:
+      throw new Error(`Employee cannot be of type ${value}`);
   }

   return new EmployeeType(value);

And then safely remove the now unreacheable base EmployeeType instantiation:

function createEmployeeType(value) {
     default:
       throw new Error(`Employee cannot be of type ${value}`);
   }
-
-  return new EmployeeType(value);
 }

Now, with the class hierarchy in place, we can start adding behavior to it. An example is the capitalization logic, which can be moved to EmployeeType:

class EmployeeType {
+  get capitalizedName() {
+    return this.toString().charAt(0).toUpperCase() + this.toString().slice(1);
+  }

relieving Employee from this burden:

class Employee {
-  get capitalizedType() {
-    return this.typeString.charAt(0).toUpperCase() + this.typeString.substr(1).toLowerCase();
-  }

   toString() {
-    return `${this._name} (${this.capitalizedType})`;
+    return `${this._name} (${this.type.capitalizedName})`;
   }
 }

And that's all for this one!

Commit history

Below there's the commit history for the steps detailed above.

Commit SHA Message
de31941 introduce EmployeeType
62b6af4 update Employee.capizaliedType to use typeString instead of type
57d116c prepare Employee to reveive type as an object
55970e0 introduce createEmployeeType factory function
26f75ff introduce Engineer subclass
2334142 return instance of Engineer if type is 'engineer' at createEmployeeType
ca572e8 introduce Manager subclass
390aae7 return instance of Manager if type is 'manager' at createEmployeeType
94a771c introduce Salesman subclass
e23e4ae return instance of Salesman if type is 'sales' at createEmployeeType
ef7644e throw erorr if employee type is invalid at createEmployeeType
5f509a5 remove now unreacheable base EmployeeType instantiation at createEmployeeType
726f5b7 add capitalization logic to EmployeeType
573c355 update Employee to use delegated capitalized type

For the full commit history for this project, check the Commit History tab.

About

Working example with detailed commit history on the "replace type code with" refactoring based on Fowler's "Refactoring" book

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project