Liskov Substitution Principle of SOLID (BPD)

Continuing our study to Be a Professional Developer and exploring SOLID principles, today we will dive into the 3rd principle of SOLID, Liskov Substituition Principle (LSP).

I found it challenging to understand this principle because there are many ways to violate LSP.  Before we check that, it’s important to understand Polymorphism.

Polymorphism

One of the four pillars from Object Oriented Programming (OOP).

Polymorphism essentially works through Overloading and Overriding.

Overloading

When we have multiple methods with the same name but different parameter lists. Typescript is not ideal for demostrating overloading, so let’s look at an example in Java:

class MathOperation {
    public int sum(int a, int b) { return a + b; }  
    public long sum(long a, long b) { return a + b; }
}

System.out.println("sum int: " + new MathOperation().sum(1, 2));
System.out.println("sum long: " + new MathOperation().sum(3L, 5L));

You can use the Java Playground to see the result:

sum int: 3
sum long: 8

In the example above, the methods have the same number of parameters but different types. The number of parameters can also vary:

class Product {
    public int buy(int price) { return price; } 
    public int buy(int price, int discount) { return price - discount; }
}

System.out.println("Buying product... Final price: " + new Product().buy(10));
System.out.println("Buying product... Final price: " + new Product().buy(10, 2));

Result:

Buying product... Final price: 10
Buying product... Final price: 8

 

Overriding

When a subclass overrides a behavior of the superclass. Check the typescript code:

export interface Animal {
    sound() : string;
}

export class Dog implements Animal {
    sound(): string { return "Bark..." }
}

export class Cat implements Animal {
    sound(): string { return "Meow..." }
}
import { Dog, Cat, Animal } from "./polymorphism/overriding";

printSound(new Dog());
printSound(new Cat());

function printSound(animal: Animal) {
    console.log('Animal sound: ', animal.sound());    
}

Result:

Animal sound:  Bark...
Animal sound:  Meow...

LSP (Liskov Substitution Principle)

With a foundation in Polymorphism, we can now explore the LSP, introduced by Barbara Liskov in 1987. The 3rd principle of SOLID states that an object of a superclass should be replaceable with objects of a subclass without altering the correctness of the program.

Key points

  • Behavioral Consistency: Subclasses should behave in a way that does not violate the expectations set by the base class.
  • Substitutability: Objects of the subclass should be able to replace objects of the superclass without causing errors or unexpected behavior.
  • Contract Adherence: Reafirm that Bertrand Meyer said: the contract from base class should be hounred for the subclass.

 

Violating the LSP

As mentioned earlier, there are many ways to violating the LSP and we need to pay attention to some warning signs, such as:

if (instance instanceof SuperClass) instance.setSomeVar(10);
else instance.setSomeAnotherVar(0);

The code above is an example of code smell and likely indicates that the principle has been violated.

How to know if the principle was violated?

According to the Liskov Substitution Principle article, from DicionarioTec, there are six ways to violate the LSP.

1 – The type of a subtype method’s parameter must be contravariant to the supertype

The code below works in Typescript, however, it violates the principle:

export class Employee {
    getSalary() { return 1000;}
}

export class Manager extends Employee{
    getSalary() { return super.getSalary() * 2;}
}
import { Employee, Manager } from "./violation/violation1";

testViolation1();

function testViolation1() {    
    getSalary(new Employee()); // Violates the LSP principle
}

function getSalary(manager: Manager) {
    console.log(`Salary: ${manager.getSalary()}`);
}

2 – The type of the value returned by the subtype method must be covariant to the supertype

The code below works in Typescript, however, it violates the principle:

export class Car {
    create() : Car { return new Car()};
}

export class Fiat extends Car {
    create() : Car { return new Car()}; // Violates the LSP principle
}
import { Fiat } from "./violation/violation2";

testViolation2();

function testViolation2() {    
    new Fiat().create(); 
}

3 – The subclass must not throw exceptions that are not subtypes of the exceptions thrown by the superclass, and must maintain the semantics expected by the client using the superclass.

Check this out:

// Superclass
export class DatabaseConnection {
    connect(): void {
        console.log("Connecting to the DatabaseConnection...");
        throw new Error("General connection error!"); // Superclass throws a general error
    }
}

// Subclass
export class SecureDatabaseConnection extends DatabaseConnection {
    connect(): void {
        console.log("Connecting to the SecureDatabaseConnection...");
        throw new AuthenticationError("Authentication failed!"); // Violates LSP by throwing an unrelated exception
    }
}

// Custom error class specific to authentication
export class AuthenticationError extends Error {
    constructor(message: string) {
        super(message);
        this.name = "AuthenticationError";
    }
}
import { SecureDatabaseConnection, DatabaseConnection, AuthenticationError } from "./violation/violation3";

// Testing with the subclass
initializeDatabase(new SecureDatabaseConnection());
initializeDatabase(new DatabaseConnection());


// Function that expects a DatabaseConnection type
function initializeDatabase(connection: DatabaseConnection) {
    try {
        connection.connect();
    } catch (error) {
        if (error instanceof AuthenticationError) {
            console.error("Caught an authorization error:", error); // Handles AuthorizationError
        } else {
            console.error("Caught a general error:", error); // Expected to handle only general Error
        }    
    }
}

Console output:

Connecting to the SecureDatabaseConnection...
Caught an authorization error: AuthenticationError: Authentication failed!
    at SecureDatabaseConnection.connect (/Users/karanalpe/Documents/dev/git/personal/be-a-professional-developer/6-liskov-substitution-principle/out/violation/violation3.js:16:15)
    at initializeDatabase (/Users/karanalpe/Documents/dev/git/personal/be-a-professional-developer/6-liskov-substitution-principle/out/index.js:10:20)
    at Object.<anonymous> (/Users/karanalpe/Documents/dev/git/personal/be-a-professional-developer/6-liskov-substitution-principle/out/index.js:5:1)
    at Module._compile (node:internal/modules/cjs/loader:1469:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1548:10)
    at Module.load (node:internal/modules/cjs/loader:1288:32)
    at Module._load (node:internal/modules/cjs/loader:1104:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:174:12)
    at node:internal/main/run_main_module:28:49
Connecting to the DatabaseConnection...
Caught a general error: Error: General connection error!
    at DatabaseConnection.connect (/Users/karanalpe/Documents/dev/git/personal/be-a-professional-developer/6-liskov-substitution-principle/out/violation/violation3.js:8:15)
    at initializeDatabase (/Users/karanalpe/Documents/dev/git/personal/be-a-professional-developer/6-liskov-substitution-principle/out/index.js:10:20)
    at Object.<anonymous> (/Users/karanalpe/Documents/dev/git/personal/be-a-professional-developer/6-liskov-substitution-principle/out/index.js:6:1)
    at Module._compile (node:internal/modules/cjs/loader:1469:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1548:10)
    at Module.load (node:internal/modules/cjs/loader:1288:32)
    at Module._load (node:internal/modules/cjs/loader:1104:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:174:12)

What’s the problem?

  • Semantic Consistency: The types of errors should align with client expectations. Throwing a specific error can violate LSP if it introduces unexpected behavior in client code.
  • Error Handling Logic: The client’s error handling logic may not account for specific exceptions, potentially leading to unhandled exceptions or logic failures.
  • Client Expectations: The subclass must maintain the expected behavior and contract defined by the superclass so that it can be substituted without altering the program’s desirable properties, such as proper error handling.

How to solve?

Change to a generic Error:

export class SecureDatabaseConnection extends DatabaseConnection {
    connect(): void {
        console.log("Connecting to the SecureDatabaseConnection...");
        throw new Error("Authentication failed!"); // Solve the Violates LSP 
    }
}

4 – A method’s preconditions cannot be stronger than the base class

// Superclasse
export class Payment {
    process(amount: number): void {
        if (amount <= 0) { throw new Error("Invalid payment amount."); }
        console.log(`Processing payment of $${amount}`);
        // ...
    }
}

// Subclasse
export class CreditCardPayment extends Payment {
    process(amount: number): void {
        if (amount <= 10) { throw new Error("Minimum payment amount is $10 for credit card payments."); } // Violates LSP Principle: Strongest precondition
        super.process(amount); 
        // ...
    }
}
import { CreditCardPayment, Payment } from "./violation/violation4";


try {
    const payment = new CreditCardPayment();
    payment.process(8); 
} catch (error) {
    console.error("Error:", error);
}

Console output:

Error: Error: Minimum payment amount is $10 for credit card payments.
    at CreditCardPayment.process (/Users/karanalpe/Documents/dev/git/personal/be-a-professional-developer/6-liskov-substitution-principle/out/violation/violation4.js:18:19)
    at Object.<anonymous> (/Users/karanalpe/Documents/dev/git/personal/be-a-professional-developer/6-liskov-substitution-principle/out/index.js:6:13)
    at Module._compile (node:internal/modules/cjs/loader:1469:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1548:10)
    at Module.load (node:internal/modules/cjs/loader:1288:32)
    at Module._load (node:internal/modules/cjs/loader:1104:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:174:12)
    at node:internal/main/run_main_module:28:49

How to solve?

Change the exception to a warning, like this:

export class CreditCardPayment extends Payment {
    process(amount: number): void {
        if (amount <= 10) { console.warn("For credit card payments, it's recommended to have a minimum amount of $10.") }
        super.process(amount); 
        // ...
    }
}

 

5 – A method’s postconditions must not be weaker than those of the base class

// Superclasse
export class Payment {
    process(amount: any): string {
        // ...

        if (typeof amount != 'number'|| amount <= 0) { throw new Error("Invalid payment amount."); }
        return "Payment successful"; // Strong postcondition: guarantees a success message.
    }
}

// Subclass
export class CreditCardPayment extends Payment {
    process(amount: any): string {
        // ...

        if (typeof amount != 'number') { throw new Error("Invalid payment amount."); } // Violates LSP Principle: Weaker postcondition
        return "Payment successful"; 
    }
}
import { CreditCardPayment } from "./violation/violation5";


try {
    const payment = new CreditCardPayment();
    console.log(`Response: ${payment.process(0)}`); 
} catch (error) {
    console.error("Error:", error);
}

Console output:

Response: Payment successful

How to solve?

Call the super method:

export class CreditCardPayment extends Payment {
    process(amount: any): string {
        // ...

        return super.process(amount);
    }
}

 

6 – Invariants must be preserved

// Superclass
export class DatabaseConnection {
    SERVER_LOCAL = "localhost:8080"

    connect(): void {
        console.log(`Connecting to: ${this.SERVER_LOCAL}` );
    }
}

// Subclass
export class SecureDatabaseConnection extends DatabaseConnection {
    connect(): void {
        // Add security...
        this.SERVER_LOCAL = "192.168.1.2"; // Violates LSP by Invariants must be preserved
        console.log(`Connecting to: ${this.SERVER_LOCAL}` );
    }
}

 

Conclusion

This article was a bit long because to understanding LSP requires recognizing how it can be violated, and there are many ways to do. Some strongly-typed programming languages, like Java, prevent violations of LSP rules, however, with the increasing use of Typescript, it’s important to understand these potential violations.

I hope you enjoyed the article. Bye 🙂

 

Code

 

Previous Article

Architecture Pattern vs Design Pattern (BPD)

Leave a Reply