A Journey of a Shopping Cart: From Corner Shop to E-Shop 🛒🛍️ (Part 1)

Subrata ChSubrata Ch
8 min read

In the ever-changing world of software development, moving from simple procedural code to advanced, modular, and reusable code is crucial. In this post, we will go through the transformation of a basic shopping cart calculation script through different programming styles and improvements, ending with an object-oriented solution.

Join me on an epic journey... 🤔 as we explore the evolution of a shopping cart from corner shops to digital platforms...👩‍💻From brick-and-mortar stores to online shopping...🛍️ We'll dive into each version, highlighting improvements in flexibility, reusability and maintainability.

To keep things simple, we will start with just one product and continuously refine and refactor our code until we achieve a solid solution. At this stage, no UI, database or transaction management will be involved. Mainly, we'll look at the shift from simple procedural programming to a more modular approach with functions, and finally, to a clean class-based implementation using modern ES6 features.

Version 1: Exploring the Procedural Programming Paradigm: Without Emphasising Functions. 🍝

const quantity = 1;
const price = 100;
const discount = 0.10; // Apply a 10% discount
const vat = 0.20; // Apply 20% VAT
    // Calculate the subtotal
    const subtotal = quantity * price;
    const discountedTotal = subtotal * (1 - discount);
    const vatAmount = discountedTotal * vat;
    // Calculate the grand total
    const finalAmount = discountedTotal + vatAmount;
console.log(`The grand total is: £${finalAmount.toFixed(2)}`);
// OUTPUT: The grand total is: £108.00

Version 2: Procedural Programming Paradigm: Utilising a Single Function. 💪

const quantity = 1;
const price = 100;
const discountRate = 10; // 10% discount
const vatRate = 20;      // 20% VAT
// Function to calculate the grand total
function calculateFinalAmount(quantity, price, discountRate, vatRate) {
    // Convert percentage rates to decimal
    const discount = discountRate / 100;
    const vat = vatRate / 100;
    // Calculate the subtotal
    const subtotal = quantity * price;
    // Apply the discount
    const discountedTotal = subtotal * (1 - discount);
    // Apply VAT
    const vatAmount = discountedTotal * vat;
    // Calculate the grand total
    const grandTotal = discountedTotal + vatAmount;
    return grandTotal;
}

// Usage 
const finalAmount = calculateFinalAmount(quantity, price, discountRate, vatRate);
console.log(`The grand total is: £${finalAmount.toFixed(2)}`);
// OUTPUT: The grand total is: £108.00

Key Differentiation between Version 1 and Version 2.

The main difference is the level of modularity and reusability. The version with the function, v2 (calculateFinalAmount), is more modular and organised, making it easier to manage and extend. The procedural code without functions performs calculations directly but lacks the benefits of modularity and reusability.

Version 3: Procedural Programming Paradigm: Adopting a Modular Functional Approach. 🧱


const quantity = 1;
const price = 100;
const discountRate = 10; // 10% discount
const vatRate = 20;      // 20% VAT

// Function to calculate the total amount before any discounts or VAT
function getTotal(quantity, price) {
    return quantity * price;
}

// Function to apply a discount
function applyDiscount(totalAmount, discountRate) {
    const discount = discountRate / 100;
    const discountAmount = totalAmount * discount;
    return {
        discountedAmount: totalAmount - discountAmount,
        discountAmount: discountAmount
    };
}

// Function to add VAT
function addVAT(totalAmount, vatRate) {
    const vat = vatRate / 100;
    const vatAmount = totalAmount * vat;
    return {
        totalWithVAT: totalAmount + vatAmount,
        vatAmount: vatAmount
    };
}

// Function to calculate the final amount
function calculateFinalAmount(quantity, price, discountRate, vatRate) {
    let totalAmount = getTotal(quantity, price);
    // Calculate discount
    const discountResult = applyDiscount(totalAmount, discountRate);
    console.log(`Total before the discount: £${totalAmount.toFixed(2)}`);
    console.log(`Discount amount (${discountRate}%): £${discountResult.discountAmount.toFixed(2)}`);
    console.log(`Total after ${discountRate}% discount: £${discountResult.discountedAmount.toFixed(2)}`);
    // Apply VAT
    const vatResult = addVAT(discountResult.discountedAmount, vatRate);
    console.log(`VAT amount (${vatRate}%): £${vatResult.vatAmount.toFixed(2)}`);
    console.log(`Total after adding ${vatRate}% VAT: £${vatResult.totalWithVAT.toFixed(2)}`);

    return vatResult.totalWithVAT;
}

// Calculate and display the final total amount
const finalAmount = calculateFinalAmount(quantity, price, discountRate, vatRate);
console.log(`Grand Total: £${finalAmount}`);
/* OUTPUT: 
Total before the discount: £100.00
Discount amount (10%): £10.00
Total after 10% discount: £90.00
VAT amount (20%): £18.00
Total after adding 20% VAT: £108.00
Grand Total: £108
*/

Version 4: Procedural Programming Paradigm: Emphasising Validation and Error Handling. 🛠️


const quantity = 1;
const price = 100;
const discountRate = 10; // 10% discount (string type)
const vatRate = 20;      // 20% VAT

// Function to validate inputs
function validateInputs(quantity, price, discountRate, vatRate) {
    quantity = Number(quantity);
    price = Number(price);
    discountRate = Number(discountRate);
    vatRate = Number(vatRate);

    if (isNaN(quantity) || quantity <= 0) {
        throw new Error('Quantity must be a positive number.');
    }
    if (isNaN(price) || price < 0) {
        throw new Error('Price must be a non-negative number.');
    }
    if (isNaN(discountRate) || discountRate < 0 || discountRate > 100) {
        throw new Error('Discount rate must be a number between 0 and 100.');
    }
    if (isNaN(vatRate) || vatRate < 0 || vatRate > 100) {
        throw new Error('VAT rate must be a number between 0 and 100.');
    }

    return { quantity, price, discountRate, vatRate };
}

// Function to calculate the total amount before any discounts or VAT
function getTotal(quantity, price) {
    return quantity * price;
}

// Function to apply a discount
function applyDiscount(totalAmount, discountRate) {
    const discount = discountRate / 100;
    const discountAmount = totalAmount * discount;
    return {
        discountedAmount: totalAmount - discountAmount,
        discountAmount: discountAmount
    };
}

// Function to add VAT
function addVAT(totalAmount, vatRate) {
    const vat = vatRate / 100;
    const vatAmount = totalAmount * vat;
    return {
        totalWithVAT: totalAmount + vatAmount,
        vatAmount: vatAmount
    };
}

// Function to calculate the final amount
function calculateFinalAmount(quantity, price, discountRate, vatRate) {
    try {
        const validatedInputs = validateInputs(quantity, price, discountRate, vatRate);
        let totalAmount = getTotal(validatedInputs.quantity, validatedInputs.price);
        console.log(`Total before the discount: £${totalAmount.toFixed(2)}`);

        // Calculate discount
        const discountResult = applyDiscount(totalAmount, validatedInputs.discountRate);
        console.log(`Discount amount (${validatedInputs.discountRate}%): £${discountResult.discountAmount.toFixed(2)}`);
        console.log(`Total after ${validatedInputs.discountRate}% discount: £${discountResult.discountedAmount.toFixed(2)}`);

        // Apply VAT
        const vatResult = addVAT(discountResult.discountedAmount, validatedInputs.vatRate);
        console.log(`VAT amount (${validatedInputs.vatRate}%): £${vatResult.vatAmount.toFixed(2)}`);
        console.log(`Total after adding ${validatedInputs.vatRate}% VAT: £${vatResult.totalWithVAT.toFixed(2)}`);

        return vatResult.totalWithVAT;
    } catch (error) {
        console.error('Error:', error.message);
    }
}

// Calculate and display the final total amount
const finalAmount = calculateFinalAmount(quantity, price, discountRate, vatRate);
/* OUTPUT: 
Total before the discount: £100.00
Discount amount (10%): £10.00
Total after 10% discount: £90.00
VAT amount (20%): £18.00
Total after adding 20% VAT: £108.00
Grand Total: £108
*/

Differentiation between Version 3 and Version 4.

Version 3: Introduces separate functions for calculating the total, applying discounts, and adding VAT, with calculateFinalAmount coordinating these steps. It uses parameters for discount and VAT rates, allowing flexible adjustments and detailed intermediate outputs.

Version 4: Builds on Version 4 by adding input validation and error handling with validateInputs. This ensures inputs are correctly formatted and within acceptable ranges, making the code more robust and maintaining detailed outputs for transparency and debugging.

Version 5: Object-Oriented Programming Paradigm: Classes and ES6 Features. ⚖️

class PricingCalculator {
    constructor(quantity, price, discountRate, vatRate) {
        this.quantity = Number(quantity);
        this.price = Number(price);
        this.discountRate = Number(discountRate);
        this.vatRate = Number(vatRate);

        this.validateInputs();
    }

    validateInputs() {
        if (isNaN(this.quantity) || this.quantity <= 0) {
            throw new Error('Quantity must be a positive number.');
        }
        if (isNaN(this.price) || this.price < 0) {
            throw new Error('Price must be a non-negative number.');
        }
        if (isNaN(this.discountRate) || this.discountRate < 0 || this.discountRate > 100) {
            throw new Error('Discount rate must be a number between 0 and 100.');
        }
        if (isNaN(this.vatRate) || this.vatRate < 0 || this.vatRate > 100) {
            throw new Error('VAT rate must be a number between 0 and 100.');
        }
    }

    getTotal() {
        return this.quantity * this.price;
    }

    applyDiscount(totalAmount) {
        const discount = this.discountRate / 100;
        const discountAmount = totalAmount * discount;
        return {
            discountedAmount: totalAmount - discountAmount,
            discountAmount: discountAmount
        };
    }

    addVAT(totalAmount) {
        const vat = this.vatRate / 100;
        const vatAmount = totalAmount * vat;
        return {
            totalWithVAT: totalAmount + vatAmount,
            vatAmount: vatAmount
        };
    }

    calculateFinalAmount() {
        let totalAmount = this.getTotal();
        const discountResult = this.applyDiscount(totalAmount);
        const vatResult = this.addVAT(discountResult.discountedAmount);
        return vatResult.totalWithVAT;
    }

    logCalculations() {
        try {
            let totalAmount = this.getTotal();
            console.log(`Total before the discount: £${totalAmount.toFixed(2)}`);

            const discountResult = this.applyDiscount(totalAmount);
            console.log(`Discount amount (${this.discountRate}%): £${discountResult.discountAmount.toFixed(2)}`);
            console.log(`Total after ${this.discountRate}% discount: £${discountResult.discountedAmount.toFixed(2)}`);

            const vatResult = this.addVAT(discountResult.discountedAmount);
            console.log(`VAT amount (${this.vatRate}%): £${vatResult.vatAmount.toFixed(2)}`);
            console.log(`Total after adding ${this.vatRate}% VAT: £${vatResult.totalWithVAT.toFixed(2)}`);

            return vatResult.totalWithVAT;
        } catch (error) {
            console.error('Error:', error.message);
        }
    }
}

// Example usage
const calculator = new PricingCalculator(1, 100, "10", 20);
const finalAmount = calculator.calculateFinalAmount();
calculator.logCalculations();

/*  OUTPUT: 
Total before the discount: £100.00
Discount amount (10%): £10.00
Total after 10% discount: £90.00
VAT amount (20%): £18.00
Total after adding 20% VAT: £108.00
*/

Comparing Version 4 and Version 5.

Version 4: Uses a procedural approach with separate functions for validating inputs, calculating totals, applying discounts, and adding VAT. Strong error handling ensures input validity, enhancing reliability and flexibility. However, functionality is spread across different functions.

Version 5: Takes an object-oriented approach with a PricingCalculator class that handles input validation and calculations. This centralises data and functionality, improving code clarity, reusability, and manageability.

Final Verdict: 🗽

Among all the versions, Version 5 (with the class and ES6 support) is generally considered the best for several reasons:

➡️ Encapsulation and Organisation: The object-oriented approach with the PricingCalculator class groups related methods and properties, improving code organisation and readability.

➡️ Flexibility and Reusability: Centralising validation and calculations within the class enhances flexibility. It can be easily instantiated with different parameters, making it adaptable and extensible.

➡️ Validation and Error Handling: Input validation in the class constructor ensures parameters are checked before calculations, leading to more reliable code with clear error messages.

➡️ Code Clarity: The class-based design and ES6 features align with modern JavaScript practices, resulting in cleaner and more maintainable code.

➡️ Low Coupling & High Cohesion: Version 5 exhibits high cohesion and low coupling, with the PricingCalculator class focusing on pricing calculations and remaining independent from other parts of the system.

Overall, Version 5 offers a more organised, maintainable, and flexible solution compared to the procedural approaches of other versions, making it the most robust choice for managing pricing calculations. 🥱

Well, we are not done yet... what about testing? Here is Part 2: A Journey of a Shopping Cart 🛒: Test-Driven Development 🛠️ (Part 2), where we use a popular JavaScript test framework called Mocha ☕🍵 to test our shopping cart's PricingCalculator class.

0
Subscribe to my newsletter

Read articles from Subrata Ch directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Subrata Ch
Subrata Ch

Software Engineer & Consultant