Skip to content

Types of Injections

Jasper Blues edited this page Feb 20, 2019 · 118 revisions

In this section. . .


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).

Initializer / Class Method Injection

(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:.


Property Injection

- (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.


Method Injection

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]


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
{
}

Injection with Run-time Arguments

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];

Limitations:

  • 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.


Injecting Collections

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 collaborating TyphoonAssembly 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

Factory Definitions

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"];
        }];
}

Circular Dependencies

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]];
    }];
}

Abstract and Base Definitions

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.