-
Notifications
You must be signed in to change notification settings - Fork 268
Types of Injections
In this section. . .
- Initializer / Class Method Injection
- Property Injection
- Method Injection
- Injection call-backs
- Injections with run-time arguments
- Injecting collections
- Factory Definitions
- Circular dependencies
- Abstract and Base Definitions
All injections are defined within one or more TyphoonAssembly sub-classes.
@interface YourApplicationAssembly : TyphoonAssembly
- (Knight *)knight
- (id<Quest>)quest;
//etc. . .
@end
Key Concept: Before [activation](Activating Assemblies) each method returns a TyphoonDefinition
. After activation we'll use the same interface to return built instances. You can declare the return type as the type being built (Objective-C) or AnyObject
(Swift).
(John Reid of http://qualitycoding.org, and others sometimes call this 'constructor injection')
- (Knight *)basicKnight
{
return [TyphoonDefinition withClass:[Knight class]
configuration:^(TyphoonDefinition* definition) {
[definition useInitializer:@selector(initWithQuest:)
parameters:^(TyphoonMethod *initializer) {
[initializer injectParameterWith:[self defaultQuest]];
}];
}];
}
- (id<Quest>)defaultQuest
{
return [TyphoonDefinition withClass:[CampaignQuest class]];
}
Injections can be specified as arguments to an initializer, eg 'initWithQuest:' or class method, eg 'knightWithQuest:'.
- Its possible to use an empty (no-args) creation method. Eg [Knight knight]
- If no initializer is specified, then [[alloc] init] is implied.
NB: because the Objective-C runtime does not provide type information about the parameters of initializers, Typhoon cannot inject initializer parameters by type for you. If you want to inject by type then you must use property injection (see below). When using initalizer injection you must supply as many injections as there are initailizer parameters explicitly by using -injectParameterWith:
.
- (Knight *)cavalryMan
{
return [TyphoonDefinition withClass:[CavalryMan class]
configuration:^(TyphoonDefinition *definition) {
[definition injectProperty:@selector(quest) with:[self defaultQuest]];
[definition injectProperty:@selector(damselsRescued) with:@(12)];
}];
}
NB: Property injection can be done [by type](What can be Injected#by-type) or with Auto Injection macros.
Methods with one or more parameters can be injected.
- (Knight *)knightWithMethodInjection
{
return [TyphoonDefinition withClass:[Knight class]
configuration:^(TyphoonDefinition *definition) {
[definition injectMethod:@selector(setQuest:andDamselsRescued:)
parameters:^(TyphoonMethod *method) {
[method injectParameterWith:[self defaultQuest]];
[method injectParameterWith:@321];
}];
}];
}
NB: You can declare method injections in the order that you'd like them to occur. See also: [#Injection call-backs]
Typhoon allows you to specify a method to be called before and after injections.
//Useful for 3rd party frameworks, otherwise just use init
definition.performBeforeInjections = @selector(applyAppTheme)];
//Intializer injection has the advantage that we can assert required
//state before proceeding, while property injection doesn't. But we
//can work around this with the following:
definition.performAfterInjections = @selector(checkStateAfterBuilt)];
//Call-back methods can also have arguments, eg:
definition.performAfterInjections = @selector(registerWithSubscriber:)
parameters:^(TyphoonMethod *method) {
[method injectParameterWith:[self eventSubscriber];
}];
//. . this behaves just like a method injection that is guaranteed
//to be called last. Although, not that for ordinary method injections
//that order is also followed.
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 do the following:
#import Typhoon.h
- (void)typhoonWillInject
{
}
- (void)typhoonDidInject
{
}
Run-time arguments allow defining a factory on the assembly interface. Here is an example:
- (UserDetailsController *)userDetailsControllerForUser:(User *)user
{
return [TyphoonDefinition withClass:[UserDetailsViewController class]
configuration:^(TyphoonDefinition *definition) {
[definition useInitializer:@selector(initWithPhotoService:user)
parameters:^(TyphoonMethod *initializer) {
[initializer injectParameterWith:[self photoService];
[initializer injectParameterWith:user];
}];
}];
}
We can obtain a UserDetailsViewController with both the static and runtime dependencies as follows:
User* aUser = self.selectedUser;
UserDetailsViewController* detailsController =
[assembly userDetailsControllerForUser:aUser];
- When to use runtime arguments.
- Runtime arguments can be propagated through to dependencies.
- Runtime arguments can be used with circular dependencies.
- Runtime arguments can be combined with Backing-a-TyphoonAssembly-with-a-factory-protocol.
- Run-time arguments must always be an object. Primitives are not possible, however they can be [wrapped into NSValue](wrap primitive values into NSValue).
- Run-time arguments must be passed in to your definitions exactly as-is. Its not possible to manipulate the argument by calling any methods on it.
This is completely optional, but reading about how typhoon assemblies work 'under the hood' makes for interesting reading, and should give some insights into these limitations.
Typhoon will inject collections - NSArray, NSSet, NSDictionary and their mutable counterparts using the following special treatment:
- References to other TyphoonDefinitions are resolved to the built instances.
-
self
or any other collaboratingTyphoonAssembly
will result in Typhoon [injecting itself](What can be Injected#injecting-typhoon-itself). - Everything else is passed through as is.
Example:
- (Knight *)knightWithCollections
{
return [TyphoonDefinition withClass:[CavalryMan class]
configuration:^(TyphoonDefinition *definition) {
[definition injectProperty:@selector(favoriteDamsels) with:@[
@"Mary",
@"Mary"
]];
[definition injectProperty:@selector(friends) with:
[NSSet setWithObjects:[self knight], [self anotherKnight], nil]];
[definition injectProperty:@selector(friendsDictionary) with:@{
@"knight" : [self knight],
@"anotherKnight" : [self anotherKnight]
}];
}];
}
See also: What can be injected
Sometimes its necessary to register a definition that produces other definitions. For example a legacy singleton that produces objects you'd like to inject into other classes in your app.
- (SwordFactory *)swordFactory
{
return [TyphoonDefinition withClass:[SwordFactory class]];
}
- (Sword *)blueSword
{
return [TyphoonDefinition withFactory:[self swordFactory]
selector:@selector(swordWithSpecification:)
parameters:^(TyphoonMethod *factoryMethod) {
[factoryMethod injectParameterWith:@"blue"];
}];
}
Sometimes you wish to define objects 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.
Typhoon supports circular dependencies in properties and methods.
- (SettingsController *)appSettingsController
{
return [TyphoonDefinition withClass:[AppSettingsController class]
configuration:^(TyphoonDefinition* definition)
{
[definition useInitializer:@selector(initWithSoundManager:settingsView:)
parameters:^(TyphoonMethod* initializer)
{
[initializer injectParameterWith:[_kernel soundManager]];
[initializer injectParameterWith:[self appSettingsView]];
}];
[definition injectProperty:@selector(title) with:@"Settings"];
}];
}
- (SettingsView *)appSettingsView
{
return [TyphoonDefinition withClass:[AppSettingsView class]
configuration:^(TyphoonDefinition* definition)
{
[definition injectProperty:@selector(delegate) with:
[self appSettingsController]];
}];
}
If you wish to encapsulate configuration that will be shared among a number of derived definitions, you can do so as follows. This will avoid unnecessary repetition and verbosity.
Define the base definition:
- (ClientBase *)abstractClient
{
return [TyphoonDefinition withClass:[ClientBase class]
configuration:^(TyphoonDefinition* definition) {
[definition injectProperty:@selector(serviceUrl) with:[
NSURL URLWithString:@"https://zaps.com/serviceRequest"]];
[definition injectProperty:@selector(networkMonitor) with:([self internetMonitor])];
[definition injectProperty:@selector(allowInvalidSSLCertificates) with:@(YES)];
[definition injectProperty:@selector(logRequests) with:@(YES)];
[definition injectProperty:@selector(logResponses) with:@(YES)];
}];
}
And now derive from it as follows:
- (StoreClient *)storeClient
{
return [TyphoonDefinition withParent:[self abstractClient]
class:[StoreClient class]
configuration:^(TyphoonDefinition* definition) {
definition.scope = TyphoonScopeWeakSingleton;
//More config specific to the sub-class.
[definition injectProperty:@selector(registered) with:@(NO)];
}];
}
- From its parent, a definition will inherit the initializer, property injections, method injections and scope. Any of these can be overridden.
- Parents can be chained ad infinitum.
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.