Code reuse is hard because software doesn’t stand still.
If I could write a program once and never care to change it then code reuse would be easy. But that doesn’t happen. As far as I can tell, it doesn’t ever happen. Even trivial-seeming things like multiplication are refined periodically, and any more complicated system changes often. There is a hierarchy of sorts in how involved changes might be.
The simplest change is purely behind-the-scenes: Behind-the-scenes: replacing a screw on the light-switch panel. you tell me the same information you always told me and I produce the same answer I always produced, but the way I produce it is not the same as it was. Maybe it’s a little faster, maybe it takes advantage of new hardware, etc. For the most part, these changes can be ignored or adopted without any downsides. The exception comes when the program depends on the “non-functional” elements of the code, such as the time it takes to run or the way it handles memory. Such non-functional dependencies are rarely present by design.
Next up is the new feature increase. Feature increase: adding a second light-switch to the panel. You can still use the new version the same way you used the old version, but there are also new ways to use it as well. Like the “invisible” change noted above, these are usually safely ignored or adopted without unwanted side effects.
A bit worse than feature increase is change in the behavior for “undefined” inputs. Changing undefined: “Now in the half-up half-down state the light is on instead of off”. For example, if I have some division code that I state is “undefined” for 1 ÷ 0, the code still has to do something for those inputs and a user can discover what it does by trying it out. A later release might change the result of 1 ÷ 0 without changing any of the “official” behavior of the code. If everyone used the method “correctly” (meaning they never tried to divide by 0) then the update’s OK, but otherwise it can cause come users grief.
Next up is the bug fix. Bug fix: putting switches formerly in the “wrong” place into the “right” place instead. This actually changes the outputs that appear for a particular input, but does so in a way that “fixes” the code to perform the advertised task. These can cause two kinds of problems. On the one hand, systems that assumed the code worked as advertised now need to be updated to use the fixed version. On the other hand, it is common to find systems that recognized and worked around, or even depended upon, the “faulty” behavior of the old code. Updating these can break them.
Similar to bug fixes are enhancements. Enhancements: Now the switch controls all the lights, not just the one. If my old square root function said the root of 2 was 1.41421 and my new version says it’s 1.41421356, the new one is closer to the mathematically correct square root. Like bug fixes, this change will require many uses to swap over for best results and will cause a few users to stop working properly.
Finally, the biggest kind of change impacts conceptual semantics or the programming interface. Interface change: Switches were causing problems. We’ll use pull-chains instead now. We might decide we need to have a different set of inputs, or that the old menu layout was getting messy and we needed to reorganize, etc. For all the good that such redesigns can have, they also mean that large portions of code that used the old design will need to be restructured to make use of the new design instead.
Every piece of code I know changes. Most change in all of the various ways I’ve listed from time to time. Handling this continually evolving landscape of code is the main challenge in code reuse.