📜 ⬆️ ⬇️

Dependency Injection in Objective-C with Magic and Blood

MVC separation is not enough


Every day, iOS applications are becoming more cumbersome, as a result of which one MVC becomes scarce.

We see more and more classes for various purposes: the logic is put into services, the models are turned into decorators, large representations are broken up into smaller parts. And the most important thing is that in this case we have a lot of dependencies, and we must somehow manage them.

Very often, to solve a dependency problem, Singleton is used, in fact a global variable to which everyone has access.
How often have you seen such a 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 drawbacks:


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 contributes to solving this problem.


Dependency injection


These problems 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 requestManager as a singleton in the application, but in tests replacing it with a mock. At the same time, neither RequestManager nor ViewController knows anything about singletons, because this behavior controls the DI framework.

There are many Objective-C implementations of DI on a githaba, but they have their drawbacks:



Blood Magic


Let's look at another framework (with other disadvantages) - Blood Magic (BloodMagic, BM)

BM implements the similarity of custom attributes for Objective-C properties of classes. It was designed with extensibility in mind and more features will be added soon. Currently 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 <BMLazy> @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 when you first call self.progressViewService and self.resourceLoader . These objects will be freed as well as ordinary properties - after the release of ViewController .

Blood Magic and Dependency Injection


By default, the class method +new used to create objects. 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 slightly verbose:

 BMInitializer *initializer = [BMInitializer lazyInitializer]; initializer.propertyClass = [ProgressViewService class]; initializer.initializer = ^id (id sender){ return [[ProgressViewService alloc] initWithViewController:sender]; }; [initializer registerInitializer]; 

propertyClass - initializer is registered for properties of this class.
initializer - the block that will be called to initialize the object. If this block is nil or the initializer is not found, then the object will be created using the +new method.
sender is an instance of a container class.

Also, the initializer has the property containerClass , which 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 the UsersViewController and ProjectsViewController . By default, containerClass is equal to the class NSObject .

Initializers help get rid of the various shared* methods and hardcodes 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


There can be many initializers in the project, so it makes sense to move them to a separate place / module.

A good solution is to split them across different files and use compiler flags. There is a simple macro in Blood Magic that hides these attributes - the lazy_initializer . All you need is to create a file without a header and add it to the compilation phase.

Example:

 // LoaderInitializer.m #import <BloodMagic/Lazy.h> #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 constructor attribute means that this method will be called earlier than main (there is a more detailed description here: GCC. Function Attributes ).

Plans for the near future


Source: https://habr.com/ru/post/198328/


All Articles