App Development

Global State and Dependency Injection on iOS

Global State and Dependency Injection Blog Featured Image IT-510x296

At WillowTree we manage a lot of application code. Much of it has evolved alongside an application over the course of years, and some of it has been developed outside of our company. We see a lot of different patterns and libraries.

This post examines dependency injection and best practices regarding using global state, starting with the JInjector library.

JInjector

One library we’ve used and seen used is JInjector which bills itself as a “dependency injection library.” Although it allows for multiple injectors to be built, the common usage is to register an instance of a class with the default injector and then retrieve it later for usage.

For instance, on app setup you might set up your API client:

APIClient *client = [[APIClient alloc] initWithAccessToken:@"abcdef1234567890"];
[[JInjector defaultInjector] setObject:client forClass:[APIClient class]];

and then when an instance needs it, it could retrieve it for use:

APIClient *client = JInject(APIClient);
[client doStuff];

(Since JInjector was used in some older projects, those examples are in Objective-C. I’ve described how Swift impacts it below.)

This technique is convenient for a few reasons.

  1. It allows any calling code to gain access to registered services.
  2. It allows calling code access without knowing about underlying implementation details, and even allows implementations to be swapped out dynamically.

But on inspection, the default injector looks an awful lot like global state. And the first point of convenience above looks like singleton-like behavior. The convenience promised by this feels a bit like accumulating tech debt that will need to be refactored away once our project reaches a certain size.

If dependency injection is touted as the solution to singletons and global state, then what’s going on?

I think JInjector is poorly described. It is not a dependency injection library at all, but is something more akin to a service locator library.

Global State

Back up. Why does global state need a solution? Why is it bad?

Primarily, mutable global state creates “action at a distance.” It is non-obvious what code is changing the global state when anyone can touch it. This can create situations where one part of your application creates bugs in another in ways that are difficult to identify and reproduce.

Secondarily, much global state is in the form of singletons or “singletons in sheep’s clothing” like with JInjector. Those patterns make the assumption that the global item is the only one used or is more important than others.

Finally, usage of global state often relies on information outside of what the code can enforce. In our example above, extracting an APIClient via JInject(APIClient) required out-of-band knowledge that one had been registered with JInjector before it was used.

In fact, when you use JInjector with Swift, the language itself helps shed light on the problem. Extraction of the APIClient with code equivalent to the JInject macro looks like this:

let client = JInjector.defaultInjector().objectForClass(APIClient.classForCoder()) as? APIClient

The strictness of Swift has made clear to us that we have no guarantee that an APIClient exists, or that the instance registered with JInjector was of an appropriate type. Our client variable is of type APIClient?, and if we have made bad assumptions about initialization we will have problems at runtime.

Those situations are often not seen as problems early on because they are manageable in small projects. But as a project grows the problems compound, and early assumptions often prove false. A popular example is the case of Dropbox moving to a multiple user architecture.

While it’s important not to prematurely optimize for anything, including refactors that may slow down development velocity, in most cases using real dependency injection is just as quick to develop with and yields more flexible results. Of course, global state might not be bad on a case by case basis. Your project might not grow to a place where it’s an issue. As our iOS Developer Matt Baranowski said, in the case of small apps, “I’m ok with some global state.”

Dependency Injection

“‘Dependency Injection’ is a 25-dollar term for a 5-cent concept.” - James Shore

Dependency injection is pushing dependencies into code, rather than having the code pull dependencies from another source (most often global state).

The most common implementations of dependency injection are described well by objc.io.

The 5¢ Concepts

Constructor injection looks like this:

class Foo {
  let dependency: Dependency
  init(dependency: Dependency) {
    self.dependency = dependency
  }
  func doWork() {
    dependency.doWork()
  }
}


let f = Foo(dependency: myDep)
f.doWork()

while property injection may look like:

class Foo {
  var dependency: Dependency!
  func doWork() {
    dependency.doWork()
  }
}


let f = Foo()
f.dependency = myDep
f.doWork()

and method injection would look like:

class Foo {
  func doWork(dependency: Dependency) {
    dependency.doWork()
  }
}


let f = Foo()
f.doWork(myDep)

In all cases, it’s important to note that Foo had its Dependency pushed to it, and Foo didn’t have to go fishing for one in global state.

Problems and Solutions

Let’s examine some concrete pain points with dependency injection, what they tell us, and how we might solve them.

blog-post-image global-state IT

A common complaint is that with constructor injection, your constructors grow to an unmaintainable nightmare. For instance, the following Worker class has a lot of dependencies:

class Worker {
  init(api: APIClient,
       cache: DataCache,
       dataSource: DataSource,
       session: SessionInformation,
       currentUser: User,
       kitchenSink: KitchenSink)
  {
     ...
  }
}

If this pattern is all over your repository then it may indicate an architectural concern. While some top level objects may have many dependencies in practice, it is likely that classes which do so do not adhere to the single responsibility principle. The best solution in that case is to refactor your code to be more modular.

Sometimes, though, parts of your app may need to aggregate all of these pieces of data. In those cases, a viable approach is to use an aggregated “context” type.

protocol Context {
  var api: APIClient { get }
  var cache: DataCache { get }
  var dataSource: DataSource { get }
  var session: SessionInformation { get }
  var currentUser: User { get }
  var kitchenSink: KitchenSink { get }
}


class Worker {
  init(context: Context) {
    ...
  }
}

This is analogous to creating smaller versions of the global scope, with some of the same pros and cons. A benefit is that the impact of the local context scope is felt only where passed explicitly. Every context also sits side by side with every other context, with no singleton-like preference for a specific context expressed in code.

This is also analogous to passing around individual service locators as a dependency. Rather than using JInjector’s default injector, an individual context-specific injector could be used. I still advocate using an explicit type like our protocol Context above, though. It is self-documenting with its variable names, and we skirt any initialization order trouble by having the language enforce that our dependencies exist when we need them.

Problem: Passing Dependencies Down Multiple Levels

Another complaint is that you may have a parent object that does not directly care about a dependency, but a child object it manages does. In this case, the parent object needs to be aware of the dependency only for its child.

For instance, the Parent instance below doesn’t care about the KitchenSink except to inject it into its children.

class Parent {
  init(kitchenSink: KitchenSink) {
    ...
  }


  func doStuffWithoutKitchenSink() {
    ...
  }


  func buildChild() -> Child {
    return Child(kitchenSink: self.kitchenSink)
  }
}


class Child {
  init(kitchenSink: KitchenSink) {
    ...
  }
}

This problem seems even worse when it’s not a directly managed object but something well down the graph that needs the dependency. Why should the Parent and intermediate objects need it if the Child can get it from the global state? Wouldn’t that approach be more decoupled?

This is arguably a problem only in interpretation. An object’s dependencies should be thought of as the union of all dependencies of the objects it manages. If a Parent can buildChild, that means the knowledge of child building is part of Parent inherently.

But if Parent is actually more generic, not tied to this Child but to the idea of “some child,” we can reorganize our code to reflect that. Consider initializing the Parent with the knowledge of how to build a particular child:

class Parent {
  typealias ChildBuilder = ()->Child
  var buildChild: ChildBuilder
  init(buildChild: ChildBuilder) {
    self.buildChild = buildChild
  }
}


let p = Parent {
    return Child(kitchenSink: self.kitchenSink)
}

In this case, rather than relying on Parent storing the information for how to build a child as code, it stores the code as data. The code is now declaring our intent more clearly. The Parent's core responsibility was not to manage a KitchenSink, but to manage a Child. The intermediate details that felt irrelevant to the rest of the Parent code are now invisible to the Parent code - all without relying on Child needing to pull dependencies from the environment.

Problem: Storyboards

Constructor injection becomes impossible when using storyboards, as the system manages controller instantiation.

One solution we use for instantiating initial view controllers is to use factory methods, like this:

class KitchenSinkViewController: UIViewController {
  private static let storyboardName = "KitchenSinkViewScene"
  var kitchenSink: KitchenSink!
  public static func build(kitchenSink: KitchenSink) -> KitchenSinkViewController {  
      let bundle = NSBundle(forClass: KitchenSinkViewController.self)
      let storyboard = UIStoryboard(name: storyboardName, bundle: bundle)
      let controller = storyboard.instantiateInitialViewController() as! KitchenSinkViewController


      // Property injection:
      controller.kitchenSink = kitchenSink
      return controller
  }
}

That approach ensures that we are injecting our dependencies properly before the instance is used.

Performing segues gives us a natural point to use property injection to set up our controllers via UIViewController-prepareForSegue(:sender:):

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
  if let controller = segue.destinationViewController as? KitchenSinkViewController {
    controller.kitchenSink = self.kitchenSink
  }
}

But if the controllers are each used in multiple contexts, it might make sense to use a custom segue to set things up instead:

protocol KitchenSinker {
  var kitchenSink: KitchenSink { get set }
}
class KitchenSinkPropagatingSegue: UIStoryboardSegue {
    override func perform() {
        if let source = sourceViewController as? KitchenSinker,
           let destination = destinationViewController as? KitchenSinker {
             destination.kitchenSink = source.kitchenSink
        }
        super.perform()
    }
}

What Drives You to Global State?

When do you reach for JInjector or a singleton? What keeps you from injecting all of your dependencies?

Send your thoughts to ios@willowtreeapps.com and we’ll follow up!

Quickstart-Guide-to-Kotlin-Multiplatform

A Quick Start Guide to Kotlin Multiplatform

Kotlin Multiplatform, though still experimental, is a great up-and-coming solution...

Read the article