Everyone expects software to be soft. To be easily changeable. At least more easily changeable than hardware. But that is not always the case, sometimes it is easier to redesign the hardware than the software, but why and what do we mean when we say it is difficult to change software?
Before the product has shipped, it should be easy to change both the hardware and the software as necessary to achieve the design. After the product has shipped, the hardware is fixed and you can only change the software. But there are often difficulties in changing software that mean it isn't as flexible as it might appear.
What do we mean when we say it is difficult to change software?
Let's start with what we don't mean.
We don't mean that it's too much typing, or too many files, or we can't find all the source code.
We don't mean that we have no ideas about how to produce the functionality required.
We don't even necessarily mean that it will take a long time.
Ok then, what do we mean?
Often, the difficulty comes from the intertwined nature of software. Each part has some dependency on another part. In a good design, there will be defined interfaces between the parts so that each part could, theoretically, be changed independently. But even then, this often only applies to "functionality" only - and software has many "non functional" requirements.
Non functional requirements are requirements that don't change the values of the output but are nevertheless important. Examples are timing requirements, especially in real-time control systems but even in user interfaces where responsiveness changes the perception of the system; safety requirements where the needs to be guarantees of behaviour under all conditions; quality requirement where the code might run but be impenetrable for future developers or be badly structured for testing; security requirements where the code might need to run in constant time to avoid leaking information about keys.
Even when the parts are designed with defined interfaces, some types of change will require that several parts are changed and that the interface changes. This will break the functionality of dependent code. Then you can start considering whether you need to introduce this change with a backwards compatibility option, and if that should be the default. These design decisions are part of what it means to be difficult to change software - in some sense the difficulty is not the immediate difficulty but setting up a problem for later (also called technical debt.)
New code is less well tested than old code. Old code has been used on different machines, platforms, environments, inputs. Do we always have a complete description of the requirements and tests that should be run? If we are rewriting to get rid of old code with known bugs, will the new code be necessarily better?
New code might bring in new libraries, or new versions of previously used libraries. You'd hope that would be better, but it is not unknown for the new version to have bugs that were not in the older versions. In either case, just because it is not your code doesn't mean that it doesn't need testing.
In embedded environments, code is often subject to space and timing constraints. There are techniques for optimising the amount of code space (Flash/ROM) used, but these tend to be less optimal in RAM usage. Conversely, low RAM usage techniques tend to use more code space and run slower. So when rewriting code, can you be sure you are making improvements or are you just moving the optimisation slider between Flash and RAM?
Once the algorithm has been chosen, it is much harder to reduce RAM requirements than Flash requirements. If your rewrite has increased the RAM demand, you can get in trouble in limited RAM environments and face a technically difficult problem of how to maintain functionality in the rest of the code.
Simple renaming isn't without problems
Even a rename change can cause difficulties later. If the variable or function is widely used, that rename shows up in differencing tool views every time you compare the current code to the code prior to rename, which can obscure the semantic change you are looking for - the significant, meaningful change in logic.
If you are using a Blame tool to look back over all the changes since the start, multiple renaming sessions can really get in your way of being able to see what really changed.
Can we rebuild it?
One final cause of difficulty in changing software - I've encountered a product where the functionality couldn't be changed by the manufacturer because the software was written by a contractor and the company no longer has the source code. I've also heard of cases where the source code was available but the build tools were obsolete and could not be installed on a modern computer.
It's not just the code
Code is never the only element in the product, so when it is changed, there are corresponding changes in the documentation, help files, instructions, videos, tutorials, etc that need to be kept in sync. Then you have the version problem, where you need to use the version of the documents that refers to the latest code.
But sure, code is always just plain text in a file, and can be changed at any time by anyone with a text editor.