The other answers give good suggestions for architecture and project layout. I wanted to address this specific point:
Code contains quite a lot of conditional compilation to separate features.
I recently ran into this on a project I'm working on and improved it using the following techniques:
Use Constants
Some of our code has been quickly modified with directives and now looks like this:
#if <SOMETHING>
callSomeFunction(aConst);
#else
callSomeFunction(bConst);
#endif
This can by improved by making a single constant that’s defined differently for each path of the #if and only calling the function once, like this:
// At top of file
#if <SOMETHING>
const int kImportantVal = aConst;
#else
const int kImportantVal = bConst;
#endif
…
// In the actual code
callSomeFunction(kImportantVal);
Combining Sections
Often in our haste we simply sprinkle #if directives throughout the code, leaving functions looking like this:
#if SOMETHING
callA();
#else
callB();
#endif
callC();
#if SOMETHING
callD();
#else
callE();
#endif
if callD() and callE() are not dependent on the result of callC(), this can be rewritten as:
#if SOMETHING
callA();
callD();
#else
callB();
callE();
#endif
callC();
Refactored Functions
Functionality can be moved into functions which are grouped together within a single #if or other directive.
If we look at the above example, another way to do it would be the following:
#if SOMETHING
void callA()
{
// ...whatever callA() above does;
}
void callD()
{
// ...whatever callD() above does;
}
#else
void callA()
{
// ...whatever callB() above does;
}
void callD()
{
// ...whatever callE() above does;
}
#endif
// ...
callA();
callC();
callD();
Use Polymorphism
In object-oriented languages like C++ and Objective-C, it’s fairly simple to create an interface that is implemented by different objects in different builds. For example, if you had this code:
@interface SomeInterface
{
}
- (void)methodA;
@end
@implementation SomeInterface
-(void)methodA
{
#if SOMETHING
doX();
#else
doY();
#endif
}
@end
You would instead make 2 classes - one for each branch of the #if SOMETHING. They might look like this:
@interface SomeInterfaceX : SomeInterface
{
}
@end
@implementation SomeInterfaceX
- (void)methodA
{
doX();
}
@end
and
@interface SomeInterfaceY : SomeInterface
{
}
@end
@implementation SomeInterfaceY
- (void)methodA
{
doY();
}
@end
You can do something similar in C++ with derived classes.
Categories
In Objective-C we have the option of adding a category only in the targets that need it. For example, in SomeClass we want one set of functionality in 2 targets, but there's some additional functionality that should only exist in 1 of the targets. As such, we take the methods that are only implemented in the 1 target and put them in a separate file containing a category that implements just those methods. That file is only compiled in the 1 target and not the other.
So you'd have something like:
@interface SomeClass {
// ... class definition that is common to both targets
}
@end
@implementation SomeClass
// ... class implementation that is common to both targets
@end
Then in a separate file
@interface SomeClass (Additions)
// ... additional methods for target that needs them
@end
@implementation SomeClass (Additions)
// ... implementation of addition methods for target that needs them
@end