3

In the .NET code base of a product, there are #if pre-processor statements to define whether certain functionality should be available. Like so:

shared logic

#if version1
    some logic
#endif
#if version2
    some other logic
#endif

Personally I twitch at this structuring. I find it harder to read, and it makes me nervous for some reason.
I was told that the reasoning behind having it like this is that there are too many changes that happen in all of the versions. If the versions were stored in separate repositories or some other method, then any future changes would have to be implemented several times over.

I find the "too much to change" argument a bit vague, but I'll admit my experience with this kind of design is limited.

Is this really the best practice or the most common approach when it comes to handling multiple product versions with differing functionality? Is there a better alternative?

migrated from stackoverflow.com Nov 14 '14 at 17:12

This question came from our site for professional and enthusiast programmers.

  • 2
    Branching/merging? – ken2k Nov 14 '14 at 13:50
  • yeah that's the immediate instinct, it is a platform independent problem after all – Phil Nov 14 '14 at 13:53
  • 2
    Why not use Unity (or other IOC) to dynamically load the correct classes for the configuration you need. How you share code (to lighten maintenance burden) between those classes is another matter, but late bound code loading is what IOC does, and you can swtich at runtime between the versions by giving the container a configuration name. So you can have an IOC config called "America", and one called "Europe" and load the correct classes through that. – PhillipH Nov 14 '14 at 14:26
4

I'd avoid having separate code bases. All that branching and merging creates lots of unnecessary maintenance work. Even worse when they get out of sync it becomes hard to determine which differences were intentional and which by mistake.

I prefer replacing #if preprocessor directives by ordinary if statements, that way all variants of the code need to compile and refactoring tools see all the code.

Beyond this simple general change you should look into refactoring the code, but how to do that depends on the nature of the differences.

Some suggestions:

  • Don't mix different variants within a single method. For example you could replace your code with:

    SharedPart();
    if(cond1)
        Variant1();
    if(cond2)
        Variant2();
    
  • Use delegates, interfaces or virtual methods to dispatch to the different variants instead of if statements.

  • Inject* an interface containing the configuration instead of using global state. This allows you to write unit tests for all variants without recompiling or restarting.
  • For features that are either enabled or disabled, I'd create a Feature enum, together with an injected* interface that determines if the feature is available. The interface could look something like:

    interface IGateKeeper
    {
        bool IsEnabled(Feature feature);
        void Require(Feature feature);
    }
    

    where Require checks if the feature is enabled and throws a meaningful exception otherwise.


* If you're not familiar with dependency injection (DI) and inversion of control (IoC) learn about them.

3

I've been in this boat. It's not fun. It can't always be a matter of using a runtime test:

if (IsConfigOne)
    ConfigOneImpl();
if (IsConfigTwo)
    ConfigTwoImpl();

Because of dependencies that are mutually exclusive (ie, I can't reference ConfigOneImpl() when I'm in ConfigTwo because that would reference assemblies I don't have in ConfigTwo...).

In general, you're better off building an abstract class that implements most of the functionality and concrete classes that are made for each individual implementation. In your case, you might have something like this:

public abstract class SomeThing {
    public void Logic() {
        CommonLogic();
        SpecificLogic();
    }
    protected void CommonLogic() { /* ... */ }
    protected abstract void SpecificLogic();
}

public class SomeThingImplConfig1 : SomeThing {
    protected void SpecificLogic() { /* ... */ }
}

I've encountered other cases where this doesn't work and was in a position where I couldn't adapt or modify things because either didn't own those classes or was bound by other constraints, I tried to isolate specific code in partial classes. For example, I had to build 3 different configurations of a set of classes that could work with unmanaged support routines, equivalents in pure managed code and equivalents for Silverlight, so I was able to isolate those by using 4 partials, one for all the shared code and interfaces, and 1 for each of the specific configurations.

Finally, there were some classes not available in Silverlight (System.Drawing, IIRC), but there were equivalents in Silverlight that had members with the same names (but were not compatible through an interface). Under those circumstances, I could use a C# type alias to make the Silverlight objects follow the System.Drawing equivalents:

#if SILVERLIGHT_CONFIG
using Rectangle = System.Windows.Rect;
#endif
2

It depends if your versioning problem is a compile-time or run-time problem.

For instance, you might want to be able to support a legacy message format in your API. That's a run-time problem because both code paths are still available within your application and can be called arbitrarily. It requires a run-time solution: DI, ifs, delegates, etc. They are really well covered by CodesInChaos' answer.

If you only want to build an older version of your code for legacy purpose, then the solution is clearly a compile-time solution. Even in a very large code base, I would still prefer to depend on a source control system for this and not on pre-processor conditions.

I would choose this path because, to my experience, the reasoning you mentioned is flawed. It doesn't matter how you store your many code-bases. Whether they are in different files or the same one, you still have multiple versions of the same code to maintain. Merging should not be an issue because changes to incompatible logic will never reflect one to one in a different code-base. It needs to be implemented independently. If it does, then your logic is not incompatible which indicates an architecture/factoring problem in your code. Aka, you are not reusing everything that you can.

On the other side, dealing with twice as much code in a function, separated through pre-processor conditions that have no concept of scope, quickly turns code into a huge and unreadable mess. You can end up having conditions that crosses scopes and the only way to understand what is really going on is by doing the pre-processor's job yourself. After-all, there is a reason why C++ developer considers the pre-processor like a demon that should have never existed. The only solution to this problem is the use of run-time if conditions to force the use of scoped logic. I would still avoid going this way because they can have a strong negative impact on performances if they are used in a critical path.

In the end, it all boils down to using the right tool for the job and an automatic text replacement tool is not the right tool to handle code versioning.

  • 1
    Note that an if statement doesn't necessarily imply a runtime condition. If the condition is a compile time constant compilers will usually omit the unreachable code (but depending on the language they might produce a warning). – CodesInChaos Nov 15 '14 at 15:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.