27 Oct 2023



Beginner

The Liskov Substitution Principle (LSP) is a fundamental principle of object-oriented programming that states that subtypes must be substitutable for their base types. This means that any code that works with a base type should also work with any subtype without causing any unexpected behavior.

Violations of the LSP can occur for a number of reasons, such as:

  • Incorrectly defining the relationship between a base class and its subclasses. For example, if a base class represents a rectangle and a subclass represents a square, the subclass should not violate the base class's contract by allowing the width and height to be different. Problem Description: The Square subclass violates the LSP by allowing the width and height to be different.

    class Rectangle {
        protected int width;
        protected int height;
    
        public void setWidth(int width) {
            this.width = width;
        }
    
        public void setHeight(int height) {
            this.height = height;
        }
    }
    
    class Square extends Rectangle {
        @Override
        public void setWidth(int width) {
            this.width = width;
            this.height = width; // LSP violation
        }
    
        @Override
        public void setHeight(int height) {
            this.height = height;
            this.width = height; // LSP violation
        }
    }
    

    Solution: Ensure that the Square subclass maintains the square's invariant (width = height) by overriding the setWidth and setHeight methods appropriately.

    class Rectangle {
        protected int width;
        protected int height;
    
        public void setWidth(int width) {
            this.width = width;
        }
    
        public void setHeight(int height) {
            this.height = height;
        }
    }
    
    class Square extends Rectangle {
        @Override
        public void setWidth(int width) {
            super.setWidth(width);
            super.setHeight(width); // Maintain the square's invariant
        }
    
        @Override
        public void setHeight(int height) {
            super.setHeight(height);
            super.setWidth(height); // Maintain the square's invariant
        }
    }
    
  • Overriding methods in a subclass in a way that is inconsistent with the base class. For example, if a base class has a method called fly() that is implemented to throw an exception, a subclass should not override that method to actually implement the flying behavior.

    Problem Description: The Sparrow subclass violates the LSP by overriding the fly method to implement flying behavior, which is inconsistent with the base class Bird where the method throws an exception.

    class Bird {
        public void fly() {
            throw new UnsupportedOperationException("Birds can't fly");
        }
    }
    
    class Sparrow extends Bird {
        @Override
        public void fly() {
            // Actual flying behavior
        }
    }
    

    Solution: Ensure that the Sparrow subclass respects the base class contract by not overriding the fly method.

    class Bird {
        public void fly() {
            throw new UnsupportedOperationException("Birds can't fly");
        }
    }
    
    class Sparrow extends Bird {
        // Do not override the fly method
    }
    
  • Adding new methods to a subclass that are not compatible with the base class. For example, if a base class represents a bird and a subclass represents a duck, the subclass should not add a new method called swim() that is not present in the base class.

    Problem Description: The Duck subclass violates the LSP by adding a new method swim, which is not present in the base class Bird.

    class Bird {
        public void eat() {
            // Eating behavior
        }
    }
    
    class Duck extends Bird {
        public void swim() {
            // Swimming behavior
        }
    }
    

    Solution: Avoid adding new methods to a subclass that are not compatible with the base class. If swimming behavior is required, consider refactoring the base class to include it.

    class Bird {
        public void eat() {
            // Eating behavior
        }
    
        public void swim() {
            // Default swimming behavior (can be overridden by subclasses)
        }
    }
    
    class Duck extends Bird {
        // No need to add a swim method
    }
    

Some common examples of LSP violations include:

  • Allowing a subclass to have a weaker precondition than its base class. For example, if a base class has a method that requires a non-null argument, a subclass should not override that method to allow a null argument.

    Problem Description: The Circle subclass violates the LSP by adding a new method draw with fewer parameters than the base class Shape.

    class Shape {
        public void draw() {
            // Draw the shape
        }
    }
    
    class Circle extends Shape {
        @Override
        public void draw() {
            // Draw the circle
        }
    
        public void draw(int radius) {
            // Draw the circle with a specified radius
        }
    }
    

    Solution: Ensure that the Circle subclass maintains the base class method's signature when adding new methods.

    class Shape {
        public void draw() {
            // Draw the shape
        }
    }
    
    class Circle extends Shape {
        @Override
        public void draw() {
            // Draw the circle
        }
    
        public void draw(int radius) {
            // Draw the circle with a specified radius
        }
    
  • Allowing a subclass to have a stronger postcondition than its base class. For example, if a base class has a method that returns a value, a subclass should not override that method to return a different type of value.

    Problem Description: The SafeCalculator subclass violates the LSP by weakening the postcondition of the divide method by returning 0 instead of throwing an exception when dividing by zero.

    class Calculator {
        public int divide(int a, int b) {
            if (b == 0) {
                throw new IllegalArgumentException("Division by zero is not allowed");
            }
            return a / b;
        }
    }
    
    class SafeCalculator extends Calculator {
        @Override
        public int divide(int a, int b) {
            if (b == 0) {
                return 0; // LSP violation
            }
            return a / b;
        }
    }
    

    Solution: Maintain the postcondition of the divide method by throwing an exception when dividing by zero.

    class Calculator {
        public int divide(int a, int b) {
            if (b == 0) {
                throw new IllegalArgumentException("Division by zero is not allowed");
            }
            return a / b;
        }
    }
    
    class SafeCalculator extends Calculator {
        @Override
        public int divide(int a, int b) {
            if (b == 0) {
                throw new IllegalArgumentException("Division by zero is not allowed"); // Maintain postcondition
            }
            return a / b;
        }
    }
    
  • Allowing a subclass to throw new exceptions that are not caught by its base class. For example, if a base class has a method that does not throw any exceptions, a subclass should not override that method to throw a new exception. Problem Description: The SubClass violates the LSP by throwing a new exception CustomException that is not caught by the base class BaseClass.

    class BaseClass {
        public void doSomething() {
            // No exceptions thrown
        }
    }
    
    class SubClass extends BaseClass {
        @Override
        public void doSomething() throws CustomException {
            // New exception thrown
        }
    }
    

    Solution: Ensure that the SubClass does not throw new exceptions that are not caught by its base class. If necessary, catch and handle exceptions within the subclass.

    class BaseClass {
        public void doSomething() {
            // No exceptions thrown
        }
    }
    
    class SubClass extends BaseClass {
        @Override
        public void doSomething() {
            try {
                // Code that may throw an exception
            } catch (CustomException ex) {
                // Handle the exception if necessary
            }
        }
    }
    

LSP violations can make code difficult to maintain and debug, and they can also lead to unexpected runtime errors. Therefore, it is important to be aware of the LSP and to avoid violating it in your code.

Here are some tips for avoiding LSP violations:

  • Carefully consider the relationship between a base class and its subclasses before defining the inheritance hierarchy.
  • Make sure that subclasses implement the base class's contract in a consistent way.
  • Do not add new methods to subclasses that are not compatible with the base class.
  • Use interfaces to define contracts between classes, rather than relying on inheritance alone.
  • Use unit tests to verify that subclasses conform to the base class's contract.
solid-principles
liskov-substitution-principle