Liskov Substitution Principle – Clean Mobile Architecture by Petros Efthymiou

Alyaa TalaatAlyaa Talaat
4 min read

The Liskov Substitution Principle (LSP) is a fundamental design principle in clean architecture that ensures derived classes or components can stand in for their base types without altering the functionality of the program. This is key for building stable, flexible applications where components are interchangeable, allowing systems to be extended with ease.


What is the Liskov Substitution Principle?

Definition: The Liskov Substitution Principle states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. In simpler terms, if you have a base class and a derived class, you should be able to substitute the derived class wherever you use the base class and expect it to work correctly.

LSP is essential in avoiding unexpected behavior and maintaining predictable interactions within a codebase, especially as it grows in complexity.


Problem Scenario

Imagine a mobile app with a notification feature that supports multiple types of notifications, such as EmailNotification and PushNotification. Each notification type should behave in a way that doesn’t break the existing notification system, regardless of which type is used. However, problems arise if, for example, some notifications can’t be sent reliably through a shared notification system due to incompatible behavior or missing functionality.


Applying the Liskov Substitution Principle

Let’s see how the Liskov Substitution Principle can be applied to ensure that all notification types function seamlessly within the notification system, following the Clean Mobile Architecture approach.

Step 1: Define a Base Class for Notifications

Start by defining a Notification interface that serves as a blueprint for all notifications.

public interface Notification {
    void send(String message);
}

Step 2: Create Concrete Implementations

Now, create concrete implementations of the Notification interface for each type of notification. Each class should ensure that it adheres to the expected behavior defined by Notification.

public class EmailNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("Sending Email: " + message);
    }
}

public class PushNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("Sending Push Notification: " + message);
    }
}

Step 3: Substitute Derived Classes Without Breaking the System

To ensure adherence to LSP, any new Notification subclass should not introduce behavior that contradicts or restricts the behavior expected by Notification. For instance, a new type of notification like SilentNotification should still be able to work within the same framework without causing unexpected results.

public class SilentNotification implements Notification {
    @Override
    public void send(String message) {
        // For a silent notification, perhaps only log it instead of sending.
        System.out.println("Logging Silent Notification: " + message);
    }
}

By adhering to the contract established by Notification, SilentNotification can be used interchangeably with other Notification types without altering the functionality of the app.

Step 4: Implement Dependency Injection for Flexibility

Using dependency injection, the application can dynamically assign different types of notifications, ensuring that it remains compliant with LSP.

public class NotificationService {
    private Notification notification;

    public NotificationService(Notification notification) {
        this.notification = notification;
    }

    public void sendNotification(String message) {
        notification.send(message);
    }
}

Step 5: Client Code

With Liskov Substitution, you can seamlessly interchange different notification types in the client code.

public class NotificationTest {
    public static void main(String[] args) {
        Notification emailNotification = new EmailNotification();
        NotificationService emailService = new NotificationService(emailNotification);
        emailService.sendNotification("Welcome to the app!");

        Notification pushNotification = new PushNotification();
        NotificationService pushService = new NotificationService(pushNotification);
        pushService.sendNotification("You have a new message!");

        Notification silentNotification = new SilentNotification();
        NotificationService silentService = new NotificationService(silentNotification);
        silentService.sendNotification("This notification is silent.");
    }
}

Output:

Sending Email: Welcome to the app!
Sending Push Notification: You have a new message!
Logging Silent Notification: This notification is silent.

The behavior remains consistent across different notification types, ensuring that LSP is upheld.


Benefits of the Liskov Substitution Principle

  • Predictability: Guarantees consistent behavior across different implementations.

  • Code Flexibility: Facilitates the addition of new types without changing existing code.

  • Reduced Bugs: Prevents unexpected behaviors that might arise from incompatible subclasses.

  • Better Testability: Allows tests to rely on predictable behaviors, improving overall testing coverage.


Bullet Points

  • The Liskov Substitution Principle ensures that derived classes or types can be substituted for base types without altering program correctness.

  • Adherence to LSP leads to code that is more predictable, adaptable, and maintainable.

  • LSP complements other SOLID principles like the Open-Closed Principle, as it encourages well-defined interfaces and prevents breaking changes.

  • Following LSP is particularly important in larger, modular systems where components need to be reliably interchangeable.


Conclusion

The Liskov Substitution Principle plays a crucial role in clean mobile architecture by ensuring that objects and their derived forms can be used interchangeably without causing unintended behavior. This adherence to predictable behavior makes the architecture more robust and adaptable, allowing mobile applications to grow and evolve with minimal disruptions. By keeping your classes compliant with LSP, you contribute to a stable, extensible codebase that stands the test of future changes and feature expansions.


0
Subscribe to my newsletter

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

Written by

Alyaa Talaat
Alyaa Talaat

As a continuous learner, I’m always exploring new technologies and best practices to enhance my skills in software development. I enjoy tackling complex coding challenges, whether it's optimizing performance, implementing new features, or debugging intricate issues.