ℹ️ This repository is part of my Refactoring catalog based on Fowler's book with the same title. Please see kaiosilveira/refactoring for more details.
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.
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"
}
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.
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!
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.
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"
}
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.
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!
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.