As long as the new Logger
class does not contain more code, the example seems to be contrived. But imagine that class gets some more methods, with code which is independent from the concrete IMessageLogger
, for example:
public class Logger
{
// ...
public void LogFormatted(string formatString, string[] parameters )
{
string message = string.Format(formatString, parameters);
_messageLogger.Log(message);
}
}
Then it makes sense to have a class where you can implement this additional code in
one place, keeping the program DRY.
The pattern we see here is the classic "strategy" pattern. This becomes obvious when you rename IMessageLogger
to ILoggingStrategy
, and the derived classes to ConsoleLoggingStrategy
and PrinterLoggingStrategy
.
It is also a demonstration of the OCP, because now you can put Logger
and IMessageLogger
(or ILoggingStrategy
) into a reusable library (or framework), whilst new IMessageLogger
derivations can reside in application code using that lib. So the lib, including the Logger
class, won't have to be changed if someone wants to add something like a new strategy like CloudLoggingStrategy
, for example. In the original code, however, a lib containing the Logger
class would have to be changed for such an extension.
Don't follow the Open/Closed principle (OCP) unless you absolutely have to. In most application development OCP is not appropriate and only leads to overly complex code, as you have already noticed.
OCP is appropriate in scenarios where you have independent, external clients using your code, and you need to evolve the code while preserving backwards compatibility. Take the .net framework: MS cannot just modify the behavior of an existing API in the framework, because that would break all the client depending on the current behavior. They can only extend the API. OCP does not actually lead to clean code - for example the frameworks have multiple deprecated API which can never be removed. But it preserves backwards compatibility.
Another scenario is if you have a "big-ball-of-mud" architecture where each change may have unexpected side effects across the system, and no automated testing to protect against regressions. In that case you may adopt a paranoid mindset, and keep the code "closed" to avoid breaking existing code. But here not to modify the code is only treating the symptoms, and will not in itself improve the quality of the code. If possible, you should prefer to treat the root cause of the problems.
If you are not forced to follow the OCP principle then it is often cleaner and simpler to modify behavior rather than extend it.
Like all programming guidelines, OC is not an universal rule but is only appropriate in specific contexts.
Best Answer
It’s not contradictory, it’s complementary:
TDD is about writing tests to formalize and verify requirements. But it’s not about finalized user requirements: it’s about currently known design requirements. These translate/ transform some aspects of the user requirements in a way that makes sense in your design, and more specially take into account the distribution of responsibilities between your components.
OCP is about shaping your design in a way not to reinvent the wheel, but also not to break things that already work well. It allows you to specialize class, and benefit from the existing tests, and write new tests only for the specialized parts. (here I say specialization to mean extension).
So there is a synergy between the two that allows to quicker reach a stable and robust design:
This approach is therefore completely compatible with incremental or evolving user requirements, which will translate to new design requirements and refacotring.
For your API:
It’s a matter of project size. The team simply has more brainpower than any individual that composes it and a team is not efficient in long abstract discussions in a long walk.