Keep your hats on! - Typhoon - The Dependency Injection Framework
What is the Dependency Injection (DI)? When I heard about DI for the first time it sounded very difficult to understand. Luckily, I was wrong ;).
Dependency injection is a simple principle. To summarize it in on sentence „The object should rely on it’s dependencies, but it shouldn’t instantiate them.”.
How? – The example
In my life I’ve learned that nothing describes something as good as a good example. Assume you have a LoginSystem
. Its duty is to receive a User
object and save it for further needs. It uses UserRepository
to save User
object which came earlier from an API. First I will show you how can it looks like when you don’t follow with DI objectives.
typedef void(^LoginSystemLoginCallback)(BOOL success, NSError *error);
@interface LoginSystem : NSObject
- (void)loginWithRequest:(LoginRequest *)request callback:(LoginSystemLoginCallback)callback;
@end
@implementation LoginSystem
- (void)loginWithRequest:(LoginRequest *)request callback:(LoginSystemLoginCallback)callback{
[[DumbAPIConnection new] invokeRESTRequest:request success:^(id response) {
[self storeUserFromResponse:response];
[self tryToInvokeCallback:callback error:nil];
} failure:^(NSError *error) {
[self tryToInvokeCallback:callback error:error];
}];
}
- (void)storeUserFromResponse:(id)response {
User *user = [[DumbUserParser new] parseResponse:response];
[[UserRepository sharedInstance] saveUser:user];
}
- (void)tryToInvokeCallback:(LoginSystemLoginCallback)callback error:(NSError *)error {
if(callback)
callback(error == nil,error);
}
@end
You can simply change that class to apply the Dependency Injection rule:
typedef void(^LoginSystemLoginCallback)(BOOL success, NSError *error);
@interface LoginSystem : NSObject
@property(nonatomic, strong) id<APIConnection> apiConnection;
@property(nonatomic, strong) id<UserRepository> userRepository;
@property(nonatomic, strong) id<UserParser> parser;
- (instancetype)initWithApiConnection:(id <APIConnection>)apiConnection userRepository:(id<UserRepository>)userRepository parser:(id <UserParser>)parser;
- (void)loginWithRequest:(LoginRequest *)request callback:(LoginSystemLoginCallback)callback;
@end
@implementation LoginSystem
- (instancetype)initWithApiConnection:(id <APIConnection>)apiConnection userRepository:(id<UserRepository>)userRepository parser:(id <UserParser>)parser {
self = [super init];
if (self) {
self.apiConnection = apiConnection;
self.userRepository=userRepository;
self.parser=parser;
}
return self;
}
- (void)loginWithRequest:(LoginRequest *)request callback:(LoginSystemLoginCallback)callback{
[self.apiConnection invokeRESTRequest:request success:^(id response) {
[self storeUserFromResponse:response];
[self tryToInvokeCallback:callback error:nil];
} failure:^(NSError *error) {
[self tryToInvokeCallback:callback error:error];
}];
}
- (void)storeUserFromResponse:(id)response {
User *user = [self.parser parseResponse:response];
[self.userRepository saveUser:user];
}
- (void)tryToInvokeCallback:(LoginSystemLoginCallback)callback error:(NSError *)error {
if(callback)
callback(error == nil,error);
}
@end
Pretty simple, isn’t it? Instead of instantiating dependencies inside the LoginSystem
class replace them with properties and export them to header file! When it comes to creating an instance of the LoginSystem
class, just assign values to proper properties.
Look how UserRepository is initialized in the first example. We can guess with a hight probability that it is a singleton, because of the method name. In DI’s example, you wouldn’t say anything like that based just only on this piece of code. I’ll come back to singletons in further chapters.
Why should you obey Dependency Injection principle?
There are mainly 2 benefits from what we did:
- The
LoginSystem
class is much easier to test – In the first version ofLoginSystem
to test it we need to take care of how to connect with API, how to parse a JSON and how to save it. Why? We cannot mock its dependencies because they are not public, so we need to testLoginSystem
as a big object. After we extracted its dependencies to public properties we can now inject mocks and check if theLoginSystem
called a method ofAPIConnection
to download a User, then if it parsed the User by calling aUserParser
method and then if it saved the User. - We can change details of
UserRepository
without changing any lines inLoginSystem
– Right nowUserRepository
uses anNSUserDefaults
to store a user object. It could be enough for some kind of application, but usually we would like to useCoreData
to take care of persistence layer. What we can do now is to create aCoreDataUserRepository
class. Conforms it toUserRepository
protocol and inject it toLoginSystem
. We switched from usingNSUserDefaults
toCoreData
without any change inLoginSystem
. How cool is that?
Init… init… init… and init – Inits everywhere!
DI leads to one problem. When it comes to creating an object you will have long list of inits. In my last project, it could look like this:
[[SCRDocumentsInteractor alloc] initWithDocumentCenterFacade:
[[SCRDocumentCenterFacade alloc] initWithDocumentProvider:
[[SCRAPIDocumentProvider alloc] initWithConnection:
[[SCRAPIConnectionAFNetworking alloc] initWithBaseURL:
[NSURL URLWithString:@"http://url"]] parser:
[[SCRMantleJSONDocumentParser alloc] init]] documentDeleter:
[[SCRDocumentDeleter alloc] initWithApiConnection:
[[SCRAPIConnectionAFNetworking alloc] initWithBaseURL:
[NSURL URLWithString:@"http://url"]]]] userPersistenceReader:
[[SCRMagicalRecordUserPersistenceReader alloc]
initWithManagedObjectContext:managedObjectContext]];
Typhoon – Dependency injection framework for iOS
Luckily there is a tool which can help you to avoid so messy inits – it’s called Typhoon.
Assemblies
Heart of Typhoon is an assembly … or maybe are assemblies. Those classes collect dependencies and linked them together. I like to create one assembly per each module. I do so, because of readability. I don’t want to create 1000+ lines files.
Keep one thing in mind. To make Assemblies work you need to activate them. The simplest way to do so is to add TyphoonInitialAssemblies
key to your .plist file. This key takes an array of assembly names (strings) as an argument.
To create an assembly just create a class and extend the TyphoonAssebmly
class. Now you need to create TyphoonDefinitions
. Typhoon doesn’t create objects immediately. It creates them when they are actually needed. TyphoonDefinitions
describe how to create objects and how to link them.
- (AppDelegate *)appDelegate{
return [TyphoonDefinition withClass:[AppDelegate class] configuration:^(TyphoonDefinition *definition) {
[definition injectProperty:@selector(someObjectNeededForSomething) with:[self someObject]];
}];
}
- (SomeObject *)someObject {
return [TyphoonDefinition withClass:[SomeObject class]];
}
Typhoon will create an instance of AppDelegate with dependencies which we want to have ;).
Let’s inject some real stuff
AppDelegate was just an example. Now is the time to create a ViewController with all it’s dependencies.
@implementation LoginAssembly
- (LoginViewController *)loginViewController {
return [TyphoonDefinition withClass:[LoginViewController class] configuration:^(TyphoonDefinition *definition) {
[definition injectProperty:@selector(loginSystem) with:[self loginSystem]];
}];
}
- (LoginSystem *)loginSystem {
return [TyphoonDefinition withClass:[LoginSystem class] configuration:^(TyphoonDefinition *definition) {
[definition useInitializer:@selector(initWithApiConnection:) parameters:^(TyphoonMethod *initializer) {
[initializer injectParameterWith:[self.applicationAssembly apiConnection]];
}];
[definition injectProperty:@selector(delegate) with:[self loginViewController]];
}];
}
@end
@implementation ApplicationAssembly
- (id <APIConnection>)apiConnection {
return [TyphoonDefinition withClass:[AFNetworkingAPIConnection class] configuration:^(TyphoonDefinition *definition) {
[definition useInitializer:@selector(initWithBaseURL:) parameters:^(TyphoonMethod *initializer) {
[initializer injectParameterWith:TyphoonConfig(@"APIBaseURL")];
}];
}];
}
@end
This is everything. Now when it comes to instantiating LoginViewController
Typhoon will automatically inject a LoginSystem
object to it! Ok … it’s nearly everything 😛
Typhoon will automatically link your view controller definition inside assembly if you use TyphoonStoryboard
object to create the view controller. It is simpler than it sounds. You just need to inject the storyboard object to AppDelegate by following lines:
@implementation ApplicationAssembly
- (AppDelegate *)appDelegate{
return [TyphoonDefinition withClass:[AppDelegate class] configuration:^(TyphoonDefinition *definition) {
[definition injectProperty:@selector(someObjectNeededForSomething) with:[self someObject]];
[definition injectProperty:@selector(storyboard) with:[self.storyboardAssembly mainStorybaord]];
}];
}
[...]
@end
@implementation StoryboardAssembly
- (UIStoryboard *)mainStorybaord {
return [TyphoonDefinition withClass:[TyphoonStoryboard class] configuration:^(TyphoonDefinition *definition) {
[definition useInitializer:@selector(storyboardWithName:factory:bundle:) parameters:^(TyphoonMethod *initializer) {
[initializer injectParameterWith:@"Main"];
[initializer injectParameterWith:self];
[initializer injectParameterWith:[NSBundle mainBundle]];
}];
}];
}
[...]
@end
Scopes
Singletons – they are hated and loved objects ;). I use them but only when I really need them. However, I like to say „it’s ok to use singletons but an object which uses the singleton should not know that it uses a singleton”. How to achieve that? Just continue reading ;).
In Typhoon when you define TyphoonDefinition
you can set a scope. The scope defines if or what reference type will Typhoon keep to the created object. It’s an enum with following values:
- TyphoonScopePrototype – it creates a new instance of class at every call
- TyphoonScopeSingleton – it creates a single instance of class and it keeps strong reference to it
- TyphoonScopeLazySingleton – the same as above, but it is initialized when it’s called for the first time
- TyphoonScopeWeakSingleton – the same as
TyphoonScopeSingleton
but it keeps weak reference - TyphoonScopeObjectGraph – default scope of Typhoon. It creates a new instance of class nearly at every call. It makes it possible to create circular dependencies.
Now it is a time to explain the above sentence.
Typhoon gives you a possibility to create object which will be a singleton. Now you can inject it to objects which gonna need that dependency. Done! Your object uses a singleton. Does he know that he uses a singleton instance? No, it doesn’t! You don’t call [MyObject sharedInstance]
anywhere inside the objects class. Why is it so cool? You can postpone the decision if it is needed to have a singleton instance! You write a class like you would never use a singleton. When it occurs to you … „Hey, I definitely need a singleton here” then you can just change the scope of its definition. definition.scope = TyphoonScopeSingleton
Circular dependency
In above chapter I used „Circular dependency” phrase. The circular dependency exists when 2 classes have references to each other. It is not so rare case. When you use a delegate you can have such chance. If Typhoon will always create a new instance of a class it would be very problematic to connect 2 objects in both ways. Definition with scope TyphoonScopeObjectGraph
will link them in both ways. Go few paragraph up to see a code example.
Last word
Dependency injection is not difficult pattern to understand. It can be explained with one sentence „The object should rely on it’s dependencies, but it shouldn’t instantiate them.”.
However, it is helpful to use some Dependency Injection Frameworks to get rid of long list of inits. One of such libraries for iOS is Typhoon. It is a kind of factory which creates objects when they are actualy needed. Our duty, as developers, is to create TyphoonDefinition
to tell Typhoon what it should create.
I’ve been using Typhoon for about last 6 months and I think it is very handy to use. Moreover it’s powerful tool and I’ve skipped few possibilities to make the article relatively short. If you have any question just write it in comments. I’ll answer for it with pleasure!
At the end, I would like to ask you for something. If you find this article interesting, help others to reach it as well. Share the link on Twitter, Facebook or any other places you can! Thanks!
You can find project with above examples on my GitHub.
What next?
Recently I’ve configured the fastlane with our Jenkins and I’m really amazed how delivery process can be automated. However, I’m just after 4-month project where I used Viper as general architecture for the app. I’m quite happy how the app looks right now, but I had few very big problems with Viper assumptions. What would you like to read about in next article? My thought about Viper or how I configured fastlane? I wait for your comments or tweets.