// SOLID & Principles · 02
Open/Closed
Extend behaviour by adding code, not by editing what already works.
How it fits together
- CheckoutPage uses / calls CheckoutService (Pay(order))
- CheckoutService uses / calls PaymentHandler (by type)
- CardHandler implements PaymentHandler
- PayPalHandler implements PaymentHandler
- InvoiceHandler implements PaymentHandler
Spot it
- A switch on type grows with every feature
- Adding a case means retesting all the others
- The same if-else chain copied in many places
It costs
Every extension point is an abstraction to maintain; guess the axis wrong and it bends nothing.
In code
public interface IPaymentHandler{ bool CanHandle(PaymentType type); Task ChargeAsync(Order order, CancellationToken ct);}public sealed class CheckoutService(IEnumerable<IPaymentHandler> handlers){ public Task PayAsync(Order order, CancellationToken ct) { var handler = handlers.Single(h => h.CanHandle(order.PaymentType)); return handler.ChargeAsync(order, ct); }}public sealed class InvoiceHandler(InvoiceBook invoices) : IPaymentHandler{ public bool CanHandle(PaymentType type) => type == PaymentType.Invoice; public Task ChargeAsync(Order order, CancellationToken ct) => invoices.CreateAsync(order, ct);}Go deeperThe full explanation and what to ask for · 1 min readExpandCollapse
Bertrand Meyer's original version relied on inheritance; the reading popularised by Robert Martin uses abstractions: depend on an interface so new behaviour arrives as a new implementation, not an edit to tested code.
Closed does not mean frozen. Bugs still get fixed and code still gets refactored; the principle is about the kind of change you expect to repeat.
You cannot be closed against everything. Add the extension point after the second or third variant shows you which way change actually comes.
Ask for it
- extension point
- polymorphism
- strategy
- plugin
- protected variation
Replace the switch on payment type in CheckoutService with a PaymentHandler interface, one implementation per type, all resolved as a collection from the DI container. A new payment type should need only a new class and one registration.