Extensibility
When modifying a system (such as adding features), one should be concerned about the ability of the system to be modified. This implies the presence of two extensibility aspects: architectural and quality-centered. I believe it is essential to understand the difference between those two aspects, especially in order to determine what are the most appropriate solution.
Architectural extensibility
The issue often encountered when developing a system larger than two men-months is that the original architecture is unable to encompass some new feature. It just wasn't done for that, and now that the feature exists in the backlog, developers are wondering how could they bend the existent product to be able to add the requested feature.
The immediate solution here is high-level refactoring. By high-level refactoring, I mean all those changes which deal precisely with either architecture—if the team is able to change the architecture, or high-level design. By comparison, low-level refactoring is about to rename methods or transform switch block into inheritance. Note that both are crucial, so high-level refactoring is not more important than low-level one.
Also worth noting, two techniques help to either avoid issues with such extensibility, or to reduce the pain of doing major changes in architecture:
Proper architecture helps a lot. Not thinking about architecture and let the product grow organically may be a fast solution, but usually creates painful situations where large chunks of the code base should be modified in order to accommodate new features. If the architecture is thought thoroughly from the beginning, it doesn't mean new features would integrate seamlessly (although much more of them would), but nevertheless, architectural changes would be less painful to do, with fewer side effects.
The lack of cascading changes (or fewer of them) is by the way crucial here: when adding a feature, you want to avoid cascading changes at all costs, since those ones are not only particularly expensive, but also can kick out the team who will spend more time adapting their work to the changes than to do something useful.
Continuous refactoring, especially at high level, makes it possible to keep the system ready for further changes, as well as ensuring that the parts of the system have enough isolation from each other. Isolation keeps cascading changes low too.
Quality-centered extensibility
This is the issue which is encountered more when extending a legacy code base. For example, a developer is asked to add a simple feature to an application, but spends weeks implementing it, because a slight change to one part of the code base breaks something in a different part which looks completely unrelated. Here, the problem is not the architecture—it can be totally adapted to the new change, but rather the quality of the code itself.
The issues can be multiple: the lack of proper testing with enough code coverage, the absence of common style, the lack of documentation, etc.
The immediate solution is low-level refactoring, as well as dealing with the real issue before adding the feature.
Low-level refactoring can be very unobtrusive and local, but can also cascade quite fast. Say we want to replace those ugly
if/elseif/elseif/elseif/else
blocks by inheritance, but to do that, we should first isolate the common logic, given that thoseif/.../else
blocks are replicated in four files. For that, we need to create a base class, but this would mean that we should modify the class hierarchy, since one of those four classes inherits from a class which is completely unrelated. A week later, we are still refactoring.Dealing with the real issue can be sometimes easy, but usually is hard as hell. For example, if the application lacks testing, the primary task before adding features is to add tests, but to do it, one should make the code possible to test. This is mostly chicken or the egg problem: before splitting a 5 000 LOC method, you should unit-test it to avoid regressions, but in order to unit-test it, you should split it first.
Again, two long-term techniques to avoid such issues:
Continuous refactoring, especially at low level. I don't think this needs to be explained any further.
Dealing with technical debt by enforcing common style, having tests, documentation, etc. No need to detail this either.