“Open” means the design allows new behavior to be added. “Closed” means existing code does not need to be edited each time that happens. A stable interface can let new implementations supply that behavior while existing code keeps using the same interface.
In the shipping notification example, NotificationDispatcher chooses a sender for each update. Its original send method checks the channel itself, so adding push notifications requires another branch.
Language
Original code
class NotificationDispatcher {
send(message: string, channel: string) {
if (channel === 'email') {
new EmailSender().send(message)
} elseif (channel === 'sms') {
new SmsSender().send(message)
} else {
throw new Error('Unknown channel: ' + channel)
}
}
}
Revised code
interface Sender {
send(message: string): void
}
class EmailSender implements Sender {
send(message: string) { emailApi.send(message) }
}
class SmsSender implements Sender {
send(message: string) { smsApi.send(message) }
}
class PushNotificationSender implements Sender {
send(message: string) { pushApi.send(message) }
}
class NotificationDispatcher {
constructor(private senders: ReadonlyMap<string, Sender>) {}
send(message: string, channel: string) {
const sender = this.senders.get(channel)
if (!sender) throw new Error('Unknown channel: ' + channel)
sender.send(message)
}
}
const senders = newMap<string, Sender>([
['email', new EmailSender()],
['sms', new SmsSender()],
['push', new PushNotificationSender()],
])
const dispatcher = new NotificationDispatcher(senders)