This series clarifies the Open-Closed Principle with examples. This post describes some approaches that may look like the OCP, but don’t deliver the expected value.
Previous post in this series covered Implicit data assumptions and externally-owned abstractions. Those are common traps to avoid, but I won’t reproduce them here.
Anti-Example: Abstract Thread
The focus of this post is a hard fail I managed early in my journey to understand the Open-Closed Principle.
This example brings us back to the chat library. Before I tried tags, I tried to use generics to allow for caller-defined custom data.
This approach was a hot mess. The complexity of the chat library exploded. Generics cascaded through the functions and data. I was allowing the caller to decide what type derivative I passed back to them, so the library couldn’t safely instantiate any of its own types.
Data store operations like saving and querying became a nightmare. I had to store some fields separately so I could query them, but then also had to serialize the whole object in order to restore object state. Querying based on custom fields like
CampaignId required translating arbitrary predicates into sql queries. I didn’t know what data might exists or where it might live on the type, so predicates were about my only option.
This generic-based approach to custom data tries to invite the caller’s domain into its own. It tries to become flexible as a whole rather than defining contained flexibility on it’s own terms.
Contrast this with the tag-based approach.
The tag based approach contains uncertainty to the tags field. Even the tags field constraints the outer shape of that flexibility. This allows the thread client to deal with tags confidently. Callers haven’t lost any of the desired functionality, and the library is much easier to work with.
Focusing too much on flexibility can undermine the Open-Closed Principle. Remember that the contract for extension belongs to the component offering flexibility. That flexibility should be contained and modeled as part of the component’s domain. Require callers to adapt within your terms, don’t invite the caller’s domain into your own.