I inherited a large code base (iOS / Objective-C) that is used to build multiple mobile apps. The apps have similar, but not identical UI.
The apps share a lot of common features, but there are features only found in one app. Some features can be later added to other apps.
Apps have relatively independent life cycle. Releases are not simultaneous. It is common to have two or three versions of each app in development at the same time: hotfix for the current published version, next minor version, next major version. Features can move between versions. I.e features can be postponed to the later version or moved to the earlier one.
For now there is a separate scheme for each app. Code contains quite a lot of conditional compilation to separate features. Code changes often transferred between branches (develop -> master, develop -> releaseXYZ) using git cherry-pick
and git revert
commands.
How can I reduce the pain of maintaining all this? I would really like to simplify feature set management between different versions of the same app and between different apps.
EDIT: Thank you for your answers so far. I understand, that I need to split code base into some kind of modules. I would really appreciate if you tell me more how I can manage these modules efficiently / less painful.
4 Answers 4
I don't know how modular the code is, but in order to handle the complexity of all of this it might be useful to create API layers. The core library has the features/functions in all apps, with feature packs that can be included in each of the final apps. Essentially the dependency graph is intended to be a directed acyclic graph (DAG).
After organizing your features into modules, you may want to include extension points so that applications can inject functionality to take care of the things in the conditional compilation. That way your code won't break just because you make a change and then forget about the part in the condition.
In each specific app, you do all your customization and wiring together.
If I were you, I'd factor out the common code into separate Xcode projects and compile them as a Framework or library. Then you can separate the different UIs into their own projects as well and include the common code in each one.
Since you also seem to have complexity issues with your SCM situation, I would consider separating all of the applications into their own repositories and the common code into its own as well. You can use something like Cocoapods or Carthage to include the common Framework into each app.
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
You could:
- Create one GIT repository containing features and maybe dummy screens that use those features;
- Each app will have a separate GIT repository, forked from the repo from Step 1 (since each app evolves differently, share common features, and for other features it slightly modifies them);
- Whenever you see appropriate, during each apps maintenance, you could send feature's commits to the base repository, in order to be available to the other apps. The base repo is just to aid you in the maintenance of common features.
Explore related questions
See similar questions with these tags.