SOLID Principles
Five principles that keep object-oriented code malleable and easy to extend without breakage.
Overview
SOLID is a set of five principles for writing object-oriented code that is easy to extend, test, and maintain. Single Responsibility: a class has one reason to change. Open/Closed: open for extension, closed for modification. Liskov Substitution: subtypes must be substitutable for their base types. Interface Segregation: no client should depend on methods it does not use. Dependency Inversion: depend on abstractions, not concrete implementations.
Origin
Robert C. Martin (Uncle Bob) assembled these principles from existing literature in his 2000 paper "Design Principles and Design Patterns". Michael Feathers coined the SOLID acronym in 2004. The principles themselves draw on Bertrand Meyer's Open/Closed Principle (1988), Barbara Liskov's substitution principle (1987), and earlier OOP research.
Examples
Single Responsibility and Dependency Inversion
# Violates SRP: UserService does auth, email, AND persistence
class UserService
def register(email, password)
user = User.create!(email: email, password: bcrypt(password))
Mailer.send_welcome(email) # knows about email
Analytics.track('signup', email) # knows about analytics
user
end
end
# Each class has one reason to change
class UserRegistrar
def initialize(repo:, mailer:, analytics:) # DIP: depend on abstractions
@repo, @mailer, @analytics = repo, mailer, analytics
end
def register(email:, password:)
user = @repo.create(email: email, password: hash(password))
@mailer.send_welcome(user)
@analytics.track(:signup, user)
user
end
endInjecting dependencies makes every collaborator replaceable with a test double, a different implementation, or null object without touching UserRegistrar.
Liskov Substitution Principle, the silent killer
// Classic LSP violation: Square inherits Rectangle
class Rectangle {
setWidth(w) { this.width = w }
setHeight(h) { this.height = h }
area() { return this.width * this.height }
}
class Square extends Rectangle {
setWidth(w) { this.width = this.height = w } // Breaks the contract!
setHeight(h) { this.width = this.height = h } // width changes too
}
function resizeAndArea(rect) {
rect.setWidth(4)
rect.setHeight(5)
return rect.area() // caller expects 20; Square returns 25
}
// Fix: don't use inheritance; use separate types with a common interface
const isResizable = (shape) => typeof shape.setWidth === 'function'Use Cases
- 01Evaluating whether a proposed class hierarchy or module boundary is sound
- 02Code review checklist for new service objects and value objects
- 03Identifying code that is hard to test, LSP and DIP violations usually make mocking painful
- 04Guiding refactors of god classes (SRP) and fragile base classes (LSP)
When Not to Use
- //As a dogma that every class must satisfy all five principles at all times, apply proportionally to complexity
- //In throwaway scripts or prototypes where the cost of applying them exceeds the benefit
- //When strict adherence produces so many tiny classes that the indirection obscures the intent
Technical Notes
- SRP is the most misunderstood: "one reason to change" means one stakeholder or actor drives change, not one method
- OCP is achieved through polymorphism and dependency injection, not through open-ended extension hooks added speculatively
- DIP is the enabling principle: once you depend on an abstraction, swapping implementations (for testing, for scaling) is mechanical
- ISP matters most in strongly-typed languages; Ruby duck typing often makes it less acute, but it still guides API design
More in Architecture