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:
readAnyFileasks for aReadableFile, so it only counts onread()working.ReadOnlyFileinherits a workingread(). Passing it gives the same result as passing aReadableFile.WritableFilealso inheritsread(), so it is just as safe to pass.WritableFileaddsWritable. Adding an ability does not weaken the parent's promise.- A method that needs writing can ask for
Writable. AReadOnlyFilecannot 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
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
| Concept | Description |
|---|---|
| LSP compliance | ReadOnlyFile and WritableFile can replace ReadableFile without breaking its read contract. |
| Compile safety | Code that asks for Writable cannot receive a read-only object. |
| Extensibility | A new type such as EncryptedReadableFile can be added while keeping the same read promise. |
| Clarity | The implemented interfaces state the capabilities instead of hiding unsupported operations in method bodies. |
| Simpler callers | Callers 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.