LSP (Liskov Substitution Principle) — the third principle of SOLID, which defines the conditions for correct inheritance in object-oriented programming. The principle was formulated by Barbara Liskov in 1987 and formalized as: if S is a subtype of T, then objects of type T can be replaced by objects of type S without changing the properties of the program. As noted in Robert Martin’s book Clean Architecture (2017), the substitution principle requires that a subclass not weaken the contract of the base class.
Key Takeaways
LSP (Liskov Substitution Principle) — the substitution principle formulated by Barbara Liskov at the OOPSLA conference in 1987. Formal definition: let q(x) be a provable property of objects x of type T. Then q(y) must be provable for objects y of type S, where S is a subtype of T. Simply put: objects of a subclass must behave so that code working with the base class continues to work correctly with the subclass as well.
In practice, LSP means that a subclass must not violate the contract of the base class. The contract includes preconditions (what is required to call a method), postconditions (what is guaranteed after the call), and invariants (conditions that persist throughout the object’s lifetime). A subclass may strengthen preconditions or weaken postconditions — this is precisely an LSP violation.
A classic example of LSP violation is a square inheriting from a rectangle. The setWidth method of a rectangle sets the width, while for a square it sets both width and height. A client expecting rectangle behavior (changing one side does not affect the other) gets an unexpected result. A square is not a valid subtype of a rectangle.
LSP establishes three conditions for correct inheritance: subclass preconditions cannot be stronger than base class preconditions (the subclass does not require more), subclass postconditions cannot be weaker than base class postconditions (the subclass guarantees no less), and base class invariants must be preserved in the subclass. These conditions are known as the Design by Contract rule according to Bertrand Meyer.
If at least one condition is violated, code using polymorphism may fail. The compiler does not check semantic contracts, only syntactic ones. Therefore, LSP is a matter of architectural discipline, not static typing.
The LSP mechanism is based on behavioral compatibility of types. If class S inherits from class T, client code should be able to use S wherever T is expected without changing its behavior. This includes not only method signatures but also their semantics.
LSP does not forbid a subclass from adding new behavior. It is forbidden to violate the expectations of code written for the base class. If the base class guarantees that the save method does not throw exceptions, the subclass must not throw them. If the base class returns a non-negative value, the subclass must not return a negative one.
In real projects, LSP is most often violated when adding conditional logic in subclass methods: «if condition — throw an exception», «if condition — return null». Each such «surprise» undermines polymorphism and forces client code to check the object type before calling — which contradicts the very idea of object-oriented design.
In mobile projects, a typical LSP violation occurs when creating base ViewModels. If BaseViewModel guarantees that the onCleared method releases all resources, and a subclass overrides this method as empty — any code relying on resource cleanup through a polymorphic onCleared call will work incorrectly. LSP requires that the subclass either calls super.onCleared() or performs the same work itself. Composition via LifecycleObserver is an alternative that eliminates LSP violation in lifecycle management.
Key indicators of LSP violation include: checking the object type via instanceof or is before calling a method, empty method implementations (stubs), throwing NotImplementedError or UnsupportedOperationException, returning null instead of a value. Each of these patterns signals that the subclass is not a valid subtype.
Another common sign is inheritance aimed at code reuse rather than modeling an «is-a» relationship. Class Bird has a fly() method. Class Penguin inherits from Bird and overrides fly() as empty or throwing an exception. This is an LSP violation: a penguin is not a valid subtype of bird.
In mobile development, LSP is violated when creating base ViewHolder, Fragment, or ViewController classes with stub methods. If a subclass does not use half of the base class methods — inheritance was chosen incorrectly. Composition or interface segregation solves the problem more correctly.
A simple test for checking LSP: write a unit test for the base class that verifies its contract (return values, exceptions, side effects). Run this test for every subclass. If the test fails — LSP is violated. This approach is called «testing through the base class contract».
In Android projects, such a test is useful for ViewModel and Repository. If BaseViewModel guarantees a Loading state before an error, and a subclass throws an error without Loading — the test will catch the LSP violation at the CI stage.
Let’s look at an Android example with ClickListener handling. An LSP violation occurs when the base implementation guarantees something and the subclass violates it.
// Base class with guarantee: onClick will be called
open class BaseClickListener {
open fun onClick(view: View) {
// basic handling
}
}
// LSP violation: subclass adds a condition throwing an exception
class RestrictedClickListener : BaseClickListener() {
override fun onClick(view: View) {
if (!isLoggedIn) {
throw IllegalStateException("Not logged in")
}
super.onClick(view)
}
}
// Correct solution: contract not violated
class ConditionalClickListener : BaseClickListener() {
override fun onClick(view: View) {
if (isLoggedIn) {
super.onClick(view)
}
}
}
An iOS example with the DataSource protocol demonstrates LSP violation by returning nil instead of data:
// Protocol with contract: returns data or error
protocol DataProvider {
func fetchData() async throws -> [String]
}
// LSP violation: returns nil without error
class SilentFailProvider: DataProvider {
func fetchData() async throws -> [String] {
return [] // empty array instead of error
}
}
// Correct LSP compliance
class NetworkProvider: DataProvider {
func fetchData() async throws -> [String] {
throw NetworkError.timeout
}
}
A practical rule: if a subclass cannot fulfill the base class contract, it should not be a subclass. An alternative is to extract an interface with a minimal contract and implement it in each type in its own way.
Composition is preferable to inheritance in situations where the «is-a» relationship is ambiguous or conditional. A classic example: is a Manager an Employee? Yes. But is a Square a valid Rectangle? LSP says «no». If you doubt the correctness of inheritance — choose composition.
In mobile development, composition is often used through dependency injection: instead of inheriting behavior from a base class, a class receives it through a constructor. A ViewModel does not inherit from Repository but accepts it as a dependency. This eliminates LSP violation by definition — no inheritance, no contract violation.
Signs that inheritance should be replaced with composition: the subclass does not use some base class methods, the subclass overrides methods with empty stubs, client code checks the object type via instanceof. In these cases, inheritance was chosen incorrectly and LSP is violated.
Interfaces solve the LSP problem without inheritance: each type implements exactly the methods it needs. Instead of a common base class Bird with a fly() method (where Penguin cannot fly) — a Flyable interface that only flying birds implement. Penguin implements Bird without a fly() method — LSP is not violated.
In Android architecture, this approach is applied through segregated UseCase interfaces: instead of one large UseCase with getAll, getById, save, delete methods — separate GetItemsUseCase and SaveItemUseCase interfaces. A client depends only on the needed interface, and any class implementing that interface is correct from the LSP perspective.
Frequently Asked Questions
Inheritance is a language mechanism; LSP is a rule for correct use of that mechanism. Inheritance guarantees signature compatibility (syntax); LSP requires behavioral compatibility (semantics). Inheritance without LSP gives polymorphism that breaks at runtime.
If the base class guarantees a non-null return — yes. If the contract allows null (optional value) — no. LSP does not forbid null; it forbids weakening the contract. Study the base class documentation and check whether the subclass contract is compatible.
LSP applies to protocols the same way as to classes. A protocol implementation must comply with the semantic contract: if a protocol defines a method as non-throwing, the implementation must not throw errors. Swift does not check this at the compiler level — the responsibility lies with the developer.
Sealed classes in Kotlin are a special case because the hierarchy is closed and known to the compiler. LSP applies to sealed classes to a lesser extent because all subtypes are explicitly enumerated in the when expression. A sealed subclass error will be local rather than a hidden polymorphic error.
Write a parameterized test for the base class that runs for all its subclasses. The test verifies key behavioral contracts: return values, exceptions, states. If the test fails on one of the subclasses — LSP is violated. In CI, such a test prevents regression of polymorphic code.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also