Building large, dependable applications is no longer limited to large, cumbersome companies. Today, smaller, more agile companies are in the mix too, thanks to those fine folks spending their hours in open-source development.

Where developers once spent time bug-squashing redundant issues, they’re now hacking open-source software into new or existing applications through Software Development Kits (SDKs). The catch? SDKs are only useful if other developers can modify the specific configurations they need.

Development is evolving significantly due to rises in availability of open source software. Where developers once wrote nearly every line of an application, they’re now using SDKs and open source frameworks in place of custom logic. This might seem a bit like cheating, but it has been great for the industry. The next move? Making open-source software flexible and agile enough for developers to use it where and when they want it.

A couple of things can start to happen if a developer is unable to make simple modifications to get your SDKs running in their custom applications:

1.     Developers will abandon your SDK and rapidly spread bad gossip about its functionality. Such is the social age.

2.     At the height of their frustration, 3rd party development teams will contact you for help. Can’t help them? See item 1. Then add extra bad gossip about your development capabilities.

3.     Arguably the worst scenario, developers will successfully Frankenstein your SDK into their application in such a monstrous manner that it compromises the stability of the entire app.

Opinions spread quickly, so providing developers with agile & flexible configuration options in your SDK will help avoid these situations.

If an SDK is small and serves a distinct purpose, then offering small configuration values can suffice. If an SDK performs more complex tasks, it is better to give developers a hook to “plug in” their own logic.

Coarse Grained Configuration (CGC) allows developers consuming your SDK to configure driving implementation classes in the same manner as dependency injection. The configuration of an SDK can drive more than small configurations like HTTP timeout, cache size, etc. In addition to the standard configuration values, entire implementations can be configured.

Providing a ConfigurationManager that loads the SDK’s default configurations as well as loading and merging any user-defined configurations, is a great start. The ConfigurationManager created is important as it helps provide the developer an initial turnkey solution.

Here is an example of a ConfigurationManager, written in Objective-C, which loads the SDK configurations from a plist bundled within, and then loads an optional plist file from the consuming application path. The configurations loaded from the application consuming an SDK should not be required to define all possible configuration values, but rather tune only the intended individual values.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#import "ConfigurationManager.h"
 
@interface ConfigurationManager ()
 
@property (strong, nonatomic) NSDictionary *configs;
 
@end
 
@implementation ConfigurationManager
 
NSString *DEFAULTS_FILE_NAME = @"SDKDefaults";
NSString *CONFIG_FILE_EXT = @"plist";
NSString *USER_FILE_NAME = @"SDK";
 
- (id) init {
    self = [super init];
    if (self) {
        NSString *defaultsPath = [[NSBundle mainBundle] pathForResource:DEFAULTS_FILE_NAME ofType:CONFIG_FILE_EXT];
        if (defaultsPath) {
            self.configs = [[NSDictionary alloc] initWithContentsOfFile:defaultsPath];
            NSLog(@"%lu SDK defaults loaded from %@.%@ file at path %@", (unsigned long)[self.configs count], DEFAULTS_FILE_NAME, CONFIG_FILE_EXT, defaultsPath);
        } else {
            [NSException raise:@"Invalid SDK configuration" format:@"Unable to locate required default configuration file!"];
        }
 
        //Looks for a SDK user defined plist file as well and merges those into the
        //existing configurations with the SDK defined configs taking precedence
        NSString *sdkPath = [[NSBundle mainBundle] pathForResource:USER_FILE_NAME ofType:@"plist"];
        if (sdkPath) {
            NSDictionary *userConfigs = [[NSDictionary alloc] initWithContentsOfFile:sdkPath];
            if (userConfigs) {
                self.configs = [self mergeDefaultConfiguration:self.configs withUserConfiguration:userConfigs];
            }
        } else {
            NSLog(@"No user defined %@.%@ file found in main bundle defaults will be used.", USER_FILE_NAME, CONFIG_FILE_EXT);
        }
    }
    return self;
}
 
+ (instancetype)sharedInstance {
    static dispatch_once_t p = 0;
 
    __strong static ConfigurationManager *_configManager = nil;
 
    dispatch_once(&p, ^{
        _configManager = [[self alloc] init];
    });
 
    return _configManager;
}
 
- (NSDictionary *)mergeDefaultConfiguration:(NSDictionary *)defaultConfig
withUserConfiguration:(NSDictionary *)userConfigs {
 
    NSMutableDictionary *mergedConfigs;
 
    if (defaultConfig) {
        mergedConfigs = [defaultConfig mutableCopy];
        [mergedConfigs addEntriesFromDictionary:userConfigs];
    } else {
        mergedConfigs = [userConfigs mutableCopy];
    }
 
    return mergedConfigs;
}

Now that the ConfigurationManager is written, it is easy to have a factory/manager/service-implementation load and create the developer-defined implementations that adhere to your protocols. This assumes, of course, you are using protocols…If not? Shame on you!

Sample Objective-C protocol that the user defined implementation is expected to adhere to:

1
2
3
4
5
6
7
@protocol PluginProtocol <NSObject>
 
@required
-(BOOL)canYouProcessMe:(NSDictionary*)dictionary;
-(NSDictionary*)process:(NSDictionary*)dictionary;
 
@end

Then the developer consuming the SDK can write their implementation logic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@interface AugmentationPlugin : NSObject <PluginProtocol>
 
@end
 
@implementation AugmentationPlugin
 
-(id)init {
    self = [super init];
    if (self) {
    }
    return self;
}
 
/** Developer controls if their implementation is invoked or not. **/
-(BOOL)canYouProcessMe:(NSDictionary *)dictionary {
    if ([dictionary objectForKey@"SDKProcess"]) {
        return YES;
    } else {
        return NO;
    }
}
 
/** Implementation processes and returns the result. **/
-(NSDictionary*)process:(NSDictionary*)dictionary {
    NSMutableDictionary *mut = [dictionary mutableCopy];
    [mut setValue:@"NO" forKey:@"SDKProcess"];
    return mut;
}
 
@end

The SDK manager is then responsible for loading the implementations and injecting the user defined implementation. The sample SDKManager below shows how a manager in the SDK would ask the ConfigurationManager for the list of defined classes for processing in a certain method. Then create those classes – whether internal to the SDK bundle — or defined in the consuming developer’s application, so they may be run inside the SDK methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@interface SDKManager()
 
@property (strong, nonatomic) ConfigurationManager *cm;
@property (strong, nonatomic) NSMutableArray *coarseGrainPlugins;
 
@end
 
@implementation SDKManager
 
-(id)init {
    self = [super init];
    if (self) {
        self.cm = [ConfigurationManager sharedInstance];
        self.coarseGrainPlugins = [[NSMutableArray alloc] init];
 
        //Gets the NSArray of NSString objective-c classnames loaded from the configuration file.
        NSArray *plugins = [self.cm plugins];
        for (NSString *className in plugins) {
            [self.coarseGrainPlugins addObject:[[NSClassFromString(className) alloc] init]];
        }
    }
    return self;
}
 
- (void)someSDKMethod {
    NSDictionary *dict = @{"key": @"value", @"SDKProcess": "YES"};
 
//Use your neccessary plugins as well as the user defined plugins to process.
    for (PluginProtocol *plugin in self.coarseGrainPlugins) {
        if ([plugin canYouProcessMe:dict]) {
            dict = [plugin process:dict];
        }
    }
}

Reflecting on these examples it’s possible to see how an agile SDK can empower developers with the tools they need to drive further innovation. Agility and flexibility are the future of open-source development, so let’s build the clean, robust tools WE would want to use. The development community will be better empowered by our efforts.