appsquickly / typhoon

Powerful dependency injection for Objective-C ✨✨ (https://PILGRIM.PH is the pure Swift successor to Typhoon!!)✨✨
https://pilgrim.ph
Apache License 2.0
2.7k stars 269 forks source link

TyphoonInstancePostProcessor protocol implemented object injections. #579

Closed sdkdimon closed 6 years ago

sdkdimon commented 6 years ago

Cant't inject into TyphoonInstancePostProcessor protocolled object.

First Case. Have an object ModuleContainer that confirms TyphoonInstancePostProcessor protocol

@interface ModuleContainer : NSObject <TyphoonInstancePostProcessor>
@end

@implementation ModuleContainer

- (void)addModule:(id)module
{
//Some logic
}

#pragma mark - TyphoonInstancePostProcessor

- (id)postProcessInstance:(id)instance
{
    if ([instance conformsToProtocol:@protocol(ModuleInput)])
    {
        [self addModule:instance];
    }

    return instance;
}

@end

Assembly

- (ModuleContainer)moduleContainer
{
    return [TyphoonDefinition withClass:[ModuleContainer class]];
}

- (SomeClass *)someClass
{
    return [TyphoonDefinition withClass:[SomeClass class] configuration:^(TyphoonDefinition *definition) {
        [definition injectProperty:@selector(moduleContainer) with:[self moduleContainer]]; 
    }];
}

Fall with exception 'No component matching id 'moduleContainer'.

Second case. Even we create some provider class TyphoonInstancePostProcessor that confirms TyphoonInstancePostProcessor protocol, Typhoon not allow inject something in that class. App will crash with random exceptions with something about parent injections error message.

@interface InstancePostProcessor : NSObject <TyphoonInstancePostProcessor>

@property (strong) ModuleContainer *moduleContainer;

@end

@implementation InstancePostProcessor

#pragma mark - TyphoonInstancePostProcessor

- (id)postProcessInstance:(id)instance
{
    if ([instance conformsToProtocol:@protocol(ModuleInput)])
    {
        [self.moduleContainer addModule:instance];
    }

    return instance;
}

@end

@interface ModuleContainer : NSObject 

- (void)addModule:(id)module;

@end

@implementation ModuleContainer

- (void)addModule:(id)module
{
//Some logic
}

@end

Assembly

- (ModuleContainer)moduleContainer
{
    return [TyphoonDefinition withClass:[ModuleContainer class]];
}

- (InstancePostProcessor *)someClass
{
    return [TyphoonDefinition withClass:[InstancePostProcessor class] configuration:^(TyphoonDefinition *definition) {
        [definition injectProperty:@selector(moduleContainer) with:[self moduleContainer]]; 
    }];
}

So exist any solution to inject at TyphoonInstancePostProcessor protocol implemented object? Or i may use old good known singleton (shared instance)?

jasperblues commented 6 years ago

Can you please attach failing code for the first case? Or show how Typhoon was bootstrapped?

sdkdimon commented 6 years ago

Typhoon fails in TyphoonComponentFactory, at method

- (id)componentForKey:(NSString *)key args:(TyphoonRuntimeArguments *)args
{
    if (!key) {
        return nil;
    }

    [self loadIfNeeded];

    TyphoonDefinition *definition = [self definitionForKey:key];
    if (!definition) {
        [NSException raise:NSInvalidArgumentException format:@"No component matching id '%@'.", key];
    }

    return [self newOrScopeCachedInstanceForDefinition:definition args:args];
}

Full exception log

InstancePostProcessor[1058:677052] *** Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: 'No component matching id 'instancePostProcessor'.'

I am create repo with sample code for clarity, with first case crash https://github.com/sdkdimon/TyphoonInstancePostProcessor.

alexgarbarev commented 6 years ago

Hey @skywinder, you are right. Currently Typhoon is designed to not register infrastructure components with global definitions registry. Here is a code responsible for that:


- (void)registerDefinitionWithFactory
{
    if ([self definitionIsInfrastructureComponent]) {
        [self registerInfrastructureComponentFromDefinition];
    }
    else {
        LogTrace(@"Registering: %@ with key: %@", NSStringFromClass(_definition.type), _definition.key);
        [_componentFactory addDefinitionToRegistry:_definition];
    }
}
alexgarbarev commented 6 years ago

I suggest you to factory signletone (used to resolve UI) inside your PostProcessor to access other dependencies inside your PostProcessor. Here is useful category to access objects from shared factory (or inject dependencies):

NSObject+TyphoonDefaultFactory.h

@interface NSObject (TyphoonDefaultFactory)

/**
 * Creates new instance of given class using default Typhoon factory
 * It uses registered TyphoonDefinition if exists and tries to register
 * new definition, if property auto-injection specified
 * */
+ (instancetype)newUsingTyphoon;

- (void)injectUsingTyphoon;

+ (instancetype)fromTyphoon;

@end

NSObject+TyphoonDefaultFactory.m

#import <Typhoon/TyphoonDefinition.h>
#import "NSObject+TyphoonDefaultFactory.h"
#import "TyphoonComponentFactory.h"
#import "TyphoonComponentFactory+InstanceBuilder.h"
#import "Typhoon+Infrastructure.h"
#import "CCMacroses.h"

@implementation NSObject (TyphoonDefaultFactory)

+ (BOOL)isAutoDefinition:(TyphoonDefinition *)definition
{
    NSString *key = [definition key];
    return [key hasPrefix:[NSString stringWithFormat:@"%@_", [self class]]];
}

+ (instancetype)newUsingTyphoon
{
    TyphoonComponentFactory *defaultFactory = [TyphoonComponentFactory factoryForResolvingUI];

    if (!defaultFactory) {
        DDLogWarn(@"Default Factory doesn't exist yet, creating object using 'new' without dependencies.");
        id result = [self new];
        return result;
    }

    NSArray *definitions = [defaultFactory allDefinitionsForType:[self class]];

    if (definitions.count == 1 && [self isAutoDefinition:definitions.firstObject]) {
        definitions = @[];
    }

    id result = nil;

    switch (definitions.count) {
    case 0:
        result = [self new];
        [defaultFactory inject:result];
        break;
    case 1:
        result = [defaultFactory componentForType:[self class]];
        break;
    default:
        result = [self new];
        DDLogWarn(@"Found more than one definition for class: %@", self);
        break;
    }

    return result;
}

- (void)injectUsingTyphoon
{
    TyphoonComponentFactory *defaultFactory = [TyphoonComponentFactory factoryForResolvingUI];
    [defaultFactory inject:self];
}

+ (instancetype)fromTyphoon
{
    TyphoonComponentFactory *factory = [TyphoonComponentFactory factoryForResolvingUI];
    return [factory componentForType:self];
}

@end

Then try this in your PostProcessor:

#import "NSObject+TyphoonDefaultFactory.h"
#import "SomeClass.h"

@interface ModuleContainer : NSObject <TyphoonInstancePostProcessor>
@property (nonatomic) SomeClass *someDependency;
@end

@implementation ModuleContainer

- (void)injectDependenciesIfNeeded
{
   if (self.someDependency) {
      return;
   }
   self.someDependency = [SomeClass fromTyphoon];
}

- (void)addModule:(id)module
{
    [self injectDependenciesIfNeeded];
//Some logic
}
sdkdimon commented 6 years ago

Thanks for workaround, but its a bit too complex for me. My solution is to forward postProcessInstance message to singleton object whose definition is not infrastructure component.

@interface InstancePostProcessor : NSObject <TyphoonInstancePostProcessor>

@end

@implementation InstancePostProcessor

#pragma mark - TyphoonInstancePostProcessor

- (id)postProcessInstance:(id)instance
{
    [[ModuleContainer sharedContainer] postProcessInstance];
    return instance;
}

@end

So i can inject dependencies in ModuleContainer, or ModuleContainer itself as dependency without troubles, as normal definition.

@interface ModuleContainer : NSObject

+ (id)sharedContainer;
- (id)postProcessInstance:(id)instance;

@end

@implementation ModuleContainer

+ (id)sharedContainer
{
    static Container *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

- (void)addModule:(id)module
{
//Some logic
}

- (id)postProcessInstance:(id)instance
{
    if ([instance conformsToProtocol:@protocol(ModuleInput)])
    {
        [self addModule:instance];
    }

    return instance;
}

@end