3. Liskov Substitution Principle

Introduction

When building software systems, ensuring that components are interchangeable without causing errors is crucial for robust architecture. The Liskov Substitution Principle (LSP), a key element of the SOLID principles, asserts that objects of a superclass should be replaceable with objects of its subclasses without affecting the application’s correctness. This principle promotes reliability and reusability in object-oriented programming.

Understanding the Liskov Substitution Principle

LSP is designed to ensure that a subclass can stand in for its superclass without disrupting the functionality of the program. Adhering to this principle helps in building software that is easy to upgrade and maintain, with components that are interchangeable. In simple terms, it is – 

A subclass should fit perfectly in place of its parent class without causing any issues.

Why is LSP Important?

  • Enhances Modularity: LSP makes it easier to manage and evolve software systems as new types of components can replace existing ones without additional modifications.
  • Reduces Bugs: By ensuring that subclasses can serve as stand-ins for their superclasses, LSP reduces the likelihood of errors during code extension.
  • Improves Code Flexibility: It allows developers to use polymorphism more effectively, making the software easier to understand and modify.

LSP in Action: Java Example

Consider a class hierarchy where Bird is a superclass, and it has several subclasses including Duck and Ostrich.

Without LSP:

class Bird {
    void fly() {
        // logic to fly
    }
}

class Duck extends Bird {
    // Ducks can fly
}

class Ostrich extends Bird {
    void fly() {
        throw new UnsupportedOperationException("Ostriches cannot fly");
    }
}

In this scenario, using an Ostrich object in place of a Bird can cause the program to fail if the fly method is called.

With LSP:

abstract class Bird {

}
abstract class FlyingBird extends Bird {
void fly() {
// logic to fly
}
}

class Duck extends FlyingBird {
// Ducks can fly
}

class Ostrich extends Bird {
// No fly method
}

This design adheres to LSP by separating birds that can fly from those that cannot, eliminating the issue of inappropriate method calls.

LSP in Action: Python Example

Let’s look at a payment system where Payment is a superclass, and it has several subclasses such as CreditCardPayment and CashPayment.

Without LSP:

class Payment:
    def process_payment(self, amount):
        pass

class CreditCardPayment(Payment):
    def process_payment(self, amount):
        print("Processing credit card payment")

class CashPayment(Payment):
    def process_payment(self, amount):
        raise NotImplementedError("Cash payments are not supported")

Using CashPayment in a context expecting a Payment can lead to runtime errors due to unsupported operations.

With LSP:

class Payment:
def process_payment(self, amount):
pass

class CreditCardPayment(Payment):
def process_payment(self, amount):
print("Processing credit card payment")

class CashPayment(Payment):
def process_payment(self, amount):
print("Processing cash payment")

By ensuring all subclasses can indeed perform process_payment, we maintain the integrity of the system.

Conclusion

The Liskov Substitution Principle is fundamental in creating scalable and robust software architectures. By ensuring that subclasses can effectively replace their superclasses, developers can build systems that are easier to maintain and extend without fear of breaking existing functionality.

Now that you know about LSP, think about how it might be applied in your current projects and reflect on any past issues where violating LSP caused problems. 

2. Open/Closed Principle

 

Source: dillbert.com

Introduction

Lets visualise a scenario where Carl, the only developer who knows how to program a critical legacy system, decides to quit. Suddenly, the team is left in a mess, not knowing how to manage or update the system. This predicament highlights a common pitfall in software development: over-reliance on specific individuals for knowledge and maintenance of a system. It underscores the importance of designing software that is resilient and adaptable, principles that are central to the Open/Closed Principle (OCP). OCP advocates for software entities to be open for extension but closed for modification, enabling systems to evolve without the need for extensive reworking or specialized knowledge. Let’s explore how applying OCP can transform a software system into a more flexible, maintainable, and scalable architecture.

Understanding the Open/Closed Principle

Software entities like classes, functions, modules, interfaces, etc. should be open for extension, but remain closed for modification.

– Open/Closed Principle

OCP is a fundamental design guideline that encourages developers to write code that doesn’t have to be changed every time the requirements change. Instead, developers should be able to extend existing code to introduce new functionality. This approach reduces the risk of bugs because you’re not modifying the existing tested and proven code.

Why is OCP Important?

We need OCP for the following reasons – 
  • Minimizes Risk: Changes to existing code can introduce bugs in systems that were previously working fine. By extending systems without modifying existing code, OCP reduces this risk.
  • Enhances Flexibility: It allows systems to grow over time through the addition of new features without the need to redesign or risk existing functionality.
  • Simplifies Maintenance: Reducing the need to alter existing code means that systems become easier to maintain and less complex to manage.

  • OCP in Action:

    Java Example – Report Generation System

    Imagine a report generation system where we initially only needed to generate HTML reports, but now we also need to support PDF reports.

    Without OCP:

    class ReportGenerator {
    public void generateReport(String reportType) {
    if (reportType.equals("HTML")) {
    // Generate HTML report
    } else if (reportType.equals("PDF")) {
    // Generate PDF report
    }
    }
    }

    Impact of Violation:

  • Code Fragility: Each time a new report type needs to be added, the ReportGenerator class must be modified. This can introduce bugs in the existing report generation logic due to changes in a class that already works correctly for current report types.
  • Increased Maintenance: Over time, as more report types are added, this class will grow increasingly complex and harder to maintain, manage, and test effectively.
  • With OCP:

    interface ReportGenerator {
    void generateReport();
    }

    class HtmlReportGenerator implements ReportGenerator {
    public void generateReport() {
    // Generate HTML report
    }
    }

    class PdfReportGenerator implements ReportGenerator {

    public void generateReport() {

    // Generate PDF report

    }

    }

    With OCP, we can see that new report types can be added without modifying existing code, ensuring ease of extending functionality with minimal errors.

    Python Example – Graphic Rendering System

    Let’s consider a simple graphic rendering system where we might start with rendering shapes, but later need to add filters.

    Without OCP:

    class GraphicRenderer:
    def render(self, shape):
    if shape.type == 'circle':
    # Render a circle
    elif shape.type == 'square':
    # Render a square

    # Adding a new shape would require changing the GraphicRenderer class.

    Impact of OCP Violation: 

  • Limited Scalability: The GraphicRenderer class is directly dependent on specific shapes. Adding a new shape means modifying this class, increasing the risk of errors in existing rendering functionality.
  • Tight Coupling: The class is tightly coupled with the shape implementations. Changes in shape handling can affect rendering code, leading to a brittle system prone to bugs during modifications.
  • With OCP:

    class Shape:
    def render(self):
    pass

    class Circle(Shape):
    def render(self):
    # Render a circle


    class Square(Shape):
    def render(self):
    # Render a square
    }

    # you can add new shapes by creating a class for that shape and extending the Shape class

    With OCP, we can see that new shapes can be added by simply extending the Shape class, ensuring stability and scalability.

    Conclusion

    The Open/Closed Principle is about building software systems that accommodate growth and change as naturally as possible. By adhering to OCP, developers can extend the capabilities of their software without the constant risk of breaking existing functionality.

    Can you now reflect on your own projects? Are there areas where applying OCP could simplify the addition of new features?