-
Notifications
You must be signed in to change notification settings - Fork 268
1.x_Assembling components with blocks
Block Assembly | Xml Assembly | 1.x_Autowiring | 1.x_Using-assembled-components | 1.x_Incorporating | 1.x_Configuration-Management-&-Testing
#News
We will be releasing Typhoon 2.0 in the coming weeks, whereupon we will archive this document. Typhoon 2.0 includes some minor changes to the block-style assembly - making it more compact and adding powerful new features.
If you wish to try Typhoon 2.0 features, please use the code in master. Otherwise checkout a 1.x tag from github or resolve from CocoaPods.
#Quick Start
Setting up a Dependency Injection container couldn't be more easy.
First, create a sub-class of TyphoonAssembly as follows:
@interface MiddleAgesAssembly : TyphoonAssembly
- (id)basicKnight;
- (id)cavalryMan;
- (id)defaultQuest;
@end
Add the method names in the header. This will allow compile-time checking and IDE code-completion. Now simply define the components:
###Perform an Initializer Injection
- (id)basicKnight
{
return [TyphoonDefinition withClass:[Knight class] initialization:^(TyphoonInitializer* initializer)
{
initializer.selector = @selector(initWithQuest:);
//For more control, you can use injectParameterAtIndex or withName, but probably just. . .
[initializer injectWithDefinition:[self defaultQuest]];
//. . . which means the order will follow that of the parameters in the selector.
}];
}
- (id)defaultQuest
{
return [TyphoonDefinition withClass:[CampaignQuest class]];
}
Notice how you get code-completion on the selector name, and components to be injected. So if you rename a class or method, those updates will show-up here too.
###Resolve the component from the container as follows:
TyphoonComponentFactory* factory = [[TyphoonBlockComponentFactory alloc] initWithAssemblies:@[
[MiddleAgesAssembly assembly]
]];
Knight* knight = [(MiddleAgesAssembly*) factory basicKnight]; //Code-completion + no 'magic strings'
And we're done!
##More Details
###Injection can be done via a property setter:
- Properties can be injected by explicit reference, or by matching the required type.
- (id)cavalryMan
{
return [TyphoonDefinition withClass:[CavalryMan class] properties:^(TyphoonDefinition* definition)
{
//wire-by type
[definition injectProperty:@selector(quest)];
//explicit wiring - useful when there's more than one component representing a given type
[definition injectProperty:@selector(quest) withDefinition:[self defaultQuest]];
}];
}
A property of type NSSet or NSArray can be populated as follows:
definition injectProperty:@selector(favoriteDamsels) asCollection:^(TyphoonPropertyInjectedAsCollection* collection)
{
//Uses Typhoon's type converter system
[collection addItemWithText:@"Mary" requiredType:[NSString class]];
[collection addItemWithText:@"Mary" requiredType:[NSString class]];
//Injects with a reference to another component
[collection addItemWithDefinition:[self anotherKnight]];
}];
Sometimes you wish to define components that depend on each other. For example a ViewController that is injected with a view, and a View that is injected with the ViewController as a delegate.
This is possible using property-style injection, but not for initializer injection.
###Additional configuration can be added in the properties block:
####Setting the scope:
- The default scope is TyphoonScopeObjectGraph - a scope unique among DI containers. This scope is especially geared towards mobile and desktop applications. When a component is resolved, any dependencies with the object-graph will be treated as shared instances during resolution. Once resolution is complete they are not retained by the TyphoonComponentFactory. This allows instantiating an entire object graph for a use-case (say for a ViewController), and then discarding it when that use-case has completed.
- TyphoonScopePrototype means that a new instance will always be created by Typhoon.
- The TyphoonScopeSingleton scope means that Typhoon will retain the instance that exists for as long as the TyphoonComponentFactory exists.
- TyphoonScopeWeakSingleton works the same as singleton, except that not components are currently using the singleton, it will be destroyed. A subsequent request will have it created again.
- (id)basicKnight
{
return [TyphoonDefinition withClass:[Knight class] initialization:^(TyphoonInitializer* initializer)
{
initializer.selector = @selector(initWithQuest:);
[initializer injectWithDefinition:[self defaultQuest]];
} properties:^(TyphoonDefinition* definition)
{
[definition setScope:TyphoonScopeSingleton];
}];
}
The container provides a way to invoke a method before or after property injection, to ensure that the instance is in the required state before receiving collaborating classes.
For example, in the case of a RootViewController, you can assert that root controller's view is not nil, before injecting child view controllers.
[definition setAfterPropertyInjection:@selector(configureBeforeUse)];
As an alternative to declaring the property injection methods in the assembly, if you're not worried about your class having a direct dependency on Typhoon, you can also implement the following protocol:
@protocol TyphoonPropertyInjectionDelegate <NSObject>
##Injection Styles
In the above examples, you saw some different styles of dependency injection.
Injection can be done by matching the required-type. (Guice-style). For these simple cases auto-wiring is also a good option: Autowiring
Injection can be done by reference. This is useful in the common requirement you have multiple components matching the same class or protocol. The injection is done by referencing a method in the assembly. This allows you to easily change the name of components as your design evolves, without anything breaking.
Injection can be done by value.
- An object instance can be provided (with auto-boxing for primitives).
- Alternatively, a text-representation can be provided. The container will look up the required class or primitive type and do a type conversion from the string-value at run-time. It's easy to register your own additional converters.
Examples:
//A vanila case
[definition injectProperty:@selector(serviceUrl) withObjectInstance:[NSURL URLWithString:@"http://www.myapp.com/service"]];
//Look-up a value using a property placeholder configurer (see instructions in Config Managment & Testing)
[definition injectProperty:@selector(serviceUrl) withValueAsText:@"${client.serviceUrl}"];
//Use auto-boxing
[initializer injectWithObjectInstance:@(NSPrivateQueueConcurrencyType)];
//Inject a class
[initializer injectWithObjectInstance:[SomeClass class]];
//Inject a struct
[definition injectProperty:@selector(frame) withObjectInstance:[NSValue valueWithPointer:CGRectMake(10, 10, 100, 100]];
##Creating a component that instantiates other components
Sometimes its necessary to have a component that creates other components. For example a legacy singleton that produces objects you'd like to inject into other classes in your app.
- (id)swordFactory
{
return [TyphoonDefinition withClass:[SwordFactory class]];
}
- (id)blueSword
{
return [TyphoonDefinition withClass:[Sword class] initialization:^(TyphoonInitializer* initializer)
{
initializer.selector = @selector(swordWithSpecification:);
//Specify type, because Obj-c runtime doesn't provide type-introspection for initializers
[initializer injectParameterNamed:@"specification" withValueAsText:@"blue" requiredTypeOrNil:[NSString class]];
} properties:^(TyphoonDefinition* definition)
{
definition.factory = [self swordFactory];
}];
}
Let's say we have an assembly where some of the components will conform to a protocol. At at run-time concrete realization of these protocols will be different depending on, for example, Production vs Test environments.
- Typhoon allows you to group related components together under a TyphoonAssembly sub-class.
- Typhoon allows you to extract a protocol or define a base-class for components that will change depending on circumstances.
Here's an example:
We create an assembly that’s going to use the interface from another assembly
@interface UIAssembly : TyphoonAssembly
//Typhoon will fill these in at runtime
@property(nonatomic, strong, readonly) VBNetworkComponents* networkComponents;
@property(nonatomic, strong, readonly) VBPersistenceComponents* persistenceComponents;
- (id)rootViewController;
- (id)signUpViewController;
- (id)storeViewController;
@end
@implementation UIAssembly
- (id)rootViewController
{
return [TyphoonDefinition withClass:[RootViewController class] properties:^(TyphoonDefinition* definition)
{
definition.scope = TyphoonScopeSingleton;
}];
}
- (id)signUpViewController
{
return [TyphoonDefinition withClass:[SignUpViewController class] initialization:^(TyphoonInitializer* initializer)
{
initializer.selector = @selector(initWithClient:view:);
[initializer injectWithDefinition:[_networkComponents signUpClient]];
[initializer injectWithDefinition:[self signUpView]];
}];
}
- (id)signUpView
{
return [TyphoonDefinition withClass:[SignUpView class]];
}
- (id)storeViewController
{
//etc. . .
}
@end
Now we provide a realization of these components as follows:
@implementation NetworkComponents
- (id)signUpClient
{
return [TyphoonDefinition withClass:[SignUpClientDefaultImpl class] properties:^(TyphoonDefinition* definition)
{
definition.parent = [self abstractClient];
}];
}
- (id)storeClient
{
return [TyphoonDefinition withClass:[StoreClientDefaultImpl class] properties:^(TyphoonDefinition* definition)
{
definition.parent = [self abstractClient];
[definition injectProperty:@selector(storeDao) withDefinition:[_persistenceComponents storeDao]];
}];
}
- (id)abstractClient
{
return [TyphoonDefinition withClass:[ClientBase class] properties:^(TyphoonDefinition* definition)
{
[definition injectProperty:@selector(serviceUrl) withValueAsText:@"${client.serviceUrl}"];
[definition injectProperty:@selector(networkMonitor) withDefinition:[self networkMonitor]];
[definition injectProperty:@selector(allowInvalidSSLCertificates) withValueAsText:@"${client.allowInvalidSSLCertificates}"];
[definition injectProperty:@selector(logRequests) withValueAsText:@"${client.logRequests}"];
[definition injectProperty:@selector(logResponses) withValueAsText:@"${client.logResponses}"];
}];
}
- (id)networkMonitor
{
return [TyphoonDefinition withClass:[Reachability class] initialization:^(TyphoonInitializer* initializer)
{
initializer.selector = @selector(reachabilityForInternetConnection);
}];
}
@end
Now we can create a container as follows:
TyphoonComponentFactory* factory = [[TyphoonBlockComponentFactory alloc] initWithAssemblies:@[
[UIAssembly assembly],
[NetworkComponents assembly],
[PersistenceComponents assembly]
]];
StoreViewController* viewController = [(UIAssembly*) factory storeViewController];
#Summary
- Block-style assembly is a great way of providing powerful dependency injection features at the same time as taking advantage of all of your IDEs code-completion, validation and refactoring tools.
- Guice (and Spring)-style auto-wiring is supported, for simple cases. For more complex cases this style of assembly is recommended.
#Where to now?
- Additional ways to resolve components are described in: Using-assembled-components
- Check out Autowiring
- See: Configuration-Management-&-Testing
- Try the sample application.
- Read the API Docs.
Something still not clear? How about posting a question on StackOverflow.
Get started in two minutes.
Get familiar with Typhoon.
- Types of Injections
- What can be Injected
- Auto-injection (Objective-C)
- Scopes
- Storyboards
- TyphoonLoadedView
- Activating Assemblies
Become a Typhoon expert.
For contributors or curious folks.