简体   繁体   中英

Injecting same property twice with Typhoon

I am planning to introduce a dependency injection framework into a newly created project, and have found Typhoon which looks really nice (except for having to use strings when referencing methods and properties in Swift). It generally seems to fit well to my app, but I have a problem with a construction I made for handling network requests.

All my View Controllers (that requires network access) inherit from a ServiceViewController which I use for lazily initializing services (a service in this context is a class for handling network communication):

class ServiceViewController {    
  var services = [BDService]()//a list of (lazily initialized) services used by the the view controller

  //create a service of the specified type (or return from the existing list if it has already been created)
  func getService<T : BDService>() -> T {
    for service in services {
        if service is T {
            return service as T
        }
    }
    let klass : T.Type = T.self
    let newService : T = klass()//don't ask - due to Swift bug: http://stackoverflow.com/questions/27336607
    services.append(newService)
    return newService
  }

  //tell all initialized services to cancel pending requests
  func cancelAllPendingTasks() {
    services.each { $0.cancelPendingTasks() }
  }

  //inform whether a request has been sent and we are still waiting for a response
  func hasPendingTasks() -> Bool {
    return services.any { $0.pendingTasks.count > 0 }
  }
}

....

//usage from MyViewController:
let accountService : BDAccountService = super.getService()
/* do something on accountService */
...
let postingService : BDPostingService = super.getService()
/* do something on postingService */
...
super.cancelAllPendingTasks() //e.g. if user clicks back button

The last methods, cancelPendingTasks and hasPendingTasks , are the reason I want to keep this architecture. By having all services for the ViewController in a list, we can ensure that we do not forget to cancel pending tasks on one of the services.

Now, when I want to introduce Typhoon, I can inject all my services in the initializer:

public dynamic func myViewController() -> AnyObject {
    return TyphoonDefinition.withClass(MyViewController.self) {
        (definition) in
        definition.useInitializer("initWithAccountService:postingService:") {//(forgive me if I wrote this wrong)
            (initializer) in
            initializer.injectParameterWith(self.accountService())
            initializer.injectParameterWith(self.postingService())
        }
    }
}

There are two downsides of this approach:

  1. Cancelling all tasks (or similar) would require to make a call to all services
  2. The services are no longer lazily initialized whenever they are needed

Ad 1) One way to handle the cancelAllPendingTasks() now would be to add the two services to the ServiceViewController.services list from the initializer. Then the cancelAllPendingTasks() would be able to access all services. But I am looking for a way to avoid having to do this, such that adding a service later does not require to "remember to do this and that"

Ad 2) I don't see a way to do this with dependency injection. But I don't see this as a big issue, as the memory footprint of the services are probably not so noteworthy :-)

So my question is: Can I in some way inject the services into the constructor and into the list of services at the same time?

Edit: When I discovered from @JasperBlues' answer that it is easy to inject the same object twice into property and initializer, my second concern gets more attention for me: If we inject a bunch of services into the initializer, but forget to inject the exact same services into the array (eg we forget one of them), we get very hard-to-find bugs, because nobody will ever discover it before the hasPendingTasks method some day returns the wrong result, and something hangs because of that. This was an impossible scenario with my original design, but if we should inject the services twice, we suddenly geta duplication that is error-prone.

Typhoon allows you to optionally use initializer injection (or not) along with any of the following:

  • Property injection
  • Method injection (with one or more parameters).
  • Other configuration such as specifying a callback method to invoke after injections have happened or setting the scope.

Its no problem to use property injection if that property was already set via an initializer.

Method injections can be invoked multiple times for the same method. (For example a method that collects names of log directories).

The citiesListController in the Typhoon Sample Application shows an example of mixing initializer injection with property injection. The user guide shows how to do method injections, set the scope or invoke a callback method after injections occur.

After working more on my problem and getting Typhoon implemented, I have some thoughts to share:

  1. It is very easy to inject an object twice:

     public dynamic func myViewController() -> AnyObject { return TyphoonDefinition.withClass(MyViewController.self) { $0.useInitializer("initWithAccountService:postingService:") { $0.injectParameterWith(self.accountService()) $0.injectParameterWith(self.postingService()) } $0.injectProperty("services", with:[self.accountService(), self.postingService()]) } } 

    This injects the same objects twice, and does not create two instances of accountService and postingService. This was not clear to me before reading @JasperBlues' answer.

  2. My concern about duplicated injections of accountService and postingService is actually very easy to overcome. I just deleted the initializer again and returned to using the getService<T>() method. Therefore, my assembly now looks like this:

     public dynamic func myViewController() -> AnyObject { return TyphoonDefinition.withClass(MyViewController.self) { $0.injectProperty("services", with:[self.accountService(), self.postingService()]) } } 

    The above populates the services array, and therefore the getService<T>() method will return the proper injected objects from this array.

  3. The third concern about lazy initialization can actually still be overcome by simply not injecting the services, but create them directly in the code as previously. After all, since it is possible to inject these services, it is still easy to test them. Still, it would probably be better to inject them due to other concerns such as having dependencies specified in one place, namely the Assembly :-)

But after all, it has been a good learning experience, and Typhoon is going into my project now :-)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM