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

Engineering Manager. Opinions are my own and not necessarily the views of my employer.
Avraam Mavridis on Twitter