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)?
7
  • 6
    This is clearly an example of observing it, and your claim 'so many' is unsubstantiated. The Lucene and RichFaces projects are notorious examples, and the Windows COMM port API, but I can't think of any others offhand. And is React really a 'big software developer'?
    – user207421
    Commented Apr 30, 2017 at 21:15
  • 71
    Like any principle, the OCP has its value. But it requires that developers have infinite foresight. In the real world, people often get their first design wrong. As time goes on, some prefer to work around their old mistakes for the sake of compatibility, others prefer to eventually clean them up for the sake of having a compact and unburdened codebase. Commented May 1, 2017 at 13:11
  • 1
    When was the last time you saw an object-oriented language "as originally intended"? The core principle was a messaging system that meant every part of the system is infinitely extensible by anyone. Now compare that to your typical OOP-like language - how many allow you to extend an existing method from the outside? How many make it easy enough to be useful?
    – Luaan
    Commented May 1, 2017 at 17:06
  • 1
    Legacy sucks. 30 years of experience has shown that you should completely dump legacy and start fresh, at all times. Today everyone has connection everywhere at all times, so legacy is just totally irrelevant today. the ultimate example was "Windows versus Mac". Microsoft traditionally tried to "support legacy", you see this in many ways. Apple have always just said "F- - - You" to legacy users. (This applies to everything from languages to devices to OSs.) In fact, Apple was totally correct and MSFT was totally wrong, plain and simple.
    – Fattie
    Commented May 2, 2017 at 10:18
  • 7
    Because there are exactly zero "principles" and "design patterns" that work 100% of the time in real life. Commented May 2, 2017 at 11:45

6 Answers 6

161

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."

26
  • 14
    IMO the biggest reason to "violate" OCP is that it takes a lot of effort to conform to it properly. Eric Lippert has an excellent blog post on why many of the .NET framework classes seem to violate OCP.
    – BJ Myers
    Commented Apr 30, 2017 at 21:39
  • 10
    THIS! The OCP says that you should write code that can be changed without being touched! Why? So you only have to test, review, and compile it once. New behavior should come from new code. Not by screwing with old proven code. What about refactoring? Well refactoring is a clear violation of OCP! Which is why it's a sin to write code thinking you'll just refactor it if your assumptions change. No! Put each assumption in it's own little box. When it's wrong don't fix the box. Write a new one. Why? Because you might need to go back to the old one. When you do, It'd be nice if it still worked. Commented Apr 30, 2017 at 23:04
  • 7
    @CandiedOrange: thanks for your comment. I don't see refactoring and OCP so contrary as you describe it. To write components which follow the OCP requires often several refactoring cycles. The goal should be a component which does not need modifications to solve a whole "family" of requirements. Nevertheless, one should not add arbitrary extension points to a component "just in case", that leads too easily to overengineering. Relying to the possibility of refactoring can be the better alternative to this in lots of cases.
    – Doc Brown
    Commented May 1, 2017 at 0:17
  • 4
    This answer does a good job of calling out the errors in the (currently) top answer - I think the key thing about being successful with open/closed though is to stop thinking in terms of "extension points" and to start thinking about decomposing your class/function structure to the point where every natural extension point is there by default. Programming "outside in" is a very good way of achieving this, where every scenario your current method/function caters to is pushed out to an external interface, which forms a natural extension point for decorators, adapters etc.
    – Ant P
    Commented May 1, 2017 at 11:24
  • 3
    Refactoring it should only be considered once you have a complete set of tests that take over the job of being the valid spec and using any of the previous options is simply not an option because of the lousy design of the 1000 line monster. You should use refactoring to improve design. Not to meet requirements. When you're done you should have something you whose behavior can be changed by writing new code not by continuing to rewrite it's code. Commented May 1, 2017 at 16:25
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)

19
  • 42
    "The principle basically says you can't modify the behavior of a component. Instead you have to provide a new variant of the component with the desired behavior, and keep the old version around unchanged for backwards compatibility." – I disagree with this. The principle says that you should design components in such a way that it shouldn't be necessary to change its behavior because you can extend it to do what you want. The problem is that we haven't figured out how to do that yet, especially with the languages that are currently in wide-spread use. The Expression Problem is one part of … Commented Apr 30, 2017 at 17:39
  • 8
    … that, for example. Neither Java nor C♯ have a solution for the Expression. Haskell and Scala do, but their userbase is much smaller. Commented Apr 30, 2017 at 17:40
  • 1
    @Giorgio: In Haskell, the solution are type classes. In Scala, the solution is implicits and objects. Sorry, I don't have the links at hand, currently. Yes, multimethods (actually, they don't even need to be "multi", it's rather the "open" nature of Lisp's methods that is required) are also a possible solution. Note that there are multiple phrasings of the expression problem, because typically papers are written in such a way that the author adds a restriction to the expression problem which results in the fact that all currently existing solutions become invalid, then shows how his own … Commented Apr 30, 2017 at 18:00
  • 1
    … language can even solve this "harder" version. For example, Wadler originally phrased the expression problem to not only be about modular extension, but statically safe modular extension. Common Lisp multimethods however are not statically safe, they are only dynamically safe. Odersky then strengthened this even more by saying it should be modular statically safe, i.e. the safety should be statically checkable without looking at the whole program, only by looking at the extension module. This can actually not be done with Haskell type classes, but it can be done with Scala. And in the … Commented Apr 30, 2017 at 18:03
  • 2
    @Giorgio: Exactly. The thing that makes Common Lisp multimethods solve the EP is actually not multiple dispatch. It is the fact that the methods are open. In typical FP (or procedural programming), the type discrimination is tied to the functions. In typical OO, the methods are tied to the types. Common Lisp methods are open, they can be added to classes after the fact and in a different module. That's the feature that makes them usable for solving the EP. For example, Clojure's protocols are single dispatch, but also solve the EP (as long as you don't insist on static safety). Commented May 1, 2017 at 9:40
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!)

7
  • That's a pretty great story about SimCity. Do you have a source?
    – BJ Myers
    Commented Apr 30, 2017 at 21:26
  • 5
    @BJMyers It's an old story, Joel Spoleky mentions it near the end of this article. I originally read it as part of a book on developing video games years ago.
    – Cort Ammon
    Commented Apr 30, 2017 at 21:28
  • 1
    @BJMyers: I am pretty sure they had similar compatibility "hacks" for dozens of popular applications.
    – Doc Brown
    Commented Apr 30, 2017 at 21:33
  • 3
    @BJMyers there is plenty of stuff like this, if you want a good read go to The Old New Thing blog by Raymond Chen, browse the History tag or search for "compatibility". There is recollection of plenty of tales, including something conspicuously close to the aforementioned SimCity case - Addentum: Chen doesn't like to call names to blame.
    – Theraot
    Commented May 1, 2017 at 6:12
  • 2
    Very few things broke even in the 95->NT transition. The original SimCity for Windows still works great on Windows 10 (32-bit). Even DOS games still work perfectly fine provided you either disable sound or use something like VDMSound to allow the console subsystem to handle audio properly. Microsoft takes backwards compatibility very seriously, and they're not taking any "let's put it in a virtual machine" shortcuts either. It sometimes needs a workaround, but that's still pretty impressive, especially in relative terms.
    – Luaan
    Commented May 1, 2017 at 17:15
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.)

15
  • 3
    "You, as the creator, are not violating the OCP by changing or even removing a component." - can you provide a reference for this? None of the definitions of the principle I have seen states that "the creator" (whatever that means) is exempt from the principle. Removing a published component is clearly a breaking change.
    – JacquesB
    Commented May 1, 2017 at 11:31
  • 2
    @JacquesB People and even code changes don't violate the OCP, components (i.e. actual pieces of code) do. (And, to be perfectly clear, that means the component fails to live up to the OCP itself, not that it violates the OCP of some other component.) The whole point of my answer is that the OCP isn't talking about code changes, breaking or otherwise. A component is either open to extension and closed to modification, or it's not, just like a method may be private or not. If an author makes a private method public later on, that doesn't mean they've violated the access control, (1/2) Commented May 1, 2017 at 12:26
  • 2
    ... nor does it mean the method wasn't really private before. "Removing a published component is clearly a breaking change," is a non sequitur. Either the components of the new version satisfy the OCP or they don't, you don't need the history of the codebase to determine this. By your logic, I could never write code that satisfies the OCP. You are conflating backwards compatibility, a property of code changes, with the OCP, a property of code. Your comment makes about as much sense as saying quicksort isn't backwards compatible. (2/2) Commented May 1, 2017 at 12:26
  • 3
    @JacquesB First, note again that this is talking about a module conforming to the OCP. The OCP is advice on how to write a module so that given the constraint that the source code cannot be changed, the module can nevertheless be extended. Earlier in the paper he talks about designing modules that never change, not about implementing a change management process that never allows modules to change. Referring to the edit to your answer, you don't "break the OCP" by modifying the code of the module. Instead, if "extending" the module requires you to modify the source code, (1/3) Commented May 1, 2017 at 14:51
  • 2
    "The OCP is a property of a particular piece of code, not the evolutionary history of a codebase." - excellent!
    – Doc Brown
    Commented May 2, 2017 at 6:33
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.

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.