84

Why do many software developers violate the open/closed principle by modifying many things like renaming functions which will break the application after upgrading?

This question jumps to my head after the fast and the continuous versions in the React library.

Every short period I notice many changes in syntax, component names, ...etc

Example in the coming version of React:

New Deprecation Warnings

The biggest change is that we've extracted React.PropTypes and React.createClass into their own packages. Both are still accessible via the main React object, but using either will log a one-time deprecation warning to the console when in development mode. This will enable future code size optimizations.

These warnings will not affect the behavior of your application. However, we realize they may cause some frustration, particularly if you use a testing framework that treats console.error as a failure.


  • Are these changes considered as a violation of that principle?
  • As a beginner to something like React, how do I learn it with these fast changes in the library (it's so frustrating)?

6 Answers6

162

IMHO JacquesB's answer, though containing a lot of truth, shows a fundamental misunderstanding of the OCP. To be fair, your question already expresses this misunderstanding, too - renaming functions breaks backwards compatibility, but not the OCP. If breaking compatibility seems necessary (or maintaining two versions of the same component to not break compatibility), the OCP was already broken before! (See (*) below for the possible root cause of this misunderstanding.)

As Jörg W Mittag already mentioned in his comments, the principle does not say "you can't modify the behavior of a component" - it says, one should try to design components in a way they are open for being reused (or extended) in several ways, without the need for modification. This can be done by providing the right "extension points", or, as mentioned by @AntP, "by decomposing a class/function structure to the point where every natural extension point is there by default." IMHO following the OCP has nothing in common with "keeping the old version around unchanged for backwards compatibility"! Or, quoting @DerekElkin's comment below:

The OCP is advice on how to write a module [...], not about implementing a change management process that never allows modules to change.

Good programmers use their experience to design components with the "right" extension points in mind (or - even better - in a way no artificial extension points are needed). However, to do this correctly and without unnecessary overengineering, you need to know beforehand how future use cases of your component might look like. Even experienced programmers can't look into the future and know all upcoming requirements beforehand. And that is why sometimes backwards compatibility needs to be violated - no matter how many extension points your component has, or how well it follows the OCP in respect to certain types of requirements, there will always be a requirement which cannot be implemented easily without modifying the component.

(*)Side note: Robert Martin admitted in 2013 that his original definition of the OCP from 1996 was poorly worded and could lead readers to mistakenly believe that its intent was to prohibit changes. In https://blog.cleancoder.com/uncle-bob/2013/03/08/AnOpenAndClosedCase.html, he wrote "What it means is that you should strive to get your code into a position such that, when behavior changes in expected ways, you don’t have to make sweeping changes to all the modules of the system. Ideally, you will be able to add the new behavior by adding new code, and changing little or no old code."

Doc Brown
  • 218,378
71

Edit: There seem to be some disagreement about what the open-closed principle actually entails. Luckily Bertrand Meyer, who defined the principle, have put his book online, so you can read it yourself rather than trust dubious second-hand sources. The discussion of the open-Closed principle is on page 57-61.

Meyer defines the open/closed principle as a class or module should not change its public interface after it is "published". But the class should also be open in the sense that functionality can be added or patched through overriding in sub-classes. Meyer allow implementation changes in the original class e.g. to fix bugs, but any change to the public interface would be a violation of the open-closed principle.

The principle is therefore not purely a design principle, but also a development method - Mayer called it "organized hacking" - where you alter behavior through overriding in sub-classes rather than though changing the original code.

Meyers does allow fixing design mistakes in the original class, but only in the case where you are able to modify all code depending on the class. This is only possible in closed projects, but not if the code has been published for use like React.

Now to the actual question:


The open/closed principle has benefits, but it also has some serious drawbacks.

In theory the principle solves the problem of backwards compatibility by creating code which is "open for extension but closed for modification". If a class has some new requirements, you never modify the source code of the class itself but instead creates a subclass which overrides just the appropriate members necessary to change the behavior. All code written against the original version of the class is therefore unaffected, so you can be confident your change did not break existing code.

In reality you easily end up with code bloat and a confusing mess of obsolete classes. If it is not possible to modify some behavior of a component through extension, then you have to provide a new variant of the component with the desired behavior, and keep the old version around unchanged for backwards compatibility.

Say you discover a fundamental design flaw in a base class which lots of classes inherit from. Say the error is due to a public field being of the wrong type. You cannot fix this by overriding a member. Basically you have to override the whole class, which means you end up extending Object to provide an alternative base class - and now you also have to provide alternatives to all the subclasses, thereby ending up with a duplicated object hierarchy, one hierarchy flawed, one improved. But you cannot remove the flawed hierarchy (since deletion of code is modification), all future clients will be exposed to both hierarchies.

Now the theoretical answer to this problem is "just design it correctly the first time". If the code is perfectly decomposed, without any flaws or mistakes, and designed with extension points prepared for all possible future requirement changes, then you avoid the mess. But in reality everyone makes mistakes, and nobody can predict the future perfectly.

Take something like the .NET framework - it still carries around the set of collection classes which were designed before generics were introduced more than a decade ago. This is certainly a boon for backwards compatibility (you can upgrade framework without having to rewrite anything), but it also bloats the framework and presents developers with a large set of options where many are simply obsolete.

Apparently the developers of React have felt it was not worth the cost in complexity and code-bloat to strictly follow the open/closed principle.

The pragmatic alternative to open/closed is controlled deprecation. Rather than breaking backwards compatibility in a single release, old components are kept around for a release cycle, but clients are informed via compiler warnings that the old approach will be removed in a later release. This gives clients time to modify the code. This seems to be the approach of React in this case.

(My interpretation of the principle is based on The Open-Closed Principle by Robert C. Martin)

JacquesB
  • 61,955
  • 21
  • 135
  • 189
21

I would call the open/closed principle an ideal. Like all ideals, it gives little consideration to the realities of software development. Also like all ideals, it is impossible to actually attain it in practice -- one merely strives to approach that ideal as best as one can.

The other side of the story is known as the Golden Handcuffs. Golden Handcuffs are what you get when you slave yourself to the open/closed principle too much. Golden Handcuffs are what occur when your product which never breaks backwards compatibility can't grow because too many past mistakes have been made.

A famous example of this is found in the Windows 95 memory manager. As part of the marketing for Windows 95, it was stated that all Windows 3.1 applications would work in Windows 95. Microsoft actually acquired licenses for thousands of programs to test them in Windows 95. One of the problem cases was Sim City. Sim City actually had a bug which caused it to write to unallocated memory. In Windows 3.1, without a "proper" memory manager, this was a minor faux pas. However, in Windows 95, the memory manager would catch this and cause a segmentation fault. The solution? In Windows 95, if your application name is simcity.exe, the OS will actually relax the constraints of the memory manager to prevent the segmentation fault!

The real issue behind this ideal is the pared concepts of products and services. Nobody really does one or the other. Everything lines up somewhere in the grey region between the two. If you think from a product oriented approach, open/close sounds like a great ideal. Your products are reliable. However, when it comes to services, the story changes. It's easy to show that with the open/closed principle, the amount of functionality your team must support must asymptotically approach infinity, because you can never clean up old functionality. This means your development team must support more and more code every year. Eventually you reach a breaking point.

Most software today, especially open source, follows a common relaxed version of the open/closed principle. It's very common to see open/closed followed slavishly for minor releases, but abandoned for major releases. For example, Python 2.7 contains many "bad choices" from the Python 2.0 and 2.1 days, but Python 3.0 swept all of them away. (Also, the shift from the Windows 95 codebase to the Windows NT codebase when they released Windows 2000 broke all sorts of things, but it did mean we never have to deal with a memory manager checking the application name to decide behavior!)

Cort Ammon
  • 11,917
  • 3
  • 26
  • 35
13

Doc Brown's answer is closest to accurate, the other answers illustrate misunderstandings of the Open Closed Principle.

To explicitly articulate the misunderstanding, there seems to be a belief that the OCP means that you should not make backwards incompatible changes (or even any changes or something along these lines.) The OCP is about designing components so that you don't need to make changes to them to extend their functionality, regardless of whether those changes are backwards compatible or not. There are many other reasons besides adding functionality that you may make changes to a component whether they are backwards compatible (e.g. refactoring or optimization) or backwards incompatible (e.g. deprecating and removing functionality). That you may make these changes doesn't mean that your component violated the OCP (and definitely doesn't mean that you are violating the OCP).

Really, it's not about source code at all. A more abstract and relevant statement of the OCP is: "a component should allow for extension without need of violating its abstraction boundaries". I would go further and say a more modern rendition is: "a component should enforce its abstraction boundaries but allow for extension". Even in the article on the OCP by Bob Martin while he "describes" "closed to modification" as "the source code is inviolate", he later starts talking about encapsulation which has nothing to do with modifying source code and everything to do with abstraction boundaries.

So, the faulty premise in the question is that the OCP is (intended as) a guideline about evolutions of a codebase. The OCP is typically sloganized as "a component should be open to extensions and closed to modifications by consumers". Basically, if a consumer of a component wants to add functionality to the component they should be able to extend the old component into a new one with the additional functionality, but they should not be able to change the old component.

The OCP says nothing about the creator of a component changing or removing functionality. The OCP is not advocating maintaining bug compatibility forevermore. You, as the creator, are not violating the OCP by changing or even removing a component. You, or rather the components you've written, are violating the OCP if the only way consumers can add functionality to your components is by mutating it e.g. by monkey patching or having access to the source code and recompiling. In many cases, neither of these are options for the consumer which means if your component isn't "open for extension" they are out of luck. They simply can't use your component for their needs. The OCP argues to not put the consumers of your library into this position, at least with respect to some identifiable class of "extensions". Even when modifications can be made to the source code or even the primary copy of the source code, it's best to "pretend" that you can't modify it as there are many potential negative consequences to doing so.

So to answer your questions: No, these are not violations of the OCP. No change an author makes can be a violation of the OCP because the OCP is not a proporty of changes. The changes, however, can create violations of the OCP, and they can be motivated by failures of the OCP in prior versions of the codebase. The OCP is a property of a particular piece of code, not the evolutionary history of a codebase.

For contrast, backwards compatibility is a property of a change of code. It makes no sense to say some piece of code is or is not backwards compatible. It only makes sense to talk about the backwards compatibility of some code with respect to some older code. Therefore, it never makes sense to talk about the first cut of some code being backwards compatible or not. The first cut of code can satisfy or fail to satisfy the OCP, and in general we can determine whether some code satisfies the OCP without referring to any historical versions of the code.

As to your last question, it's arguably off-topic for StackExchange in general as being primarily opinion-based, but the short of it is welcome to tech and particularly JavaScript where in the last few years the phenomenon you describe has been called JavaScript fatigue. (Feel free to google to find a variety of other articles, some satirical, talking about this from multiple perspectives.)

0

You don’t or shouldn’t have the goal to conform to OCP at all. It is a tool that makes life easier if you use it properly, that’s all. And it comes at a cost. You will as a reasonable developer always weigh what the cost and what the benefits are.

One thing that affects the cost is the size of the audience. In a project that affects only my team, I have the tools to rename a method or a variable and it’s absolutely fine because I have all the users of the code under my control. If I write a library used by thousand people it’s different.

To use OCP I need to anticipate behaviour changes that people may want. An example is an iOS UITableViewController. Apple anticipated that you want to show a table with a variable number of items, with or without sections, with different item types requiring different code to draw an item, or to measure the size of an item. And some things I forgot. You can change all these without changing the class. If you do something that isn’t anticipated you are stuck. It would be madness to create a subclass of UITableViewController to display different items, so OCP saves you a lot of work.

So what React does, is it a violation of OCP? Answer: I don’t care, they seem to have good reasons. How do you learn something with changes? Answer: I’m in my 60s and have no problems. If you can’t, that keeps me competitive.

gnasher729
  • 49,096
0

Several good answers have already been given, but I want to focus on another part of your question that's not been explicitly addressed.

The source of your frustration stems from developers changing existing code. I'm unfortunately going to have to burst your bubble here: that's a healthy development practice.

It's unrealistic to expect a developer to write code that will remain perfect in perpetuity. Not just because the situation might change down the line in a way that a person could not account for it at the time, but also because people's understanding of a problem domain might mature over time and they'll become more aware of better approaches later.

OCP and SOLID are clean coding guidelines. The 'cleanliness' of code is not expressed as how long code can remain unchanged, but rather the change-friendliness of code, i.e. the ability to reasonably contain the blast radius when a change needs to be made. It doesn't tell you how to prevent making changes in the first place.
There is no guideline that can tell you how to write perfect code, because the correctness of your implementation hinges on your specifications, which are inherently contextual and not universally definable.

I understand the ambiguity that this introduces for OCP, which does seem to argue that the base implementation should never be changed, but that is too harsh of an interpretation of what the guideline actually tries to convey. Instead of reading it like this:

You should never update your code once it has been written.

A more realistic interpretation would be:

Try to structure your code in a way that you don't have to repeatedly update the same bit of code in the same way over and over, such as adding another concrete implementation to a list of known implementations, because it's liable to keep growing over time and you don't want this to be an additional location that you have to remember to update every time.


Every short period I notice many changes in syntax, component names, ...etc

I've already pointed out that code is liable to change over time, but in response to your comment here I'd like to dig into this a little further.

Part of the expectation that things can be written in stone once and then kept that way indefinitely is a waterfall mindset (as opposed to an agile mindset). Waterfall/agile isn't a matter of right/wrong, but rather that they are different approaches to requirements analysis and development.

You'll find that a lot of software teams are shifting into an agile mindset, because it better aligns with how the requirements are being defined. Requirements are usually not something that you can perfect at the first pass, but rather an evolving and growing list of "it would be nice if ..." which is often even based on previous versions you've delivered. This requires iterative development, which means releasing a version, letting the end users work with it, collecting feedback on what can be done better, and then changing the product to further refine what it brings to the table.

React is very much an agile product. New versions fix things that have been learned from previous versions, which is also going to include restructuring the existing components to provide a more harmonious framework whose components are better balanced.

It sucks that this means things change, but that's inherently part of the product you're signing up to use. This is also mitigated by versioned releases (i.e. semver), where breaking changes are only added in major releases. If you don't want to deal with breaking changes, then don't upgrade to a newer major release.


The core point I wanted to touch on here is that the underlying insistence in this question is one of wanting things to not change, and unfortunately that is just not a very compatible expectation to have in the landscape that is software development, especially as the world is shifting to a predominantly agile mindset.

Flater
  • 58,824