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

Manually created assembly looses it's factory reference #551

Open AlexeyPoldeo opened 7 years ago

AlexeyPoldeo commented 7 years ago

Hello!

I'm newbie to Typhoon and hitting a lot of stupid problems on ObjC...

I prefer to create assemblies manually because using layered architecture and need to create new assembly for each user layer. It's not possible to use automatic assemblies generation because I have to inject some user specific information into the assembly.

Everything works fine until the end of the method. After that any attempt to instantiate new object causes "method not found" crash.

I did some research and found that manually activated assemblies become instances of TyphoonAssemblyAccessor and later loosing reference to fabric property. This property resets to nil because declared as weak. It makes the assembly unusable anywhere outside the method scope where is was created because it immediately looses it's fabric.

I've figured out that the only way to keep this reference alive is to patch the sources (make it strong) or to make the assembly default.

This definitely is something that I do not want to do.

Please help.

PS: Please do not suggest to switch to Swift or use plist assemblies creation.

alexgarbarev commented 7 years ago

Can you provide your usage code?

AlexeyPoldeo commented 7 years ago
@interface AppAssembly : TyphoonAssembly

+ (AppAssembly *) create {

    AppAssembly *assembly = [[AppAssembly new] activated];

    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];

    [assembly inject:[userDefaults rac_channelTerminalForKey:@"currentUserId"] withSelector:@selector(currentUserIdDefault)];

    return assembly;
}
AlexeyPoldeo commented 7 years ago

For test purposes I've put [AppAssembly create] into viewDidLoad and inside this method everything works fine. But In the other method (button press callback) I can see reference to AppAssembly since it's retained, but fabric and assembly properties are nil.

I'm using current version of Typhoon installed using Pods.

AlexeyPoldeo commented 7 years ago

When I checking assemblies created in PocketForecast sample app, I see that it has TyphoonBlockComponentFactory instance class. Mine's are TyphoonAssemblyAccessor.

I'm really confused...

etolstoy commented 7 years ago

Hi, @AlexeyPoldeo ! I think that the problem is in the way you are trying to use Typhoon.

I prefer to create assemblies manually because using layered architecture and need to create new assembly for each user layer.

When you create multiple assemblies manually (by calling [[AppAssembly new] activated] each time), you don't share dependency graph between them. It means, that if you have a singleton object in AppAssembly, this instance won't be accessible in your second assembly.

It's not possible to use automatic assemblies generation because I have to inject some user specific information into the assembly.

Well, this is possible but this approach doesn't belong to best practices. Consider assemblies as simple factories without any internal state. They are just proxies to access an underlying pool of created dependencies.

AlexeyPoldeo commented 7 years ago

With all my respect, but I see too many limitations in comparison to Dagger 2. It's possible to name it as features, but they are not...

Senior developers, who are smart enough to learn how to use Typhoon, do not need to put configuration to plist file to simplify their work. They want to get some tool to manage complicated object graphs and layer fabrics.

jasperblues commented 7 years ago

@AlexeyPoldeo If you want to create assemblies manually this is no problem, however the way you tried is not the correct way for Typhoon. What you need to do is:

Example

@interface UIAssembly : TyphoonAssembly

// Typhoon will automatically proxy the two collaborating assemblies 
// (NetworkComponents and TyphoonAssembly<PersistenceComponents>) here.
@property(nonatomic, strong, readonly) NetworkComponents* networkComponents;

// Collaborating assemblies can be backed by a protocol. We declare 
// type using TyphoonAssembly<FactoryProtocol> syntax to tell Typhoon
// that this is a collaborating assembly. In the app's own classes no further
// coupling to Typhoon is necessary, and we may declare a property as type
// id<FactoryProtocol>, avoiding your classes from being aware of Typhoon.
@property(nonatomic, strong, readonly) TyphoonAssembly<PersistenceComponents> persistenceComponents;

// Local components that require components from collaborating assemblies ... 
- (RootViewController *)rootViewController;

- (SignUpViewController *)signUpViewController;

- (StoreViewController *)storeViewController;
@end

We declare a property of type NetworkComponents, like this: @property(nonatomic, strong, readonly) NetworkComponents* networkComponents;

And then to activate you just call:

UIAssembly *uiAssembly = [[UIAssembly new] activate];

Typhoon will automatically wire in the NetworkComponents assembly. If you wish to override this with a sub-class or anything else that fulfills the contract you can use:

UIAssembly *uiAssembly = [[UIAssembly new] 
    activateWithCollaboratingAssemblies:@[
        [TestNetworkComponents new], 
        [PersistenceComponents new]];

Typhoon for Objective-C is nothing like Dagger 2. The way that it works is:

This is a very Objective-C-esque approach, which at the time Typhoon was written is what people were after.

More info on assembly modularization is available here: https://github.com/appsquickly/Typhoon/wiki/Modularizing-Assemblies

Typhoon for Swift is somewhat more like Dagger in that it uses compile-time rather than runtime processing.

Thank you for trying Typhoon and let me know if you need further assistance setting up.

AlexeyPoldeo commented 7 years ago

Thank you for details, Jasper, but I didn't got what I did wrong...

I've created assembly and it's fabric died after finishing the enclosing method.

Example:

@property (nonatomic) TestAssemply *assembly;

It it bug or it's by design?

jasperblues commented 7 years ago

Can you send me a sample project? Sounds like a bug or user error of some sort. Likely the latter. Your code above looks fine - sample project will help me understand better.

nicoabie commented 5 years ago

@jasperblues @alexgarbarev @etolstoy I'm attaching a simple project where I believe this issue is replicated. It contains a test that asserts if the assembly injected by property is the same as the assembly passed to the initializer. (using the former works as expected) BugFactory.zip

Thanks in advance.