5 Minutes or Less: Refactoring
Learn refactorings in just a few minutes - putting words to those neat, tidy things you hopefully do to your code
Refactoring is the process of changing and restructuring code without affecting its behavior. It's also a skill you'll need to have if you want to become senior as a developer/software engineer.
Nowadays we can literally ask a computer to write our code, which will undoubtedly lead to more code in the future. Also, because more and more junior developers enter the field, and considering ~80% of budgets for IT projects are in maintenance, there just is no way around the fact that being good at cleaning and maintaining code is a critical skill for getting better in this field. The ability to detect and mitigate bad code is only going to grow over time.
In this repo I've compiled resources, examples, and tables from the seminal book on refactoring, called... you guessed it: Refactoring: Improving the Design of Existing Code, written by Martin Fowler in 1999, now recently out in its second edition with code examples given in JavaScript. If you're a software engineer with any self-respect you owe it to yourself to get yourself a copythis book. Fowler is funny too, making the book pretty easy to digest. The reason it's a book, rather than a very long Markdown file like this, is because Fowler goes into detail about considerations and things to think about, for every single pattern. Buy it!
Why this repo?
This is part of a series of bit-size educational resources I am putting together under the name "Five Minutes or Less", because you should be able to learn something new (every day?) in just a few minutes. I'm trying to make it as easy as possible to get such nuggets of new information in various software subjects.
Of course, in this case, you'd not learn all refactorings in 5 minutes, but one or a few would be doable, as most aren't very complicated!
Five Minutes or Less
Tooling recommendations
If you're a Visual Studio Code user, you should know there are already some built-in functionality to assist with refactoring.
And while there are several plugins for refactoring, my favorite is Abracadabra, refactor this!. Funky name, right? You can see some of its full refactoring functionality at this page.
Other resources
There's also a fantastic site at refactoring.guru that goes into far more detail than the list here. Highly recommended, and it has both lots of free content and paid material as well if you fancy going deep.
Code smells and relevant refactorings
This list is reproduced from the book.
Refactorings
All these refactorings are generated by ChatGPT 3.5 in August 2023, using the prompt:
Give me fun, original, minimalist TypeScript examples for the following refactorings from Martin Fowler's book (2nd edition from 2018): [LIST]
To the best of my ability and knowledge they are validated as correct examples. I'll happily update if you find anything strange!
Also see Martin Fowler's Refactoring catalog for more.
Running the code
You can either clone this repo or simply copy the individual examples into the TypeScript playground.
- Change Function Declaration (Rename Function; Rename Method; Add Parameter; Remove Parameter; Change Signature)
- Change Reference to Value
- Change Value to Reference
- Collapse Hierarchy
- Combine Functions into Class
- Combine Functions into Transform
- Consolidate Conditional Expression
- Decompose Conditional
- Encapsulate Collection
- Encapsulate Record (Replace Record with Data Class)
- Encapsulate Variable (Encapsulate Field; Self-Encapsulate Field)
- Extract Class
- Extract Function
- Extract Superclass
- Extract Variable (Introduce Explaining Variable)
- Hide Delegate
- Inline Class
- Inline Function (Inline Method)
- Inline Variable (Inline Temp)
- Introduce Assertion
- Introduce Parameter Object
- Introduce Special Case (Introduce Null Object)
- Move Field
- Move Function (Move Method)
- Move Statements into Function
- Move Statements to Callers
- Parameterize Function (Parameterize Method)
- Preserve Whole Object
- Pull Up Constructor Body
- Pull Up Field
- Pull Up Method
- Push Down Field
- Push Down Method
- Remove Dead Code
- Remove Flag Argument (Replace Parameter with Explicit Methods)
- Remove Middle Man
- Remove Setting Method
- Remove Subclass (Replace Subclass with Fields)
- Rename Field
- Rename Variable
- Replace Command with Function
- Replace Conditional with Polymorphism
- Replace Constructor with Factory Function (Replace Constructor with Factory Method)
- Replace Derived Variable with Query
- Replace Function with Command (Replace Method with Method Object)
- Replace Inline Code with Function Call
- Replace Loop with Pipeline
- Replace Nested Conditional with Guard Clauses
- Replace Parameter with Query (Replace Parameter with Method)
- Replace Primitive with Object (Replace Data Value with Object; Replace Type Code with Class)
- Replace Query with Parameter
- Replace Subclass with Delegate
- Replace Superclass with Delegate (Replace Inheritance with Delegation)
- Replace Temp with Query
- Replace Type Code with Subclasses (Extract Subclass; Replace Type Code with State/Strategy)
- Separate Query from Modifier
- Slide Statements (Consolidate Duplicate Conditional Fragments)
- Split Loop
- Split Phase
- Split Variable (Remove Assignments to Parameters; Split Temp)
- Substitute Algorithm
Not in the 2nd edition:
- Replace Control Flag with Break (Remove Control Flag)
- Replace Error Code with Exception
- Replace Exception with Precheck (Replace Exception with Test)
- Replace Magic Literal (Replace Magic Number with Symbolic Constant)
- Return Modified Value
Change Function Declaration
Aliases: Rename Function; Rename Method; Add Parameter; Remove Parameter; Change Signature.
π In the Refactoring catalog.
// Before
function calculateCircleArea(radius: number): number {
return Math.PI * radius * radius;
}
// After
class Circle {
constructor(public radius: number) { }
calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
}
Change Reference to Value
π In the Refactoring catalog.
// Before
class Temperature {
constructor(public value: number) { }
getFahrenheit(): number {
return this.value * 9 / 5 + 32;
}
}
// After
class CelsiusTemperature {
constructor(public celsius: number) { }
getFahrenheit(): number {
return this.celsius * 9 / 5 + 32;
}
}
Change Value to Reference
π In the Refactoring catalog.
// Before
class Product {
constructor(public name: string) { }
}
class Order {
constructor(public product: Product) { }
}
// After
class ProductRegistry {
private products: Map<string, Product> = new Map();
getProduct(name: string): Product | undefined {
return this.products.get(name);
}
addProduct(product: Product): void {
this.products.set(product.name, product);
}
}
class Order {
constructor(public productName: string) { }
}
Collapse Hierarchy
π In the Refactoring catalog.
// Before
class Shape {
constructor(public name: string) { }
}
class Circle extends Shape {
constructor(public radius: number) {
super('Circle');
}
calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
}
// After
class Circle {
constructor(public radius: number) { }
calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
}
Combine Functions into Class
π In the Refactoring catalog.
// Before
function formatName(firstName: string, lastName: string): string {
return `${lastName}, ${firstName}`;
}
// After
class NameFormatter {
constructor(public firstName: string, public lastName: string) { }
format(): string {
return `${this.lastName}, ${this.firstName}`;
}
}
Combine Functions into Transform
π In the Refactoring catalog.
// Before
function capitalizeWord(word: string): string {
return word.charAt(0).toUpperCase() + word.slice(1);
}
function formatTitle(title: string): string {
const words = title.split(' ');
const capitalizedWords = words.map(capitalizeWord);
return capitalizedWords.join(' ');
}
// After
class TitleFormatter {
constructor(public title: string) { }
format(): string {
const words = this.title.split(' ');
const capitalizedWords = words.map(this.capitalizeWord);
return capitalizedWords.join(' ');
}
private capitalizeWord(word: string): string {
return word.charAt(0).toUpperCase() + word.slice(1);
}
}
Consolidate Conditional Expression
π In the Refactoring catalog.
// Before
function calculateBonus(salary: number, yearsWorked: number, isTopPerformer: boolean): number {
let bonus = 0;
if (isTopPerformer) {
bonus += salary * 0.2;
}
if (yearsWorked >= 5) {
bonus += salary * 0.1;
}
return bonus;
}
// After
function calculateBonus(salary: number, yearsWorked: number, isTopPerformer: boolean): number {
let bonus = 0;
if (isTopPerformer || yearsWorked >= 5) {
bonus += salary * 0.2;
}
return bonus;
}
Decompose Conditional
π In the Refactoring catalog.
// Before
function calculateShippingCost(orderTotal: number, isPriority: boolean): number {
let shippingCost = 0;
if (orderTotal > 100) {
shippingCost = isPriority ? 10 : 20;
} else {
shippingCost = isPriority ? 15 : 30;
}
return shippingCost;
}
// After
class ShippingCalculator {
calculateShippingCost(orderTotal: number, isPriority: boolean): number {
if (orderTotal > 100) {
return isPriority ? 10 : 20;
} else {
return isPriority ? 15 : 30;
}
}
}
Encapsulate Collection
π In the Refactoring catalog.
// Before
class ShoppingCart {
items: string[] = [];
addItem(item: string): void {
this.items.push(item);
}
removeItem(item: string): void {
const index = this.items.indexOf(item);
if (index !== -1) {
this.items.splice(index, 1);
}
}
getItems(): string[] {
return this.items;
}
}
// After
class ShoppingCart {
private items: string[] = [];
addItem(item: string): void {
this.items.push(item);
}
removeItem(item: string): void {
const index = this.items.indexOf(item);
if (index !== -1) {
this.items.splice(index, 1);
}
}
getItemCount(): number {
return this.items.length;
}
}
Encapsulate Record
Aliases: Replace Record with Data Class.
π In the Refactoring catalog.
// Before
const employee = {
firstName: "John",
lastName: "Doe",
age: 30,
salary: 50000,
};
// After
class Employee {
constructor(
public firstName: string,
public lastName: string,
public age: number,
public salary: number
) { }
}
const employee = new Employee("John", "Doe", 30, 50000);
Encapsulate Variable
Aliases: Encapsulate Field; Self-Encapsulate Field.
π In the Refactoring catalog.
// Before
let discount = 0.15;
function applyDiscount(price: number): number {
return price * (1 - discount);
}
// After
class DiscountManager {
private static discount = 0.15;
static applyDiscount(price: number): number {
return price * (1 - this.discount);
}
}
Extract Class
π In the Refactoring catalog.
// Before
class Order {
customerName: string;
customerAddress: string;
items: string[];
constructor(customerName: string, customerAddress: string, items: string[]) {
this.customerName = customerName;
this.customerAddress = customerAddress;
this.items = items;
}
printOrderDetails(): void {
console.log(`Customer: ${this.customerName}`);
console.log(`Address: ${this.customerAddress}`);
console.log(`Items: ${this.items.join(', ')}`);
}
}
// After
class Customer {
constructor(public name: string, public address: string) { }
}
class Order {
constructor(public customer: Customer, public items: string[]) { }
printOrderDetails(): void {
console.log(`Customer: ${this.customer.name}`);
console.log(`Address: ${this.customer.address}`);
console.log(`Items: ${this.items.join(', ')}`);
}
}
Extract Function
Aliases: Extract Method.
π In the Refactoring catalog.
// Before
function calculateTotalPrice(cart: number[], taxRate: number): number {
let total = 0;
for (const item of cart) {
total += item;
}
return total * (1 + taxRate);
}
// After
function calculateSubtotal(cart: number[]): number {
let subtotal = 0;
for (const item of cart) {
subtotal += item;
}
return subtotal;
}
function calculateTotalPrice(subtotal: number, taxRate: number): number {
return subtotal * (1 + taxRate);
}
Extract Superclass
π In the Refactoring catalog.
// Before
class Employee {
name: string;
id: number;
constructor(name: string, id: number) {
this.name = name;
this.id = id;
}
// Employee-specific methods...
}
class Manager extends Employee {
teamSize: number;
constructor(name: string, id: number, teamSize: number) {
super(name, id);
this.teamSize = teamSize;
}
// Manager-specific methods...
}
class Developer extends Employee {
programmingLanguage: string;
constructor(name: string, id: number, programmingLanguage: string) {
super(name, id);
this.programmingLanguage = programmingLanguage;
}
// Developer-specific methods...
}
// After
class Employee {
name: string;
id: number;
constructor(name: string, id: number) {
this.name = name;
this.id = id;
}
// Employee-specific methods...
}
class Manager extends Employee {
teamSize: number;
constructor(name: string, id: number, teamSize: number) {
super(name, id);
this.teamSize = teamSize;
}
// Manager-specific methods...
}
class Developer extends Employee {
programmingLanguage: string;
constructor(name: string, id: number, programmingLanguage: string) {
super(name, id);
this.programmingLanguage = programmingLanguage;
}
// Developer-specific methods...
}
class Salesperson extends Employee {
region: string;
constructor(name: string, id: number, region: string) {
super(name, id);
this.region = region;
}
// Salesperson-specific methods...
}
Extract Variable
Aliases: Introduce Explaining Variable.
π In the Refactoring catalog.
// Before
// Before
function calculateTotalPrice(quantity: number, pricePerUnit: number, taxRate: number): number {
const totalPrice = quantity * pricePerUnit;
return totalPrice + totalPrice * taxRate;
}
// After
function calculateTotalPrice(quantity: number, pricePerUnit: number, taxRate: number): number {
const basePrice = quantity * pricePerUnit;
const taxAmount = basePrice * taxRate;
return basePrice + taxAmount;
}
Hide Delegate
π In the Refactoring catalog.
// Before
class Department {
manager: Employee;
constructor(manager: Employee) {
this.manager = manager;
}
getManager(): Employee {
return this.manager;
}
}
class Employee {
name: string;
constructor(name: string) {
this.name = name;
}
}
// After
class Department {
manager: Employee;
constructor(manager: Employee) {
this.manager = manager;
}
getManagerName(): string {
return this.manager.name;
}
}
Inline Class
π In the Refactoring catalog.
// Before
class Address {
street: string;
city: string;
country: string;
constructor(street: string, city: string, country: string) {
this.street = street;
this.city = city;
this.country = country;
}
}
class Customer {
name: string;
address: Address;
constructor(name: string, address: Address) {
this.name = name;
this.address = address;
}
}
// After
class Customer {
name: string;
street: string;
city: string;
country: string;
constructor(name: string, street: string, city: string, country: string) {
this.name = name;
this.street = street;
this.city = city;
this.country = country;
}
}
Inline Function
π In the Refactoring catalog.
// Before
function calculateTotal(price: number, quantity: number): number {
return applyDiscount(price * quantity);
}
function applyDiscount(amount: number): number {
return amount * 0.9;
}
// After
function calculateTotal(price: number, quantity: number): number {
return price * quantity * 0.9;
}
Inline Variable
Aliases: Inline Temp.
π In the Refactoring catalog.
// Before
function calculateTotalPrice(quantity: number, pricePerUnit: number, taxRate: number): number {
const basePrice = quantity * pricePerUnit;
const taxAmount = basePrice * taxRate;
return basePrice + taxAmount;
}
// After
function calculateTotalPrice(quantity: number, pricePerUnit: number, taxRate: number): number {
return quantity * pricePerUnit + quantity * pricePerUnit * taxRate;
}
Introduce Assertion
π In the Refactoring catalog.
// Before
function calculateDiscountedPrice(price: number, discount: number): number {
if (discount < 0 || discount > 1) {
throw new Error("Invalid discount percentage");
}
return price * (1 - discount);
}
// After
function calculateDiscountedPrice(price: number, discount: number): number {
console.assert(discount >= 0 && discount <= 1, "Invalid discount percentage");
return price * (1 - discount);
}
Introduce Parameter Object
π In the Refactoring catalog.
// Before
function createOrder(customer: string, product: string, quantity: number): Order {
// ...
}
// After
class OrderInfo {
constructor(public customer: string, public product: string, public quantity: number) { }
}
function createOrder(orderInfo: OrderInfo): Order {
// ...
}
Introduce Special Case
Aliases: Introduce Null Object.
π In the Refactoring catalog.
// Before
function calculateDiscountedPrice(price: number, discount: number): number {
if (discount === 0) {
return price;
}
return price * (1 - discount);
}
// After
class Discount {
constructor(public value: number) { }
apply(price: number): number {
if (this.value === 0) {
return price;
}
return price * (1 - this.value);
}
}
Move Field
π In the Refactoring catalog.
// Before
class Customer {
name: string;
}
class Order {
customer: Customer;
constructor(customer: Customer) {
this.customer = customer;
}
}
// After
class Order {
constructor(public customer: Customer) { }
}
Move Function
Aliases: Move Method.
π In the Refactoring catalog.
// Before
class Account {
// ...
}
class AccountType {
isPremium(): boolean {
return true;
}
}
// After
class Account {
constructor(private type: AccountType) { }
isPremium(): boolean {
return this.type.isPremium();
}
}
Move Statements into Function
π In the Refactoring catalog.
// Before
function printInvoice(order: Order): void {
console.log("Invoice:");
console.log(`Customer: ${order.customer}`);
console.log(`Total: ${order.total}`);
}
// After
class Order {
printInvoice(): void {
console.log("Invoice:");
console.log(`Customer: ${this.customer}`);
console.log(`Total: ${this.total}`);
}
}
Move Statements to Callers
π In the Refactoring catalog.
// Before
function applyDiscount(order: Order): void {
if (order.total > 100) {
order.total *= 0.9;
}
}
// After
class Order {
applyDiscount(): void {
if (this.total > 100) {
this.total *= 0.9;
}
}
}
Parameterize Function
Aliases: Parameterize Method.
π In the Refactoring catalog.
// Before
function calculateTotal(price: number, quantity: number, taxRate: number): number {
return price * quantity * (1 + taxRate);
}
// After
function calculateTotal(price: number, quantity: number, taxRate: number): number {
return price * quantity * (1 + taxRate);
}
Preserve Whole Object
π In the Refactoring catalog.
// Before
function orderDelivery(customer: Customer): void {
const address = customer.address;
// ... process the address for delivery
}
// After
function orderDelivery(address: Address): void {
// ... process the address for delivery
}
Pull Up Constructor Body
π In the Refactoring catalog.
// Before
class Product {
constructor(public name: string) {
console.log("Product created.");
}
}
class Book extends Product {
constructor(name: string, public author: string) {
super(name);
}
}
// After
class Product {
constructor(public name: string) { }
}
class Book extends Product {
constructor(name: string, public author: string) {
super(name);
console.log("Product created.");
}
}
Pull Up Field
π In the Refactoring catalog.
// Before
class Vehicle {
// ...
}
class Car extends Vehicle {
engineType: string;
// ...
}
class Bicycle extends Vehicle {
// ...
}
// After
class Vehicle {
engineType: string;
// ...
}
class Car extends Vehicle {
// ...
}
class Bicycle extends Vehicle {
// ...
}
Pull Up Method
π In the Refactoring catalog.
// Before
class Employee {
// ...
}
class Salesperson extends Employee {
calculateCommission(salesAmount: number): number {
return salesAmount * 0.1;
}
}
class Manager extends Employee {
// ...
}
// After
class Employee {
calculateCommission(salesAmount: number): number {
return salesAmount * 0.1;
}
}
class Salesperson extends Employee {
// ...
}
class Manager extends Employee {
// ...
}
Push Down Field
π In the Refactoring catalog.
// Before
class Vehicle {
// ...
}
class Car extends Vehicle {
color: string;
// ...
}
class Bicycle extends Vehicle {
// ...
}
// After
class Vehicle {
// ...
}
class Car extends Vehicle {
color: string;
// ...
}
class Bicycle extends Vehicle {
// ...
}
Push Down Method
π In the Refactoring catalog.
// Before
class Employee {
// ...
}
class Salesperson extends Employee {
calculateCommission(salesAmount: number): number {
return salesAmount * 0.1;
}
}
class Manager extends Employee {
// ...
}
// After
class Employee {
// ...
}
class Salesperson extends Employee {
// ...
}
class Manager extends Employee {
calculateCommission(salesAmount: number): number {
return salesAmount * 0.05;
}
}
Remove Dead Code
π In the Refactoring catalog.
// Before
function calculateTotal(price: number, quantity: number): number {
// Dead code, never used
const taxRate = 0.1;
return price * quantity * (1 + taxRate);
}
// After
function calculateTotal(price: number, quantity: number): number {
return price * quantity;
}
Remove Flag Argument
π In the Refactoring catalog.
// Before
function sendNotification(message: string, isUrgent: boolean): void {
if (isUrgent) {
// Send urgent notification
} else {
// Send normal notification
}
}
// After
function sendUrgentNotification(message: string): void {
// Send urgent notification
}
function sendNormalNotification(message: string): void {
// Send normal notification
}
Remove Middle Man
π In the Refactoring catalog.
// Before
class Department {
manager: Employee;
constructor(manager: Employee) {
this.manager = manager;
}
getManager(): Employee {
return this.manager;
}
}
// After
class Department {
constructor(public manager: Employee) { }
}
Remove Setting Method
π In the Refactoring catalog.
// Before
class Person {
name: string;
setName(name: string): void {
this.name = name;
}
}
// After
class Person {
constructor(public name: string) { }
}
Remove Subclass
Aliases: Replace Subclass with Fields.
π In the Refactoring catalog.
// Before
class Employee {
// ...
}
class Salesperson extends Employee {
// ...
}
class Manager extends Employee {
// ...
}
// After
class Employee {
// ...
}
Rename Field
π In the Refactoring catalog.
// Before
class Person {
fn: string;
ln: string;
constructor(firstName: string, lastName: string) {
this.fn = firstName;
this.ln = lastName;
}
}
// After
class Person {
constructor(public firstName: string, public lastName: string) { }
}
Rename Variable
π In the Refactoring catalog.
// Before
function calculateTotal(prc: number, qty: number): number {
return prc * qty;
}
// After
function calculateTotal(price: number, quantity: number): number {
return price * quantity;
}
Replace Command with Function
π In the Refactoring catalog.
// Before
class BankAccount {
balance: number;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
withdraw(amount: number): void {
this.balance -= amount;
}
}
// After
class BankAccount {
balance: number;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
deduct(amount: number): void {
this.balance -= amount;
}
}
Replace Conditional with Polymorphism
π In the Refactoring catalog.
// Before
class Employee {
type: string;
monthlySalary: number;
commission: number;
constructor(type: string, monthlySalary: number, commission: number) {
this.type = type;
this.monthlySalary = monthlySalary;
this.commission = commission;
}
calculatePay(): number {
if (this.type === "fullTime") {
return this.monthlySalary;
} else if (this.type === "commissioned") {
return this.monthlySalary + this.commission;
}
}
}
// After
class Employee {
calculatePay(): number {
throw new Error("Abstract method");
}
}
class FullTimeEmployee extends Employee {
constructor(private monthlySalary: number) {
super();
}
calculatePay(): number {
return this.monthlySalary;
}
}
class CommissionedEmployee extends Employee {
constructor(private monthlySalary: number, private commission: number) {
super();
}
calculatePay(): number {
return this.monthlySalary + this.commission;
}
}
Replace Constructor with Factory Function
Aliases: Replace Constructor with Factory Method.
π In the Refactoring catalog.
// Before
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
// After
class Person {
private constructor(public name: string, public age: number) { }
static createPerson(name: string, age: number): Person {
return new Person(name, age);
}
}
Replace Derived Variable with Query
π In the Refactoring catalog.
// Before
class Order {
items: number[];
constructor(items: number[]) {
this.items = items;
}
getTotal(): number {
let total = 0;
for (const item of this.items) {
total += item;
}
return total;
}
}
// After
class Order {
items: number[];
constructor(items: number[]) {
this.items = items;
}
getTotal(): number {
return this.items.reduce((sum, item) => sum + item, 0);
}
}
Replace Function with Command
Aliases: Replace Method with Method Object.
π In the Refactoring catalog.
// Before
class Light {
isOn: boolean;
constructor() {
this.isOn = false;
}
turnOn(): void {
this.isOn = true;
}
turnOff(): void {
this.isOn = false;
}
}
// After
class Light {
isOn: boolean;
constructor() {
this.isOn = false;
}
commandOn(): LightCommand {
return new TurnOnCommand(this);
}
commandOff(): LightCommand {
return new TurnOffCommand(this);
}
}
interface LightCommand {
execute(): void;
}
class TurnOnCommand implements LightCommand {
constructor(private light: Light) { }
execute(): void {
this.light.isOn = true;
}
}
class TurnOffCommand implements LightCommand {
constructor(private light: Light) { }
execute(): void {
this.light.isOn = false;
}
}
Replace Inline Code with Function Call
π In the Refactoring catalog.
// Before
function calculateTotal(price: number, quantity: number): number {
const taxRate = 0.1;
return price * quantity * (1 + taxRate);
}
// After
function calculateTotal(price: number, quantity: number): number {
return applyTax(price * quantity);
}
function applyTax(amount: number): number {
const taxRate = 0.1;
return amount * (1 + taxRate);
}
Replace Loop with Pipeline
π In the Refactoring catalog.
// Before
function getDiscountedPrices(products: Product[]): number[] {
const discountedPrices: number[] = [];
for (const product of products) {
const discount = calculateDiscount(product);
discountedPrices.push(product.price * (1 - discount));
}
return discountedPrices;
}
// After
function getDiscountedPrices(products: Product[]): number[] {
return products.map(product => product.calculateDiscountedPrice());
}
class Product {
constructor(public price: number) { }
calculateDiscountedPrice(): number {
const discount = calculateDiscount(this);
return this.price * (1 - discount);
}
}
Replace Nested Conditional with Guard Clauses
π In the Refactoring catalog.
// Before
function calculatePrice(product: Product): number {
let price = product.basePrice;
if (product.isPromoEligible()) {
if (product.isPremium()) {
price *= 0.9;
} else {
price *= 0.95;
}
}
return price;
}
// After
function calculatePrice(product: Product): number {
if (!product.isPromoEligible()) {
return product.basePrice;
}
if (product.isPremium()) {
return product.basePrice * 0.9;
}
return product.basePrice * 0.95;
}
Replace Parameter with Query
Aliases: Replace Parameter with Method.
π In the Refactoring catalog.
// Before
function calculateTotalPrice(price: number, quantity: number): number {
return price * quantity;
}
// After
function calculateTotalPrice(product: Product): number {
return product.getPrice() * product.getQuantity();
}
Replace Primitive with Object
Aliases: Replace Data Value with Object; Replace Type Code with Class.
π In the Refactoring catalog.
// Before
function calculateTotalPrice(price: number, quantity: number): number {
return price * quantity;
}
// After
class Quantity {
constructor(private value: number) { }
multiply(price: number): number {
return this.value * price;
}
}
const totalPrice = new Quantity(quantity).multiply(price);
Replace Query with Parameter
π In the Refactoring catalog.
// Before
function isDiscountEligible(orderTotal: number): boolean {
return orderTotal > 100;
}
function calculateTotal(orderTotal: number, discountRate: number): number {
if (isDiscountEligible(orderTotal)) {
return orderTotal * (1 - discountRate);
}
return orderTotal;
}
// After
function calculateTotal(orderTotal: number, discountRate: number, isDiscountEligible: boolean): number {
if (isDiscountEligible) {
return orderTotal * (1 - discountRate);
}
return orderTotal;
}
Replace Subclass with Delegate
π In the Refactoring catalog.
// Before
class TeamLead extends Employee {
// ...
}
class Employee {
teamLead: TeamLead;
getTeamLead(): TeamLead {
return this.teamLead;
}
}
// After
class Employee {
delegate: TeamLeadDelegate;
getTeamLead(): TeamLeadDelegate {
return this.delegate;
}
}
class TeamLeadDelegate {
// ...
}
Replace Superclass with Delegate
Aliases: Replace Inheritance with Delegation.
π In the Refactoring catalog.
// Before
class Circle extends Shape {
radius: number;
constructor(radius: number) {
super();
this.radius = radius;
}
// ...
}
// After
class CircleDelegate {
shape: Shape;
constructor(shape: Shape) {
this.shape = shape;
}
getRadius(): number {
// Return the radius using the shape data
}
// ...
}
Replace Temp with Query
π In the Refactoring catalog.
// Before
function calculateTotalPrice(price: number, quantity: number): number {
const totalPrice = price * quantity;
return totalPrice > 1000 ? totalPrice * 0.9 : totalPrice;
}
// After
function calculateTotalPrice(price: number, quantity: number): number {
return (price * quantity) > 1000 ? price * quantity * 0.9 : price * quantity;
}
Replace Type Code with Subclasses
Aliases: Extract Subclass; Replace Type Code with State/Strategy.
π In the Refactoring catalog.
// Before
class Employee {
type: string;
constructor(type: string) {
this.type = type;
}
}
// After
abstract class Employee {
abstract calculatePay(): number;
}
class FullTimeEmployee extends Employee {
calculatePay(): number {
// Calculation for full-time employee
}
}
class PartTimeEmployee extends Employee {
calculatePay(): number {
// Calculation for part-time employee
}
}
Separate Query from Modifier
π In the Refactoring catalog.
// Before
class Account {
balance: number;
withdraw(amount: number): void {
if (amount > this.balance) {
throw new Error("Insufficient balance");
}
this.balance -= amount;
}
}
// After
class Account {
balance: number;
canWithdraw(amount: number): boolean {
return amount <= this.balance;
}
withdraw(amount: number): void {
if (!this.canWithdraw(amount)) {
throw new Error("Insufficient balance");
}
this.balance -= amount;
}
}
Slide Statements
Aliases: Consolidate Duplicate Conditional Fragments.
π In the Refactoring catalog.
// Before
let discount: number = 0;
if (order.total > 100) {
discount = 0.1;
}
// After
if (order.total > 100) {
let discount: number = 0.1;
}
Split Loop
π In the Refactoring catalog.
// Before
function calculateTotalPrice(cart: Cart[]): number {
let total = 0;
for (const item of cart) {
total += item.price;
}
for (const item of cart) {
total -= item.discount;
}
return total;
}
// After
function calculateTotalPrice(cart: Cart[]): number {
let totalPrice = 0;
for (const item of cart) {
totalPrice += item.price;
}
let totalDiscount = 0;
for (const item of cart) {
totalDiscount += item.discount;
}
return totalPrice - totalDiscount;
}
Split Phase
π In the Refactoring catalog.
// Before
function processOrder(order: Order): void {
validateOrder(order);
updateInventory(order);
createInvoice(order);
}
// After
function processOrder(order: Order): void {
validateOrder(order);
updateInventory(order);
}
function updateInventory(order: Order): void {
// ...
}
function createInvoice(order: Order): void {
// ...
}
Split Variable
Aliases: Remove Assignments to Parameters; Split Temp.
π In the Refactoring catalog.
// Before
let fullName: string = `${user.firstName} ${user.lastName}`;
// After
let firstName: string = user.firstName;
let lastName: string = user.lastName;
Substitute Algorithm
π In the Refactoring catalog.
// Before
function foundPerson(people: string[]): string {
for (const person of people) {
if (person === "Don") {
return "Don";
}
if (person === "John") {
return "John";
}
if (person === "Kent") {
return "Kent";
}
}
return "";
}
// After
function foundPerson(people: string[]): string {
const candidates = ["Don", "John", "Kent"];
for (const person of people) {
if (candidates.includes(person)) {
return person;
}
}
return "";
}
Refactorings not in the 2nd editions
Replace Control Flag with Break
Aliases: Remove Control Flag.
π In the Refactoring catalog.
// Before
function findValue(arr: number[], target: number): boolean {
let found = false;
for (const item of arr) {
if (item === target) {
found = true;
break;
}
}
return found;
}
// After
function findValue(arr: number[], target: number): boolean {
for (const item of arr) {
if (item === target) {
return true;
}
}
return false;
}
Replace Error Code with Exception
π In the Refactoring catalog.
// Before
function divide(a: number, b: number): number {
if (b === 0) {
return -1; // Error code for division by zero
}
return a / b;
}
// After
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Division by zero");
}
return a / b;
}
Replace Exception with Precheck
Aliases: Replace Exception with Test.
π In the Refactoring catalog.
// Before
function calculateDiscountedPrice(price: number, discount: number): number {
try {
if (discount < 0 || discount > 1) {
throw new Error("Invalid discount");
}
return price * (1 - discount);
} catch (error) {
console.error(error);
return price;
}
}
// After
function calculateDiscountedPrice(price: number, discount: number): number {
if (discount < 0 || discount > 1) {
console.error("Invalid discount");
return price;
}
return price * (1 - discount);
}
Replace Magic Literal
Aliases: Replace Magic Number with Symbolic Constant.
π In the Refactoring catalog.
// Before
function calculateTotal(price: number, quantity: number): number {
return price * quantity * 1.1; // 1.1 is the tax rate
}
// After
const TAX_RATE = 1.1;
function calculateTotal(price: number, quantity: number): number {
return price * quantity * TAX_RATE;
}
Return Modified Value
π In the Refactoring catalog.
// Before
function modifyValue(value: number): number {
value *= 2;
value += 10;
return value;
}
// After
function modifyValue(value: number): void {
value *= 2;
value += 10;
}