For example, the open-close principle. The author blames this advice on tooling of the 90s and proposes instead “Change the code to make it do something else”.
This has nothing to do with tooling, but the fact that pulling the rug under an established code base could have very unintended effects, compared to simply adding and extending functionality without touching what is already there.
By doing as the author suggests you’ll end up with either 500 broken tests or 5000 compiler errors in the best case, or in the worst case an effectively instantly legacied code base where you can’t trust anything to do what it says.
I once had to change an entire codebase’s usage of ints to uuids, which took roughly 2 whole days of fixing types and tests, even though logically it was almost equivalent. Imagine changing anything and everything to “make it do something else”.
What's the alternative here? If you had to change a codebase's usage of ints to uuids, should the original author have used dependency inversion and required an IdentifierFactory that was ints at the time so you could just swap out the implementation? And if they did - why wouldn't they have just used UUIDs in the first place? You're betting on the fact that the original author anticipated a particular avenue of further change, but also made the wrong decision for their initial implementation, which seems like the wrong bet. If they made the wrong decision, they almost certainly didn't anticipate the need for another one, either.
And how long would it have taken for the original author to use an IdentifierFactory instead of ints and write meaningful tests for it? Less than two days?
In the uuid case, the person had no choice. Remember, these are principles, not laws, and at some point your system is making concrete choices. Choosing UUIDs isn’t necessarily a OO design problem. He was just highlighting how expensive it can be if you require changes to your fundamental classes to extend or change behavior. In the identifier type case, it’s rare that folks abstract this stuff away. Though: I do know a LOT of systems that use synthetic identifiers for this exact purpose, as larger enterprises tend to deal with many more different identifier types from different integrations, from a DB type that can’t hold new identifiers, because IDs need to be sharper/distributed etc. So yeah, it’s a principal, and one should choose if it’s worth the upfront cost for its benefits.
OCP though more commonly refers to:
1. Building small, universally useful abstractions first
2. Extending behavior of that abstraction or system by writing new code rather than changing published code directly.
This is trivial when you have a few patterns under your belt. Template factories, builders, strategies, commands. I mean, while it’s not the best idea in most cases, even just inheriting a parent class and giving a new concrete new behavior is still better than changing something fundamental to the system.
Like has been said 999 times in this thread, software isn’t black and white. You have to make choices about where you go concrete and where you abstract, and gauge that against risk factors. A somewhat complex class you expect to go away in a couple months? Make it a god class that anyone who wants to can scan through. A fundamental class that will be used by hundreds of developers and underpin most operations in a production system where 5 minutes of downtime costs tens of thousands of dollars? It’s worth the upfront cost to build with these standards.
Changing ints to UUIDs is a classic example of the Primitive Obsession smell, and the solution is to wrap primitives in a type representing their semantic meaning, such as “ID,” or “PrimaryKey,” not to use a factory. That way, when you need to change the underlying type from int to UUID, you only need to do it in one place.
Indeed. Unfortunately, in some languages – like Java or C#, it is harder to do without incurring a significant cost (boxing/unboxing) than in languages that allow type aliases/typedefs.
In theory, yes, but in practice performance is dominated by network and (less often) algorithms. The cost of boxing/unboxing doesn’t even register except in rare cases, which can be specifically coded for.
It has a fair bit to do with tooling. For example, C++ suffers from the fragile base class problem and some changes can cause long compile times. Nowadays, we have tests and deployment pipelines that are explicitly designed to let us make and deploy changes safely.
Honestly, if you cannot change your code, you have a problem.
The OCP is imo poorly named, but it has far bigger implications than the post acknowledges. For one, it implies the concept of abstraction layers. In particular, base libraries should provide abstractions at the level of the library. In this way it's able to achieve being "closed for modification but open for extension".
Flipping it around, if base libraries were to be always open for modification instead of extension, then instead of writing your feature logic in your own codebase, you might be tempted to submit a pull request to React.js to add your feature logic. That sounds ridiculous but that's the equivalent of what I see a lot of new engineers do when they try to fit the feature logic they need to implement anywhere it makes sense, often in some shared library in the same codebase.
>This has nothing to do with tooling, but the fact that pulling the rug under an established code base could have very unintended effects, compared to simply adding and extending functionality without touching what is already there.
That's still a matter of tooling. With type-checking, static analysics, and test suites, changing code doesn't have "very unintended effects".
Back in the day, without those, things were much more opaque.
This has nothing to do with tooling, but the fact that pulling the rug under an established code base could have very unintended effects, compared to simply adding and extending functionality without touching what is already there.
By doing as the author suggests you’ll end up with either 500 broken tests or 5000 compiler errors in the best case, or in the worst case an effectively instantly legacied code base where you can’t trust anything to do what it says.
I once had to change an entire codebase’s usage of ints to uuids, which took roughly 2 whole days of fixing types and tests, even though logically it was almost equivalent. Imagine changing anything and everything to “make it do something else”.