Understanding Liskov Substitution Principle with Real Construction Examples

Understanding Liskov Substitution Principle with Real Construction Examples

In software development, especially when building systems like construction expense tracking or invoice management apps, it is crucial to ensure your object-oriented design is maintainable and safe. One of the SOLID principles that guarantees reliability in inheritance is the Liskov Substitution Principle (LSP).

The Liskov Substitution Principle states that objects of a subclass should be replaceable for objects of the base class without changing the correctness of the program. In practical terms, wherever you use a base class, you should be able to plug in any of its derived classes without unexpected behavior.

Let’s explore how LSP applies in real-world construction scenarios like invoice processing, expense tracking, and payment handling.

 

Key Principle:

Behavior Preservation           

            A subclass should behave like the base class in all scenarios.

No Contradictions     

            Subclass should not override methods with conflicting or unexpected behavior.

No Side Effects          

            Using subclass in place of base class must not cause errors or break logic.

Contract Adherence   

            Subclass must honor the base class's method preconditions and postconditions.

Safe Polymorphism    

            You should be able to use a derived class object anywhere a base class object is expected.

Behavior Preservation

In a construction billing system, we might have a base class called PaymentMethod that defines how a payment is processed. Any subclass, such as BankTransfer, must preserve the behavior of the base class.

class PaymentMethod

{

    public virtual string ProcessPayment(decimal amount) => $"Paid {amount}";

}

 

class BankTransfer : PaymentMethod

{

    public override string ProcessPayment(decimal amount) => $"Bank Transfer: {amount} successful";

}

 

void PayContractor(PaymentMethod method, decimal amount)

{

    Console.WriteLine(method.ProcessPayment(amount));

}

When BankTransfer is used in place of PaymentMethod, it behaves exactly as expected. The behavior is preserved, and LSP is satisfied.

 

No Contradictions

Now imagine we introduce a new type of payment — credit payments. We implement it like this:

class CreditPayment : PaymentMethod

{

    public override string ProcessPayment(decimal amount)

    {

        throw new InvalidOperationException("Credit payment not allowed");

    }

}

This would break the client code that expects every PaymentMethod to process payments. Instead of throwing an error from a subclass, we can redesign the hierarchy to avoid contradiction.

A better solution is to introduce interfaces that group related behavior:

interface IPaymentMethod

{

    string ProcessPayment(decimal amount);

}

 

class CreditPayment

{

    public string RequestApproval(decimal amount) => $"Credit approval required for {amount}";

}

This approach avoids using inheritance where it doesn’t make sense and preserves expected behavior without contradiction.

 

No Side Effects

Consider a base class Expense that tracks costs in a construction project. If we extend it to create a MaterialExpense and introduce side effects in property setters, we risk breaking LSP.

class Expense

{

    public virtual decimal Amount { get; set; }

}

 

class MaterialExpense : Expense

{

    public override decimal Amount

    {

        get => base.Amount;

        set

        {

            base.Amount = value;

            SendNotification(); // Unexpected side effect

        }

    }

 

    private void SendNotification()

    {

        Console.WriteLine("Email sent for material expense.");

    }

}

To fix this, the notification logic should be moved outside the property setter. A service or separate method should trigger it after the amount is set, ensuring the subclass does not behave unexpectedly when used as an Expense.

class MaterialExpense : Expense

{

    public void SetAmountAndNotify(decimal amount)

    {

        Amount = amount;

        SendNotification();

    }

 

    private void SendNotification()

    {

        Console.WriteLine("Email sent for material expense.");

    }

}

This preserves the purity of the property setter and respects LSP.

 

Contract Adherence

Let’s look at invoice submission. Suppose the base class Invoice allows submission for any amount greater than zero. A derived class OnlineInvoice might restrict submission to amounts above 1000, violating the contract.

class Invoice

{

    public virtual void Submit(decimal amount)

    {

        if (amount <= 0)

            throw new ArgumentException("Amount must be positive");

        Console.WriteLine("Invoice submitted");

    }

}

 

class OnlineInvoice : Invoice

{

    public override void Submit(decimal amount)

    {

        if (amount < 1000)

            throw new ArgumentException("Online invoice must be >= 1000");

        base.Submit(amount);

    }

}

This change introduces a stricter precondition than the base class, breaking LSP. To fix this, the subclass must honor or relax, but not tighten, the preconditions.

A correct version would validate such business rules externally, not in the subclass.

class OnlineInvoice : Invoice

{

    public override void Submit(decimal amount)

    {

        base.Submit(amount);

        Console.WriteLine("Online confirmation email sent");

    }

}

Now, OnlineInvoice adheres to the contract defined by Invoice.

 

Safe Polymorphism

A classic example of safe polymorphism is using different types of expenses across your construction system. Suppose we define a base class Expense, and extend it into LabourExpense and TransportExpense.

class Expense

{

    public virtual string GetDetails() => "Generic expense";

}

 

class LabourExpense : Expense

{

    public override string GetDetails() => "Labour charges for site work";

}

 

class TransportExpense : Expense

{

    public override string GetDetails() => "Transport of construction materials";

}

 

void PrintExpenseDetails(Expense expense)

{

    Console.WriteLine(expense.GetDetails());

}

You can safely pass any subclass of Expense to PrintExpenseDetails, and it will behave consistently. This demonstrates LSP in action — subclasses are substitutable without breaking behavior.

 

Final Thoughts

LSP is more than just a theoretical concept. It helps you build robust, reliable, and maintainable code in real-world applications such as construction invoice systems, expense trackers, and contractor payment modules.

Whenever you’re designing with inheritance, ask yourself: Can I replace the base class with this subclass without any issues? If the answer is no, it’s time to refactor.

Respecting LSP leads to better abstractions, clearer responsibilities, and code that's easier to test and extend.

Comments

Popular posts from this blog

Promises in Angular

Mastering Your Angular Workflow: Essential CLI Commands for Efficient Development

Observables in Angular