Reliant is a Dependency Injection (DI) framework for Objective-C, both for OS X and iOS. Its goal is to make its use as simple as possible, while not limiting its possibilities. It aims to have as little impact as possible on your project code. It also aims to be loyal to Objective-C's dynamic nature.
The motivation for this library came from being used to a highly testable infrastructure in other languages thanks to DI. Looking at the typical design pattern to solve the loose coupling problem, the Abstract Factory pattern is the natural solution. Although factories, in conjunction with mock libraries provide a fairly nice and testable solution, the pure loose coupling is never reached, since you still have a dependency to a factory in almost all classes in your project, which is a rather large footprint. Before starting this library, I looked for opinions about DI in dynamic languages at the one hand, and in frontend driven solutions at the other hand. Reliant is an answer to these questions.
At the moment, Reliant is still under development, and put here for review by the community. Although we consider the latest version to be pretty complete, there is still room for improvement. Obviously, since this is open source, do feel free to add your own insights/ideas/remarks/opinions.
The framework is set up to be lightweight, you basically need an OCSApplicationContext
which serves as the container of the managed objects. To register managed objects in this
container, the application context uses an OCSConfigurator
instance. The OCSConfigurator
is responsible for creating OCSDefinitions
, which describe the objects you want to put
under the application context's control.
At the moment, Reliant identifies two types of objects: singletons and prototypes. (These names are taken from the well know design patterns)
- A
singleton
is a stateless shared object, which is created only once. Objects created as singleton should be thread safe! Reliant further identifies eager and lazy singletons. Eager means that they will be instantiated when the application context boots up, lazy means they will be instantiated Just-in-Time, when they are requested.
For iOS, Reliant also reacts to memory warnings, by clearing its singleton scope. In this case, all singletons become lazy singletons and will be initialized again when requested.
- A
prototype
will be created each time it is requested from the application context. Be carefull though! If you inject a prototype into a singleton, the prototype's livecycle is bound to the singleton!
Objects are identified by a key and can have aliases, both strings. The framework makes sure these are unique. Exceptions will be thrown if an attempt is made to add an object with a non-unique key or alias.
Now let's look at a Quick start example.
Reliant is available via CocoaPods, you can add this line to your podfile:
pod 'Reliant'
Then run
pod install
for more information about CocoaPods, refer to http://cocoapods.org
Now you are ready to import the headers in your own code:
#import <Reliant/OCSApplicationContext.h>
Documentation is available through CocoaDocs: http://cocoadocs.org/docsets/Reliant/
To get started with Reliant, you need to tell the OCSApplicationContext to start up. You will need to provide a configurator instance to the application context. More on configurators later, bare with us for now.
//Initialize a configurator
id<OCSConfigurator> configurator = [[OCSConfiguratorFromClass alloc]
initWithClass:[YourObjectFactory class]];
//Initialize the application context with the configurator
OCSApplicationContext *context = [[OCSApplicationContext alloc]
initWithConfigurator:configurator];
//Start the context
[context start];
//Done! Well...
This will bootstrap the entire application context. At the time the start method finishes its job, it will have loaded your defintions and it will have instantiated and injected your eager singletons.
Now where should you put this peace of code? As close as possible to where the application startup. For iOS this means in the application:didFinishLaunchingWithOptions: method in your UIApplicationDelegate. For OS X this is almost the same: applicationDidFinishLaunching: in your NSApplicationDelegate.
This is a ready-made configurator implementation. It uses the information found in a class provided by you through introspection. The provided class will also serve as the creator of your objects, hence we will call it a factory class. The idea behind this is to give you a programatic way to define objects, and make the configuration itself subject to testing. This makes the use of external configuration files and/or macros obsolete, which yields more robust code.
So what you need to use in this configurator, is a factory class. The methods in this class will be responsible for creating your objects. In order for the framework to detect these methods, you will need to follow some naming conventions on them.
OCSConfiguratorFromClass will detect 4 kinds of methods. (Replace YourObjectKey each time with a unique name)
- (id) createSingleton/*YourObjectKey*/; //Lazy singleton definition
- (id) createEagerSingleton/*YourObjectKey*/; //Eager singleton definition
- (id) createPrototype/*YourObjectKey*/;//Prototype definition
- (NSArray *) aliasesFor/*YourObjectKey*/;//Alias definitions
For your convenience we created some xCode snippets to help you create these methods.
Let's look at them in more detail:
For each lazy singleton you need, you should add a method with the following signature:
- (id) createSingletonFoo {
return [[Foo alloc] init];
}
You can also use what is called constructor injection by calling another createSingleton or createEagerSingleton method:
- (id) createSingletonBar {
return [[Bar alloc] initWithSomeObject:[self createSingletonFoo]];
}
Don't worry about calling the same createSingleton method more than once, the framework will only really call each method once and reuse the same result on the succeeding calls, making the results true singletons.
You don't necessarily need to inject your objects through constructor injection. Later on we will explain how objects are injected through the use of properties.
To create eager singletons, add this kind of method:
- (id) createEagerSingletonFooBar {
return [[FooBar alloc] init];
}
For creating prototypes we can use a similar approach. Only the method name changes a bit:
- (id) createPrototypeFooBar {
return [[FooBar alloc] init];
}
Remember! Each time a prototype is requested, this method will be called. You should therefore consider to keep the initialization as performant as possible.
Registering aliases for an object is also possible. Again, you just need to add a method with a certain signature:
- (NSArray *) aliasesForFoo {
return @[@"_foo",@"_fuu"];
}
By default, two aliases are already registered for each object. They take the form of the key in uppercase (eg. FOO, BAR, FOOBAR, ...) and the key starting with a lowercase (eg. foo, bar, fooBar, ...). Aliases must be unique, and should also never be equal to an object key. If an attempt is made to add a duplicate, an exception will be raised. The automatically added aliases, will only be added if they are not a duplicate of the key.
You can add other methods, which might help in creating your objects. These will be ignored by the framework, but you can obviously use them in your create methods.
In larger applications, the factory class can quickly become huge. This is where you can and should use Objective-C's category mechanism. For each logical group of objects you can create a category, named after this logical group. All methods in all categories of your factory class will be taken into account. It might look something like this (interfaces will be omitted for brevity).
@implementation ReliantFactory
- (id) createSingletonGeneralObject {
return ...;
}
@end
@implementation ReliantFactory (Services)
- (id) createEagerSingletonServiceA {
return [[ServiceA alloc] init];
}
- (id) createEagerSingletonServiceB {
return [[ServiceB alloc] init];
}
@end
@implementation ReliantFactory (Repositories)
- (id) createEagerSingletonRepositoryA {
return [[RepositoryA alloc] init];
}
- (id) createEagerSingletonRepositoryB {
return [[RepositoryB alloc] init];
}
@end
All objects created in the application context will be injected after their creation. This is done as explained before by constructor injection and/or by using Objective-C's KVC mechanism. Reliant will scan your object's properties. If a writable property's name matches with a key or an alias for an object in the application context, and if its current value is nil, the matching object will be injected in this property. All other properties will be left alone. This will be done for the entire class hierarchy of the instance.
You can use the injection mechanism described above on objects which are not setup in the application context. A good example would be a UIViewController. In order to make things easier, you can make use of the fact that we have bootstrapped our application context in the UIApplicationDelegate. Since the UIApplication is a shared object (hey, another singleton!) we can do our injection from here.
We already discussed that Reliant will clear its singleton cache whenever a memory warning occurs. Reliant thereby releases its ownership of the instances. However, it can not be held responsible for the objects injected outside of its scope as discussed above. You should therefore retain/release any injected objects yourself. For property injection, this means that your dependent properties should have the retain attribute on it.
This is what you need to do:
//In your UIApplicationDelegate
@implementation MyAppDelegate {
OCSApplicationContext *_context;
}
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//...
//Initialize a configurator
id<OCSConfigurator> configurator = [[OCSConfiguratorFromClass alloc]
initWithClass:[YourObjectFactory class]];
//Initialize the application context with the configurator
_context = [[OCSApplicationContext alloc] initWithConfigurator:configurator];
//Start the context
[_context start];
//...
}
- (void) performInjectionOn:(id) object {
[_context performInjectionOn:object];
}
@end;
//In your UIViewController
@implementation MyViewController
@synthesize foo;
- (void) viewDidLoad {
[super viewDidLoad];
AppDelegate *appDelegate = (AppDelegate *) [UIApplication sharedApplication].delegate;
[appDelegate performInjectionOn:self];
}
- (void) viewDidUnload {
//Set any injected property to nil here!
self.foo = nil
[super viewDidUnload];
}
@end
And that's all there is to it. The property foo will be injected by Reliant.
Warning: when using storyboards you should load your storyboard manually. You can accomplish this by removing your storyboard in the project settings and loading the storyboard in code.
UIStoryboard *sb = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];
UIViewController *vc = [sb instantiateViewControllerWithIdentifier:@"myViewController"];
Reliant will, by default, not try to inject properties like view on UIViewController. However if your implementation has the need for ignoring extra properties you can implement the following method in your class:
+ (BOOL) OCS_reliantShouldIgnorePropertyWithName:(NSString *) name {
static NSArray *excludedProps;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
excludedProps = @[@"ignoredProperty1", @"ignoredProperty2"];
});
//it's important here to call this method on super, as reliant has it's own category on NSObject
return [super OCS_reliantShouldIgnorePropertyWithName:name] || [excludedProps containsObject:name];
}
For your information here are the default ignored properties on the classes they belong to:
- NSObject
- accessibilityLanguage
- accessibilityValue
- accessibilityHint
- accessibilityLabel
- accessibilityPath
- UIResponder
- restorationIdentifier
- UIViewController
- tabBarItem
- title
- toolbarItems
- view
- aggregateStatisticsDisplayCountKey
- nibName
- storyboard
- parentViewController
- modalTransitionView
- mutableChildViewControllers
- childModalViewController
- parentModalViewController
- searchDisplayController
- dropShadowView
- afterAppearanceBlock
- transitioningDelegate
- customTransitioningView
As we already saw, a configurator is responsible for setting up definitions and creating object instances based on those definitions. Although a default class configurator (OCSConfiguratorFromClass) is provided by Reliant, you can always build your own. Your custom configurator should conform to the OCSConfigurator protocol. In the configurator's designated initializer, you should start building your object definitions. A configurator, although not enforced, should maintain its own definition registry. A configurator should not start creating instances for these definitions just until the contextLoaded: message is send to it. Only after all work is done, the configurator should return objects through its objectForKey:inContext: method. When work is done, the initializing property should be true/YES/whatever-other-bool-literal-you-prefer.
An abstract implementation is also provided. This is the OCSConfiguratorBase which deals with the boilerplate code for keeping track of registered definitions and objects. If you extend this class, you should import the OCSConfiguratorBase+ForSubclassEyesOnly.h header in your implementation (.m file). This will allow you to call "protected" methods and properties, hidden for non-extending classes. You should never use this category outside of a subclass, doing so will cause unexpected behavior.
If you extend OCSConfiguratorBase, you should not override the methods defined in OCSConfigurator. You must instead implement createObjectInstanceForKey:inContext: and internalContextLoaded: (See API documentation for more information)
Although the framework is extendible, we encourage you to use the provided OCSConfiguratorFromClass or extend via the OCSConfiguratorBase.
// CustomConfigurator.h
@interface CustomConfigurator : OCSBaseConfigurator
@end
// CustomConfigurator.m
TODO
@end
We have provided some snippets to easily create the signature to create singletons in the configurator class.
These can be found here: xCode Snippets
You can install these by downloading these snippets and placing them in the following folder:
~/Library/Developer/Xcode/UserData/CodeSnippets/
If the folder doesn't exist you can create it. Restart xCode and you should be ready to start using the snippets.
In the class implementation of your configurator start typing 'create' and all of the snippets for create singletons, prototypes, and aliases should be at your finger tips.
- Discussion on the necessity of DI in Objective C/Dynamic languages
-
Spring framework. I dare say this is the de facto standard IoC container in the Java world. Although going much (much ... much) further then DI, this framework was one of if not the pioneer in DI framework. Since spring 3.0, a Configuration class system is available. Reliant builds on this principle.
-
Guice. Google's type safe DI solution. Partially as an answer to spring, which was not very strong on the type-safety side of things at that time. Spring fixed this in version 3.0. Although Guice is a very well thought of DI framework, which should be marvelled for its simplicity and light-weightness, I personally feel that basing a DI framework for a dynamic language on Guice is a bridge too far. It would break down too many of the main goals of Guice, namely type safety. I'm not saying type safety is unimportant, I'm just saying that Objective-C (and other dynamic languages for that matter) needs a different approach.
-
Objection. Another DI framework for Objective-C, based on Guice. As stated before, the "binding" approach did look appealing to me at first, but I don't think binding to types works very well in Objective-C. But still, a very well made port.
- Filip Maelbrancke: for second opinions and rubber ducking
- Bart Vandeweerdt and Willem Van Pelt: for reviewing this documentation
- AppFoundry: for letting me use this in production code
- Oak Consultancy Services: for necessary resources
- Liesbet Gouwy: for unconditional support
- Kato Seghers: for being born
If not via GitHub, find me on twitter: @mikeseghers
Reliant is released under the Apache License, Version 2.0