75

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
    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 Apr 30 '17 at 21:15
  • 63
    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. – Theodoros Chatzigiannakis May 1 '17 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 May 1 '17 at 17:06
  • 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 May 2 '17 at 10:18
  • 4
    Because there are exactly zero "principles" and "design patterns" that work 100% of the time in real life. – Matti Virkkunen May 2 '17 at 11:45
147

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!

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

  • 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 Apr 30 '17 at 21:39
  • 2
    @BJMyers: thanks for the link. Jon Skeet has an excellent post about the OCP as beeing very similar to the idea of protected variation. – Doc Brown Apr 30 '17 at 21:42
  • 8
    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. – candied_orange Apr 30 '17 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 May 1 '17 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 May 1 '17 at 11:24
68

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 private 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)

  • 37
    "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 … – Jörg W Mittag Apr 30 '17 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. – Jörg W Mittag Apr 30 '17 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 … – Jörg W Mittag Apr 30 '17 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 … – Jörg W Mittag Apr 30 '17 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). – Jörg W Mittag May 1 '17 at 9:40
20

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!)

  • That's a pretty great story about SimCity. Do you have a source? – BJ Myers Apr 30 '17 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 - Reinstate Monica Apr 30 '17 at 21:28
  • 1
    @BJMyers: I am pretty sure they had similar compatibility "hacks" for dozens of popular applications. – Doc Brown Apr 30 '17 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 May 1 '17 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 May 1 '17 at 17:15
11

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

  • 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 May 1 '17 at 11:31
  • 1
    @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) – Derek Elkins May 1 '17 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) – Derek Elkins May 1 '17 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) – Derek Elkins May 1 '17 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 May 2 '17 at 6:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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