Skip to content

LSP - The Liskov Substitution Principle

If code works with a base type, it must keep working with any subtype. No surprise exceptions, no stricter inputs, no weaker promises.

Builds on Abstract classes and inheritance and Method overriding and polymorphism

Stage 2core2 min read

LSP - Good Code Example


Code Overview

package SOLID.LSP.GoodCode;

// Every implementation of Readable promises that read() works.
public interface Readable {
    void read();
}

// Writing is a separate capability. Read-only types never promise it.
public interface Writable {
    void write();
}

public class ReadableFile implements Readable {
    @Override
    public void read() {
        System.out.println("Reading from a file");
    }
}

public class ReadOnlyFile extends ReadableFile {
    // It inherits a real read() implementation and makes no write promise.
}

public class WritableFile extends ReadableFile implements Writable {
    @Override
    public void write() {
        System.out.println("Writing to a file ....");
    }
}

public class Main {
    public static void main(String[] args) {
        ReadableFile readableFile = new ReadOnlyFile();
        readableFile.read();

        WritableFile writableFile = new WritableFile();
        writableFile.read();
        writableFile.write();

        readAnyFile(readableFile);
        readAnyFile(writableFile);
    }

    public static void readAnyFile(ReadableFile file) {
        file.read();
    }
}

The comments on the interfaces and classes are added for teaching. The repo .java files have none.

There are now two honest promises. Readable promises read(), and Writable promises write(). A class takes on only the promises it can keep.


Why This Code is Good

Follow each substitution one at a time:

  1. readAnyFile asks for a ReadableFile, so it only counts on read() working.
  2. ReadOnlyFile inherits a working read(). Passing it gives the same result as passing a ReadableFile.
  3. WritableFile also inherits read(), so it is just as safe to pass.
  4. WritableFile adds Writable. Adding an ability does not weaken the parent's promise.
  5. A method that needs writing can ask for Writable. A ReadOnlyFile cannot be passed, and the compiler catches the mistake.

No subtype throws an exception for something its parent promised. Callers need no subtype checks, so swapping objects stays safe.


Class Hierarchy Diagram

Loading diagram…

ReadOnlyFile keeps the read promise. WritableFile keeps it too and adds writing through Writable.

A small naming warning: Java already has java.lang.Readable. The local interface works in this package, but a name such as ReadableSource avoids confusion in real projects.


Benefits of This Design

ConceptDescription
LSP complianceReadOnlyFile and WritableFile can replace ReadableFile without breaking its read contract.
Compile safetyCode that asks for Writable cannot receive a read-only object.
ExtensibilityA new type such as EncryptedReadableFile can be added while keeping the same read promise.
ClarityThe implemented interfaces state the capabilities instead of hiding unsupported operations in method bodies.
Simpler callersCallers do not need instanceof checks before reading or writing.

Summary

The design splits read and write into two honest abilities. ReadableFile promises only reading, both subclasses keep that promise, and WritableFile explicitly adds writing. Every subtype can stand in for its parent without a surprise exception or changed meaning.

This does add more types. Add these interfaces only when some code really asks for "anything readable" or "anything writable". Do not create an interface that no caller uses.

Compare with BadCode and read LSP-Notes.md for the complete guide.

In this topic

After code6 files

GoodCode/Main.java22 linesGitHub
package SOLID.LSP.GoodCode;public class Main {    @SuppressWarnings("unused")    public static void main(String[] args) {        ReadableFile readableFile = new ReadOnlyFile();        readableFile.read();        WritableFile writableFile = new WritableFile();        writableFile.read();        writableFile.write();        readAnyFile(readableFile);        readAnyFile(writableFile);    }    public static void readAnyFile(ReadableFile file) {        file.read();    }}
GoodCode/Readable.java6 linesGitHub
package SOLID.LSP.GoodCode;public interface Readable {    void read();}
GoodCode/ReadableFile.java10 linesGitHub
package SOLID.LSP.GoodCode;public class ReadableFile implements Readable {    @Override    public void read() {        System.out.println("Reading from a file");    }}
GoodCode/ReadOnlyFile.java4 linesGitHub
package SOLID.LSP.GoodCode;public class ReadOnlyFile extends ReadableFile {}
GoodCode/Writable.java6 linesGitHub
package SOLID.LSP.GoodCode;public interface Writable {    void write();}
GoodCode/WritableFile.java9 linesGitHub
package SOLID.LSP.GoodCode;public class WritableFile extends ReadableFile implements Writable {    @Override    public void write() {        System.out.println("Writing to a file ....");    }}

Done with this topic?

How well do you know it?

PreviousOCP - The Open/Closed Principle
Practise 7 questions