Code Smell: Divergent Change

Continuing the series of blog posts about code smells, let’s talk about Divergent Change. Divergent change occurs when you have to do too many changes in a class/module to introduce a new feature/change. Imagine the following Product class:

class Product {
    private type: string;

    constructor(type: string) {
        this.type = type;
    }

    public getBasePrice() : number {
        switch (this.type) {
            case 'food':
                return 10;
            case 'drinks':
                return 7;
            case 'books':
                return 3;
            default:
                return 0;
        }
    }

    public getTaxPercent() : number {
        switch (this.type) {
            case 'food':
            case 'drinks':
                return 24;
            case 'books':
                return 8;
            default:
                return 0;
        }
    }

    public getProductCategory(): string {
        switch (this.type) {
            case 'food':
            case 'drinks':
                return 'Food and Beverages';
            case 'books':
                return 'Education';
            default:
                return '-';
        }
    }
}

If we want to introduce a new product type, let’s say Vitamins we would have to make changes in 3 different methods of the class.

class Product {
    private type: string;

    constructor(type: string) {
        this.type = type;
    }

    public getBasePrice() : number {
        switch (this.type) {
            case 'food':
                return 10;
            case 'drinks':
                return 7;
            case 'books':
                return 3;
            case 'vitamins':
                return 1;
            default:
                return 0;
        }
    }

    public getTaxPercent() : number {
        switch (this.type) {
            case 'food':
            case 'drinks':
                return 24;
            case 'books':
                return 8;
            case 'vitamins':
                return 3;
            default:
                return 0;
        }
    }

    public getProductCategory(): string {
        switch (this.type) {
            case 'food':
            case 'drinks':
                return 'Food and Beverages';
            case 'books':
                return 'Education';
            case 'vitamins':
                return 'Pharmaceutical';
            default:
                return '-';
        }
    }
}

We can improve the code by introducing a mapping:

type ProductInfo = {
    tax?: number,
    basePrice?: number;
    productCategory?: string;
}

class Product {
    private type: string;
    static productTypes: { [key: string]: ProductInfo } = {
        food: {
            tax: 24,
            basePrice: 10,
            productCategory: 'Food and Beverages'
        },
        drinks: {
            tax: 24,
            basePrice: 7,
            productCategory: 'Food and Beverages'
        },
        books: {
            tax: 8,
            basePrice: 3,
            productCategory: 'Education'
        },
    }

    constructor(type: string) {
        this.type = type;
    }

    public getBasePrice() : number {
        return Product.productTypes[this.type].basePrice || 0;
    }

    public getTaxPercent() : number {
        return Product.productTypes[this.type].tax || 0;
    }

    public getProductCategory(): string {
        return Product.productTypes[this.type].productCategory || '-';
    }
}

Now if we want to introduce a new product type (e.g. Vitamins), we will make changes in a single place without having to touch the methods of the class (with the possibility of introducing a bug affecting the existing products for example).

Published 22 Feb 2020

Software Engineering Lead, Certified AWS Solutions Architect. Opinions are my own and not necessarily the views of my employer.
Avraam Mavridis on Twitter