Dependency Injection in Objective-C with Magic and Blood

Original author: AlexDenisov
  • 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 requestManageras a singleton in an application, but replace it with mock in tests. In this case, no RequestManagerno ViewControllerdo 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 and Viewcreated 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 @­dynamicproperties will be created on the first call of self.progressViewServiceand 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 nilor 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 containerClassthat 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 UsersViewControllerand ProjectsViewController. Defaults to containerClassclass 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_initializerwill be replaced by __attribute__((constructor)) static void. The attribute constructormeans 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

Also popular now: