C++ – How to effectively manage long-term feature development and short-term bug fixing at the same time

ccontinuous integrationdesignrelease-management

We have a huge C, and C++ code base. There is a plan to add a new feature into the existing system, but, this should happen only after 3 to 4 months.

So, we are looking for best possible options.

  1. Branching off is one option, but, this involves porting the bug fixes to the feature branch from the trunk.
  2. Staying on the trunk itself, have ifdef to differentiate the new feature compilation
  3. Split the source/header files that involve the new feature, edit the makefiles to pick the right file.

I understand this question is pretty vague, and only people working on it can well decide the way forward. But, I'm looking for generalized opinions on a case like this.

Best Answer

I'll try to post some helpful hints, but with all due respect, this is Release Management 101, and if you really have a huge code base and a need for parallel work streams in the organization, you would do well to either read a book on it or hire somebody with more experience in this area.

Assuming this situation:

  • The business needs new functionality that will take a long time to complete.
  • The business needs various bugs fixed now.
  • You have multiple developers who can work on different areas simultaneously.
  • You cannot defer the development of the feature until after the bugs are fixed.

Make absolutely sure that point #4 is true, because if you can invalidate that assumption, your job suddenly gets a whole lot easier. I can't tell you much time I've saved in the past by simply not doing bug-fixing and feature-development at the same time. That said, it is sometimes an unfortunate necessity.

Formally, your options are:

  1. Feature Branches: You create a branch where all of the new feature development will take place. Not recommended because they inherently break Continuous Integration. You can of course clone your "CI build" for the feature branch, but that's not actually CI because the code hasn't actually been integrated, it's just being built and maybe tested in isolation.

  2. Feature Toggles: This is typically not done with an ifdef but rather a configuration option. The ability to set this at runtime, not build time, is important because it makes testing and deployment a lot easier. Compile-time toggles require you to maintain two separate builds and two separate artifacts, but you can only promote one, a situation which tends to introduce ambiguity and process short-cutting.

    Runtime feature toggles are partially recommended with some caveats:

    • It is important to minimize the surface area of the toggle. Ideally you would want the check to happen in only one place. Feature toggles that get too big for their britches are evil for the same reason that Singletons and other global state are. Try to check it just once, at application startup.

    • Too many feature toggles can become a spaghetti mess of their own over time, essentially creating a program-within-a-program. Feature Z depends on Feature Y which depends on Feature X but not Feature Q which is deprecated and breaks feature AB... etc. So in addition to minimizing the scope of individual toggles, you need to take steps to minimize the number of toggles and properly manage the dependencies between them. Even if there are no dependencies, you still need to run all end-to-end tests with each state that could plausibly be used in production, which quickly becomes a combinatorial explosion for the testers and release managers.

  3. Parameterized Build or simply Multiple Targets: This is an incredibly useful tool/technique but I've never seen it used to build different feature sets and can't recommend that as an option here. You would normally use it to build for different platforms and/or perform certain one-off-tasks like running integration tests, generating documentation, etc. Using it as a poor-man's feature toggle has all of the potential drawbacks of a feature toggle and hides all of the complexity in your build scripts, which are generally the least flexible and most difficult to test of all your code.

  4. Feature Abstraction, AKA Branch By Abstraction (a dangerous misnomer since it does not involve any actual branching): This means you encapsulate all of the interesting functionality of the new feature in a single class and use refactoring techniques (particularly Extract Interface) to encapsulate all of the existing functionality in the same interface. Then when you're ready to launch the feature, you just change the implementation.

    This combines the advantages of (a) being in your trunk or mainline, (b) being fully covered by unit tests and possibly integration tests, and (c) not introducing any new global state to the application. This is much easier to do if your application uses dependency injection but not impossible if it doesn't. This is always the best choice if and when you are able to do it - it introduces the least overhead and the lowest risk, and can be combined very easily with options 2 or 3 above if you need to make the feature available on-demand to your testers.

    Note that when using this option, once the feature has actually been launched, you should reevaluate whether the abstraction is still necessary and remove it if the overhead of maintaining it outweighs whatever benefits you're still getting.

You should choose options 4, 2, and 1 in descending order of preference. This is coming from someone with a whole lot of experience with all of the above options.

Final note: Fixing a bug or adding a new feature is not refactoring. Refactor does not mean "change", it refers to preserving the existing functionality exactly during some code change. So please don't use it to refer to your situation; what you're doing is the opposite of refactoring.

Related Topic