When Do You Kiss Backwards Compatibility Goodbye?
Arandir asks: "Backwards compatibility is great for users. But it sucks for developers. After a while your normally sensible and readable code becomes a nightmare spaghetti tangle of conditions, macros and multiple reinventions of the wheel. Eventually you have to kiss off backwards compatibility as more trouble than it's worth. The question is, when? Should my code conform to POSIX.1 or Single UNIX 2? Should I expect the user to have a ISO Standard C++ compiler? What about those users with SunOS-4.1.4, Slackware-3.2, and FreeBSD-2.2?" This question is really kind of difficult to answer in the general sense. The best advice one can give, of course, is "when you can get away with it". Not much help, that, but the lost of backwards compatibility, like most complex decisions, depends on a lot of factors. The key factor in most developers eyes, of course, is the bottom line. Have many of you been faced with this decision? What logic did you use to come to your decision and what suggestions do you have for others who might find themself in this predicament?
Ironically I am doing it right now. Good part it is Saturday, and other developers do not know. Or they will lynch me..
<^>_<(ô ô)>_<^>
You should know who they are, what equipment they have, who is making who a favor (ie: who has to adapt to whom), and specially you should know what they want (such as how much backward compatibility).
Abandoning backwards compatibility is often a controversial action, that needs to be carefully considered beforehand. Often, the best course of action is to consult with the Licensing Dept. They've been making great progress in the time it takes to reword licensing so that it is illegal for end users to attempt to use older hardware and/or software, often they can provide a solution in less than 3 months. This provides the Legal Dept. with a steady stream of secondary revenue, when they audit, and sometimes sue those users who call support hotlines. A win-win situation for all of us!
The two things I would say are, when you really reach the point where all the old crap is really clogging up the veins, fix it all at once. Make a clean break. Then people can at least keep in mind what is happening, what works with 2.x and what is still only for 1.x.
The other thing is, try to design to keep this from happening. Expose APIs that don't need to change much instead of the actual functions or objects that you use. One more level of indirection won't kill your performance in almost every case, but it will give you a whole lot more room to re-engineer when you decide you have to.
All that applies to the case where you control the interface and you need to change it. When you're publishing source code and want to decide what tools you can expect the user to have to make use of it, that's a marketing decision and not a technical one. You're talking about how many people will be excluded from your audience if you use GTK or assume a conformant C++ compiler. Technically the newer tools and libs are generally better, that's pretty clear. I think it's going to be a judgement call on the part of the developers as to how much they care about a lot of people being able to use their code. If they are willing to wait for the world to catch up before being able to use their program, then they can use the latest and greatest. If not, then they have to aim at a realistic profile.
I think I like Apple's backwards compatibility, but it's not making one app backwards compatible,
For instance, when they migrated to the PPC architecture, they made apps that ran on both platforms and older OSes, then capped development and froze the older version at whatever version number, then developed for newer machines. The rest of the apps followed, like AOL 2.7 is AOL's last 68k release, they just developed only for PPC since then, although apps like Opera still make a 68k version alongside a PPC version.
Of course, you need to know your users, Will they be satisfied with a frozen and version capped release, provided there are no bugs?
I think there are several approach you may choose.
If you think your program/library has been widely adopted by many people, it would be very very hard to scrap the old one and start anew. You will provoke the wrath of other developers that use your program/library. If this is the case, then the possible compromise can be:
To do revamping, you'd probably want to look on how the Windows COM approach. I'd hate to say this, but this approach is generally good, but I don't know whether I can come up with a better solution.
Or alternatively do an OOP approach. OOP can help modularizing your code if it is done properly. (That's why KDE rocks)
That's my 2c, though.
--
Error 500: Internal sig error
This is the story of my app, Kinkatta ( http://kinaktta.sourceforge.net/ ). It originally was a QT only app and only recently did it move over to utilizing KDE. But that in itself isn't exactly what this is about so I will talk about the more spisific case. From .25 to .91 today we have gone through I believe 4 different formats in which we save our settings, buddylists and so forth. In each of the changes we had to add some code to convert it over. The best solution we found is to know our users and make sure they know about us. What I mean about that is that we only support the previous settings format and make sure everyone uses it. Kinkatta's users are kept informated when a new release is out through a number of ways. We then keep that format for as long as it takes for us to be sure that 99% of the users are using that format. One of the nice things incorperated into kinkatta is the auto-check feature. On login it will goto the webpage and see if there is a new version and if there is then it will tell the user. This prompts them to stay up with the new releases much more then if the feature was not there (and yes you can disable it). Do to the gpl/lgpl nature of the app people will upgrade more often and are unlikly to stay with version 0.64.1 This is a true plus point for the open source. Because of it we havn't had to worry about users who don't want to pay the 29.95 for the new version.
Do you changes clothes while making the "chee-chee-cha-cha-choh" transformation sound?
Luckily open source doesn't have ot suffer from the issue as much since source availability ensures that old software can often be tweaked or sometimes just recompiled to make it work with new versions of dependent libraries.
How long to maintain backwards compatibility is really the question of your business domain. An in house app can probably be changed significantly without impacting many people while a widget library (like QT for example) must maintain backwards compatibility for at least a couple of minor versions. The ability to simply recompile old code after a major change in the library is a welcome feature too.
Your pizza just the way you ought to have it.
God created the universe in 6 days because He didn't have to worry about an installed base.
Did that make sense to you before you pressed the submit button? It sure seems pretty silly now. You're suggesting updating an old version to new standards. That is what the entire concept of versioning is based around! You cap old versions and start anew for the entire purpose of keeping your applications up to date.
I'm a loner Dottie, a Rebel.
Two rules of thumb:
1) Support whatever 90% of your users are using
2) Support the prior two versions
If you can't do the above, make a clean break and give it a new name or change the major version number and list the changes in the release notes.
If you have to make a clean break, if possible:
1) Provide a migration path
2) Provide an interop interface
And above all, listen to your users.
"... Forget the above rules and use java."
1.0, 1.1 or 1.2? IBM or Sun or Blackdown? AWT or Swing?
It takes a lot more than OO design to solve this problem. The fact is that initial designs are almost always naive, and lack the specific flexibilities (and inflexibilities) the Ideal Solution would require. So, very often, are second iterations. But the third time's usually the charm.
The need to depart from backward compatibility is the result of correcting design flaws. And design flaws happen, regardless of your programming methodology. Design flaws are less a product of programming methodology, and more a product of not completely knowing the problem.
Java accommodates design flaws a little bit better not by being object-oriented, but by relying so heavily on dynamic binding. While this can make recovering from Bad Design simpler, it only lets you create bandaids that do nothing to fix the original design problems. And all that dynamic binding costs Java in performance across the board.
Release a set of updates, only change the minor version number, break one critical function in each update, fix it and break a different critical function in the next. Repeat until users no longer depend on the functionality that you want to change, then introduce the new functionality.
;)
But first, go read "How to write bad code," and start following those suggestions too.
Java accommodates design flaws a little bit better not by being object-oriented, but by relying so heavily on dynamic binding.
FYI, dynamic binding is a crucial element in OO programming. Programmers wouldn't have been able to downcast if you don't have this. It is indeed that this creates a lot of slowdown, but researcher has done flow analysis research to eliminate many of this.
Concerning the design: Yes, you're right. Nobody starts with a "perfect" design. We will extend it, twist it and twirl it until it comes into a huge mess when we have to scrap the whole thing and rethink it.
But, design flaws can be minimized. It doesn't depend on programming methodologies, but rather to a design methodologies -- which is pretty much debated by now. Using models (e.g. UML models) to depict a huge project maybe worthwhile because we can get a quick overview of what we are upto. Most of the times we can locate the design mistakes pretty quickly.
Design flaws can also be minimized by documenting specification (which is pretty much a "meta-programming" approach). Sadly, nobody wants to do this. If you can get this done, there are a lot of tools (albeit still in research) that can automatically check your program whether it conforms to the spec or not.
--
Error 500: Internal sig error
I like Microsoft's solution, which I'm sure was done somewhere else first. At any rate, here's the situation...
:)
:)
:)
Microsoft face the issue that they want to maintain backwards compatibility with everything, so they can leverage off the existing popularity of their platform. This means they can't let new libraries break old applications.
They had a problem once with MFC, where they updated its memory allocation scheme to make it more optimal. Problem was, a whole lot of old apps happened to work because they accessed memory incorrectly, but the old memory allocation scheme didn't reveal their bugs (I think they were doing buffer overruns, but the old scheme allocated a bit of extra room). Anyway, Microsoft released an updated MFC DLL, and suddenly old applications started breaking. It wasn't really MS' fault, but it was a big event, and I think it was the last time they touched old code like that!
Anyway, this is where they have their "DLL hell" problem too. Different apps are written against different versions of a DLL, many times with workarounds for known bugs in those DLLs. A new DLL comes along (installed by another application) and suddenly working applications start breaking.
So here comes COM. I've encountered it with DirectX, and it works like this. When you request something like a DirectDraw object or a DirectDrawSurface object, you also tell the class factory what *version* of the interface you want. Then you're provided with that actual implementation of the library. If you write your game against DirectX 2, but the user has DirectX 5, well your request to the DirectX DLL will actually give you the version 2 implementation! Which is cool, because if you worked around old bugs, those bugs are still there; they're not fixed for you!
As far as I know, you're not getting an "emulation" of the old implementation either; I'm pretty sure they just include binaries for each version of the interface. They could easily dynamically load whichever one you request.
Of course it means bloat, but there's no other solution. If you want backwards compatibility, you can't fake it, because it's all the really subtle things (like bugs that people worked around) that will bite you in the butt. It's been Microsoft's nemesis but its strength through the years, just as it has been Intel's too. Both companies need to remain backwards compatible with a *LOT* of stuff out there, and so they're forced to have all this legacy architecture. It would be nice to start from scratch, but then they'd be levelling the playing field, and that's no fun
Of course, this backwards compatibility drama affects everyone, not just Windows people. Just the other day someone installed a new PAM RPM on my Mandrake Linux. It installed fine, being a newer version, but some subtle change in behaviour meant I could no longer log into my own box. That's no fun either
- Brendan
Commit yourself to a strict policy: nothing in a minor version will break anything since the last major version. If your code is at 1.9.99, it should be backwards-compat with 1.0.0. If your code is at 1.1.0 and backwards compatability breaks, move it to a 2.0 release.
Typically, users expect breakage--or, at the very least, problems related to upgrades--with major versions. With minor versions, they don't expect breakage.
Follow the Law of Least Surprise. If you break backwards compatability, up the major by one.
Insofar as when to break backwards compatability, that's a much harder question. The obvious answer is a little philosophic: not all engineering problems can be solved by saying ``screw backwards compatability'', and some engineering problems cannot be solved without saying it.
The trick is learning which is which.
So where does one get a box full of all these unixes? I do agree with you on that part about broad testing. But I can't afford to buy all those boxes (especially not an IBM zSeries). And finding shell accounts (especially root ones) for various testing seems to not go beyond Linux, FreeBSD, and a handful of OpenBSD and Solaris. Even the IBM mainframe accounts are available to only a few people, and then for a limited time (have you ever known an open source project to take 3 months and stop development then because it's "done"?). I do have Linux, FreeBSD, and OpenBSD on Intel, and Linux, OpenBSD, and Solaris on Sparc. What else would you suggest?
As for Java ... I'm waiting until environments are built that can do what I do now in C. I'm hoping gcj will let me do at least some of these things in Java. But there are some things I doubt it can ever do. You can prove me wrong by rewriting LILO and init into Java.
now we need to go OSS in diesel cars
A lot of the discussion seems to be related to issues of things like programming languages and operating systems (which are important). But what about keeping up with old formats and protocols? I think the issue is more one of what your project works with, than it is what language you choose (including the OS as part of the former).
I'm not so much looking for specific answers to the above questions, but rather, a general idea of how you think one should go about deciding those issues to come up with the best answers in some given situation.
now we need to go OSS in diesel cars
Code was ugly and hideous
Someone forked the tree a while back, and now we had to support 2 seperate source trees (this one was really annoying, because if fix a bug and change some behaviour in one side, since both sides had to be able to talk to each other, you'd have to introduce a corrosponding "fix" in the other side.
The core architecture was woefully outdated and ineficient
Speed was an issue, and the current architecture was limiting, and the code was optimized about as well as it could be (this was also part of the uglyness problem)
we were spending about 70% of our time fixing bugs in the old code, it took this much time because they were little and stupid and with the code in the state that it was in it took forever to trace things down.
Well, we had been wanting (desperately) to redesign from the ground up for a while, but the powers-that-be wouldn't give us the time, until one customer asked for a feature, marketing promised that they'd get it, and we said "Know what? We can't do that. Not with the current infrastructure." So the powers-that-be said "do what needs to be done!" and we said "yipee!".
Moral?
How much extra time is spent during debugging code that is due to the current state of the code?
How does the core architecture compare to what you will need in the future? Will it support everything?
How efficient is the old codebase, and how efficient does it need to be?
Can you get the time required to do so? This is a one-step-backwards before two-steps-ahead thing.
Do you trust the people that you are working with enough to be able to competantly and efficiently create the new architecture? This is a serious question, because I have worked with some people that are good if you tell them exactly what to do, but I wouldn't trust them to recreate everything.
Will you be required to keep the old codebase going while you are in the process of converting? If old customers need bug fixes, you might be forced to keep the old sourcebase around for a while.
Can you make the new design backwards compatible? If not, can you provide a wrapper of sorts, or some easy way to convert your customers from your old version to the new one?
If you are going to be redesigning the user interface or the API at all, then you must also think about the impact on your customers.
Just some food for thought.
If God gave us curiosity
A feature that exists in the major UNIX systems, but is not part of the standards, will majorly improve the performance of my project, and make it a lot easier to code up. Should I use it or not?
Of course the question is vague. I didn't state which systems and for a reason: I don't want to focus on the specifics (although I do have a specific case in mind), but rather, I want to focus on the general principle with this issue. Just how far should I go to make sure my program works on every damned UNIX out there? How much is important?
now we need to go OSS in diesel cars
OO design is a pretty vague term, as it means different things to different people. However one concept that is proven to work for backwards compatibility is component design as found in COM and other component based frameworks.
For those who are not familiar with the concept you don't deal with objects per say, instead you deal with interfaces to implementations of objects.
For example
ICat *pCat = NULL;
pCat = MakeCat(IID_ICAT);
pCat->Meow();
delete pCat
COM does it a little differently but the basics are there. You request an implementation to an interface, not the object itself. The way this works for different versions is that instead of IID_ICAT you can have IID_ICAT2 and ICat2 interfaces without having to break your old ICat clients. The implementation could even share much of the old code.
For example:
ICat2 *pCat = NULL;
pCat = MakeCat(IID_ICAT2);
pCat->MeowLoudly();
delete pCat
Admittedly it's not the most elegant design, but it works in the sense that you're not breaking old clients and still have room to support new interfaces.
-Jon
this is my sig.
"Programming is like sex. Make one mistake and support it for the rest of your life."
This is a confused discussion. A lot of people are mixing up "backward compatability for users" with "not making significant changes to the code base". The two are largely unrelated, except that screwing up the latter will also mess up the former.
What your user sees is, and always must be, decided by your requirements spec, not programmer whim. The only people who can get away with doing otherwise are those developing for their own interest (hobbyists, people involved in open source development, etc).
To put it bluntly, blanket statements like "meet 90% of your users' BC needs" are garbage. In many markets (notably the bespoke application development market) if you drop 10% of your users in the brown stuff, your contract is over, and your reputation may be damaged beyond repair. Look at MS; years later and in the face of much better libraries, MFC still survives, because people are still using it (including MS) and they daren't break it.
This is far removed from rewriting things significantly as far as the code goes, which is where things like the standards mentioned come into things. I'm sorry, Cliff, but you don't do this "when you can get away with it" if you're any good.
Every time you rewrite any major piece of code, "just to tidy things up", you run the risk of introducing bugs. You need to be pretty sure that your rewrite is
- necessary to meet your requirements, or
- fixing more than it breaks and not breaking anything unacceptable
or, preferably, both.If the rewrite is justified using these objective criteria, then you do it. When you do, you try to minimise the number of changes you make, and to keep the overall design clean. You retest everything that might conceivably have been broken, and you look very carefully at anything that didn't work -- it's quite possible that the people who originally wrote this code months or years ago made assumptions they forgot to document and you've broken them. Finally, if and only if your rewrite is performing acceptably and all the tests are done, you decide to keep it. If not, you throw it away and start rewriting again.
And for the record, yes, I spent most of last week rewriting a major section of our application, as a result of a code review with another team member. We kept the overall design, tweaked a few things within it, and rewrote most of the implementation. Now we need to retest it all, update all the docs, etc. This little exercise has cost our company thousands of pounds, but in this particular case it's justified by a needed performance increase and the significant reduction in bug count. But you can bet we thought very hard about it before we touched the keyboard.
If you disagree, post your argument. (-1, Overrated) isn't your personal censorship tool for views you don't like.
A key to providing backward compatibility is "design intent"; i.e., closely examine the backwards compatibility issue when you are first thinking about creating a piece of software. Internal data structures, external file formats, APIs, etc. are all influenced by the design constraints placed upon a project. If one of those constraints is backward compatibility then these structures will all be built differently than in the case where no backward compatibility is ever required.
.MIF files influences coding decisions.
FrameMaker is a great example of an application that appears to have been architected from day one to provide backward compatibility: every version of FrameMaker imports and exports Maker Interchane Files (.MIF files) and so it is trivial to move files between releases of the application. While I'm sure this causes the developers some headaches from time to time, I know from personal experience that a constant anchor point like
Having done work on an ASCII interchange mechanism for a multiplatform application, I can be fairly certain that the FrameMaker decision isn't very difficult to implement: each release of the application has a pair of small functions, one to walk the internal data structure and emit the ASCII interchange format, and another that parses the ASCII interchange file and produces an internal data structure.
When we designed our application, the ASCII interchange functionality was deemed important; this influenced the internal data structures, which in turn influenced the binary data files. If we had tried to bolt backward compatibility on at a later date (i.e., in version 2.0) it could have been a lot of work; whereas, building it in from day one didn't cause any extra work.
Conscious design intent is the key to making backward compatibility a non-issue.
Your web server should support all versions of HTTP. (You meant HTML?)
POP3 and IMAP4 are not 'new versions' of each other... neither is outdated. one is not a replacement for the other. Needs dictated solely by users.
Your web site should require no more funcionality than needed ot operate the way you want it to. That's just good programming. Don't use cookies or javascript or java if you don't need to.
You can stick to Unicode, because ISO-8859 maps into it properly.
A properly designed and maintained OO system will alleviate some of the backward compatibility issues. The thing is, most OO systems are not well maintained, even if they were reaonsably well designed originally. Most developers, in my experience, are far too reluctant to refactor properly.
This generally boils down to one repeated mistake. When changed requirements suggest a change in the responsibilities of your classes and/or the relationships between them, too many developers try to change the implementation or interface of classes in isolation instead.
Unfortunately, in the long run, no amount of interface hackery can make up for an inappropriate distribution of responsibilities. It's just fudging the design, and the one thing you don't want to do with an OO design is mess it up with fudges.
The reason you get to this "it's got no chance, let's start from scratch" syndrome is that too many fudges were allowed to accumulate. Instead of refactoring as they go along, keeping a design with clear responsibilities and relationships, programmers try to shove things in where they don't really belong. Once or twice, you can get away with it, but it doesn't scale, and ultimately leads to an unmaintainable mess.
Of course, the problem is that adopting the "I'll just fix it here" mentality is, in the immediate timeframe, quick and cheap. In the long run, it's much more expensive -- a rewrite from the top will do horrible things to your budget -- but people rarely consider that far ahead.
Such is the curse of the OO paradigm, where the benefits are only to be had by those who think: most people don't.
If you disagree, post your argument. (-1, Overrated) isn't your personal censorship tool for views you don't like.
Old code is hard to read - even if it is your code - because you lack the overall 'grasp' of what the code is doing - which the developer had had when it was written. Old code becomes just lines of code instead of part of an intelligent structure; you can see the 'trees' but the 'forest' has been lost.
This means that the problem with old code is that YOU don't know what the developers were doing; not that THE DEVELOPERS didn't know what they were doing!
It is a very easy mistake to confuse those two types of ignorance. Add in a little 'it can't be me that is wrong' attitude and the name of the game becomes a contemptuous 'out with the old in with the new.'
The truth is that there are differences in skill levels of programmers - old code written by good programmers is a lot better than new code written by poorer quality programers. If you didn't know that programmers differ in skill level it is conclusive proof that you are not a good programmer; to a bad programmer all code looks the same - that is why a bad programmer is a bad programmer
The API has barely changed in the last 25 years. A friend of mine has an application that's been running unchanged since the 1970s. It has contined to work across generations of hardware. And it's in assembler.
They had the advantage that their OS was decades ahead of its time. UNIVAC had symmetrical multiprocessing with threads in a protected mode environment thirty years ago.. And threads were designed in, not bolted on like UNIX.