L — Liskov Substitution Principle (LSP): Objects of a subclass should be usable anywhere an object of the parent class is expected, without breaking the program's correctness.
class Rectangle {
function setWidth(w) { this.width = w }
function setHeight(h) { this.height = h }
function area() { return this.width * this.height }
}
class Square extends Rectangle {
function setWidth(w) { this.width = w; this.height = w } // forces height too!
function setHeight(h) { this.width = h; this.height = h }
}
function test(Rectangle rect) {
rect.setWidth(5)
rect.setHeight(10)
assert(rect.area() == 50) // passes for Rectangle, FAILS for Square (area is 100)
}
This is the classic LSP violation: a Square technically 'is-a' Rectangle' geometrically, but substituting a Squarewhere aRectangleis expected breaks the caller's reasonable assumptions about howsetWidth/setHeight` behave independently. LSP tells us this inheritance relationship, despite looking geometrically correct, is a poor OOP design choice.
I — Interface Segregation Principle (ISP): Clients shouldn't be forced to depend on methods they don't use — prefer several small, focused interfaces over one large, general-purpose one.
// Violates ISP — a Robot doesn't eat, but must implement eat() anyway
interface Worker {
function work()
function eat()
function sleep()
}
class Robot implements Worker {
function work() { ... }
function eat() { throw Error("Robots don't eat!") } // forced, nonsensical implementation
function sleep() { throw Error("Robots don't sleep!") }
}
// Follows ISP — split into focused interfaces, implement only what applies
interface Workable { function work() }
interface Eatable { function eat() }
interface Sleepable { function sleep() }
class Robot implements Workable { function work() { ... } } // only implements what it needs
class Human implements Workable, Eatable, Sleepable { ... }
D — Dependency Inversion Principle (DIP): High-level modules shouldn't depend on low-level modules directly — both should depend on abstractions (interfaces).
// Violates DIP — OrderService is tightly coupled to one specific email implementation
class OrderService {
private mailer = new GmailSender() // hardcoded, concrete dependency
function placeOrder() { this.mailer.send(...) }
}
// Follows DIP — OrderService depends on an abstraction, not a concrete class
interface Notifier { function send(message) }
class GmailSender implements Notifier { function send(message) { ... } }
class SmsSender implements Notifier { function send(message) { ... } }
class OrderService {
function __construct(private Notifier notifier) {} // any Notifier works
function placeOrder() { this.notifier.send(...) }
}
service = new OrderService(new GmailSender()) // or SmsSender — swappable, testable
This pattern — passing dependencies in rather than creating them internally — is called Dependency Injection, and is the practical technique most commonly used to satisfy DIP. It's foundational to how most modern frameworks (Laravel, Spring, ASP.NET) are structured, and makes code dramatically easier to test, since real dependencies can be swapped for fakes/mocks in tests.