
Dependency Injection in Objective-C with Magic and Blood
- Transfer
MVC separation is not enough
Every day, iOS applications are becoming more cumbersome, as a result of which one MVC is not enough.
We see more and more classes for various purposes: the logic is taken out in services, models are turned into decorators, large representations are divided into smaller parts. And most importantly, in this case, we have a lot of dependencies, and we must somehow manage them.
Very often, Singleton is used to solve the dependency problem, essentially a global variable that everyone has access to.
How often have you seen this code?
[[RequestManager sharedInstance] loadResourcesAtPath:@"http://example.com/resources" withDelegate:self];
// или
[[DatabaseManager sharedManager] saveResource:resource];
This approach is used in many projects, but it has some disadvantages:
- the singleton that is used inside the tested class is hard to replace with a mock object
- essentially singleton is a global variable
- in terms of SRP, an object should not control its Singleton behavior
The first problem is quite simple to solve - you need to use the properties:
@interface ViewController : UIViewController
@property (nonatomic, strong) RequestManager *requestManager;
@end
But this approach has other disadvantages - now someone has to "fill in" this property.
Blood Magic helps solve this problem.
Dependency Injection
These issues are not unique to Objective-C. If we look at more “industrial” languages, such as Java or C ++, we can find a solution. A widely used approach in Java - Dependency Injection (DI)
DI allows you to use it
requestManager
as a singleton in an application, but replace it with mock in tests. In this case, no RequestManager
no ViewController
do not know anything about Singleton, because it controls the behavior of DI freymvork. There are many DI implementations on Objective-C on the github, but they have their drawbacks:
- description of dependencies using macros or string constants
- implementation occurs only if the object is created in a "special" way (this option will not work with
ViewController
's andView
created from storyboards and anything) - the implemented class must implement some protocol (it will not work with third-party or standard libraries)
- initialization cannot be moved to a separate module
- XML
Blood Magic
Let's look at the next framework (with other drawbacks) - Blood Magic (BloodMagic, BM)
BM implements a kind of custom attributes for the properties of Objective-C classes. It was designed with scalability in mind and more features will be added soon. At the moment, only one attribute is implemented - Lazy, Lazy Initialization .
This attribute allows you to initialize properties on demand, without writing routine code. Thus, instead of similar sheets:
@interface ViewController : UIViewController
@property (nonatomic, strong) ProgressViewService *progressViewService;
@property (nonatomic, strong) ResourceLoader *resourceLoader;
@end
@implementation ViewController
- (void)loadResources
{
[self.progressViewService showProgressInView:self.view];
self.resourceLoader.delegate = self;
[self.resourceLoader loadResources];
}
- (ProgressViewService *)progressViewService
{
if (_progressViewService == nil) {
_progressViewService = [ProgressViewService new];
}
return _progressViewService;
}
- (ResourceLoader *)resourceLoader
{
if (_resourceLoader == nil) {
_resourceLoader = [ResourceLoader new];
}
return _resourceLoader;
}
@end
you can simply write:
@interface ViewController : UIViewController
@property (nonatomic, strong, bm_lazy) ProgressViewService *progressViewService;
@property (nonatomic, strong, bm_lazy) ResourceLoader *resourceLoader;
@end
@implementation ViewController
@dynamic progressViewService;
@dynamic resourceLoader;
- (void)loadResources
{
[self.progressViewService showProgressInView:self.view];
self.resourceLoader.delegate = self;
[self.resourceLoader loadResources];
}
@end
And that’s all. Both
@dynamic
properties will be created on the first call of self.progressViewService
and self.resourceLoader
. These objects will be released as well as ordinary properties - after the release of ViewController
'a.Blood Magic and Dependency Injection
By default, the class method is used to create objects
+new
. But it is possible to describe your own custom initializers, which are a key feature of BM as a DI framework. Creating a custom initializer is a bit verbose:
BMInitializer *initializer = [BMInitializer lazyInitializer];
initializer.propertyClass = [ProgressViewService class];
initializer.initializer = ^id (id sender){
return [[ProgressViewService alloc] initWithViewController:sender];
};
[initializer registerInitializer];
propertyClass
- the initializer is registered for the properties of this class. initializer
- the block that will be called to initialize the object. If this block nil
or initializer is not found, then the object will be created using the method +new
. sender
- an instance of the container class. The initializer also has a property
containerClass
that allows you to describe the creation of the same property in different ways, based on the container. For example:BMInitializer *usersLoaderInitializer = [BMInitializer lazyInitializer];
usersLoaderInitializer.propertyClass = [ResourceLoader class];
usersLoaderInitializer.containerClass = [UsersViewController class];
usersLoaderInitializer.initializer = ^id (id sender){
return [ResourceLoader usersLoader];
};
[usersLoaderInitializer registerInitializer];
BMInitializer *projectsLoaderInitializer = [BMInitializer lazyInitializer];
projectsLoaderInitializer.propertyClass = [ResourceLoader class];
projectsLoaderInitializer.containerClass = [ProjectsViewController class];
projectsLoaderInitializer.initializer = ^id (id sender){
return [ResourceLoader projectsLoader];
};
[projectsLoaderInitializer registerInitializer];
Thus , different objects will be created for
UsersViewController
and ProjectsViewController
. Defaults to containerClass
class NSObject
. Initializers help get rid of the various
shared*
methods and hardcode described at the beginning of the article:BMInitializer *initializer = [BMInitializer lazyInitializer];
initializer.propertyClass = [RequestManager class];
initializer.initializer = ^id (id sender){
static id singleInstance = nil;
static dispatch_once_t once;
dispatch_once(&once, ^{
singleInstance = [RequestManager new];
});
return singleInstance;
};
[initializer registerInitializer];
Organization and storage of initializers
A project can have many initializers, so it makes sense to move them to a separate place / module.
A good solution is to split them into different files and use compiler flags. There is a simple macro in Blood Magic that hides these attributes
lazy_initializer
. All that is needed is to create a file without a header and add it to the compilation phase. Example:
// LoaderInitializer.m
#import
#import "ResourceLoader.h"
#import "UsersViewController.h"
#import "ProjectsViewController.h"
lazy_initializer ResourseLoaderInitializers()
{
BMInitializer *usersLoaderInitializer = [BMInitializer lazyInitializer];
usersLoaderInitializer.propertyClass = [ResourceLoader class];
usersLoaderInitializer.containerClass = [UsersViewController class];
usersLoaderInitializer.initializer = ^id (id sender){
return [ResourceLoader usersLoader];
};
[usersLoaderInitializer registerInitializer];
BMInitializer *projectsLoaderInitializer = [BMInitializer lazyInitializer];
projectsLoaderInitializer.propertyClass = [ResourceLoader class];
projectsLoaderInitializer.containerClass = [ProjectsViewController class];
projectsLoaderInitializer.initializer = ^id (id sender){
return [ResourceLoader projectsLoader];
};
[projectsLoaderInitializer registerInitializer];
}
lazy_initializer
will be replaced by __attribute__((constructor)) static void
. The attribute constructor
means that this method will be called earlier main
(there is a more detailed description here: GCC. Function Attributes ).Plans for the near future
- implement protocol support (
@property (nonatomic, strong) id loader)
add a description of work and implementations
describe add new attributes
add more attributes