The SOLID Principles
Five principles, one goal
Section titled “Five principles, one goal”SOLID is an acronym for five principles of object-oriented design. They share a single aim: code that absorbs change without breaking, and that you can test in isolation. Almost every pattern later in this course is really one of these principles applied to a specific problem, so learning them now means the patterns will feel familiar rather than arbitrary.
Single Responsibility Principle
Section titled “Single Responsibility Principle”A class should have one reason to change. If a class both formats a report and emails it, a change to the email logic risks breaking the formatting, and vice versa. Splitting those concerns into separate classes means each has a single, well-defined job — and a single force that can require it to change.
Open/Closed Principle
Section titled “Open/Closed Principle”Software should be open for extension but closed for modification. You should be able to add new behaviour by adding new code, not by editing tried-and-tested code. A payment processor that switches on a payment-type string must be edited for every new method; one that accepts a PaymentMethod interface gains new methods by adding a class, leaving the processor untouched.
Liskov Substitution Principle
Section titled “Liskov Substitution Principle”Any subtype must be usable wherever its base type is expected, without surprising the caller. If a Square subclass of Rectangle silently changes both dimensions when you set the width, code that worked with rectangles breaks with squares. The subtype broke the contract; substitution must be honest.
Interface Segregation Principle
Section titled “Interface Segregation Principle”Clients should not be forced to depend on methods they do not use. A fat Machine interface with print, scan, and fax forces a simple printer to implement fax it cannot perform. Several small, focused interfaces let each client depend on exactly what it needs.
Dependency Inversion Principle
Section titled “Dependency Inversion Principle”High-level policy should not depend on low-level details; both should depend on an abstraction. Below, a NotificationService depends on a MessageSender interface rather than a concrete email class. Swapping in SMS — or a fake for tests — requires no change to the service at all.
interface MessageSender { send(to: string, body: string): void;}
class EmailSender implements MessageSender { send(to: string, body: string): void { console.log(`email to ${to}: ${body}`); }}
class NotificationService { // Depends on the abstraction, not a concrete sender. constructor(private readonly sender: MessageSender) {}
notify(user: string): void { this.sender.send(user, 'Your order has shipped'); }}
const service = new NotificationService(new EmailSender());from typing import Protocol
class MessageSender(Protocol): def send(self, to: str, body: str) -> None: ...
class EmailSender: def send(self, to: str, body: str) -> None: print(f"email to {to}: {body}")
class NotificationService: # Depends on the abstraction, not a concrete sender. def __init__(self, sender: MessageSender) -> None: self._sender = sender
def notify(self, user: str) -> None: self._sender.send(user, "Your order has shipped")
service = NotificationService(EmailSender())package main
import "fmt"
type MessageSender interface { Send(to, body string)}
type EmailSender struct{}
func (EmailSender) Send(to, body string) { fmt.Printf("email to %s: %s\n", to, body)}
// NotificationService depends on the abstraction, not a concrete sender.type NotificationService struct { sender MessageSender}
func (n NotificationService) Notify(user string) { n.sender.Send(user, "Your order has shipped")}
func main() { service := NotificationService{sender: EmailSender{}}}trait MessageSender { fn send(&self, to: &str, body: &str);}
struct EmailSender;
impl MessageSender for EmailSender { fn send(&self, to: &str, body: &str) { println!("email to {to}: {body}"); }}
// Depends on the abstraction, not a concrete sender.struct NotificationService { sender: Box<dyn MessageSender>,}
impl NotificationService { fn notify(&self, user: &str) { self.sender.send(user, "Your order has shipped"); }}
fn main() { let service = NotificationService { sender: Box::new(EmailSender) };}Inverting a dependency
Section titled “Inverting a dependency”The diagram below contrasts the rigid version, where high-level policy points straight at a concrete detail, with the inverted version, where both depend on an abstraction.
classDiagram
class NotificationService
class MessageSender {
<<interface>>
+send(to, body)
}
class EmailSender
class SmsSender
NotificationService --> MessageSender : depends on abstraction
EmailSender ..|> MessageSender : implements
SmsSender ..|> MessageSender : implements