This series clarifies the Open-Closed Principle with examples. This post recaps what we’ve learned

Motivating Questions

Remember the motivating questions for the Open-Closed Principle

  • How can we publish libraries that can adapt to unknown users without changing library code?
    • In general, how do we accommodate unknown use cases?
  • How do we prevent changes from cascading between services (or other kinds of components)?
  • How does a service isolate it’s domain and support semantics needed by the caller?
  • How can a service compose new dependencies without changing the service?
  • Design stamina: How can code handle more use cases but remain simple?

Take a moment to reflect on what predictions you made about these questions at the start of the series. How has your perspective changed?

Recap

The Open-Closed Principle is one of the SOLID principles. It states

You should be able to extend the behavior of a system without having to modify that system. - Bob Martin

The OCP illuminates how components offer flexibility defined on the component’s own terms, allowing the component to adapt to caller needs without changing internally. This is much like how parameters enable functions to be resused by many consumers without changing the function.

The OCP enables components to focus on the essence of their own problem domain and defer flexible details to other components. Focusing too much on flexibility or reuse can undermine expected value. Instead focus on isolating domains from each other.

Some of our primary tools to create this flexibility and independence are

  • Flexible or semi-structured data, like tags. This allows callers to imprint their own meaning on data without requiring the component to handle the data differently.
  • Passing behaviors like event handlers and callbacks allow resuse of flows with interchangable behaviors
  • Self-owned interfaces allow non-domain dependencies to be deferred for callers to inject later.
  • Dependency Inversion with adapters to map between services, also known as the Ports and Adapters pattern. This allows services to become reusable libraries that are composed into different use cases as needed. This pattern also isolates cross-cutting concerns and integrations, allowing different system configurations to be accumulated and swapped rather than ripped and replaced.