How we do iOS apps: Part 1 – Dependency Injection

Oct, 2, 2015 • Mike Seghers

Categories: Dependency Injection, iOS

This is the first post in a series where we describe how be build iOS apps at AppFoundry.

In this post we’ll explain Dependency Injection.

First steps

To see the advantages of Dependency Injection, we need a small example. Let’s say we are an App Development company, and we want to create iOS apps. To do this, we first of all need a designer. A designer will create designs based on a set of requirements. A developer will then take these designs and craft them into an app. Let’s look at how we might have done this in 2012.

struct SkeumorphicDesigner {
  func createDesign(requirements: Requirements) -> Design {
    //Draws some boxes with stitches, applies colors
    //returns Design
 }
}

struct ObjectiveCDeveloper {
  func createApp(requirements: Requirements,
                       design: Design) -> App {
    //Does some code between [], applies the design using
    //InterfaceBuilder, returns App
}

We’ve now built a model for creating designs and developing apps. We still need to put these two guys together:

class AppFactory2012 {
  private let developer:ObjectiveCDeveloper
  private let designer:SkeumorphicDesigner

  init() {
    developer = ObjectiveCDeveloper()
    designer = SkeumorphicDesigner()
  }

  public func createApp(requirements:Requirements) -> App {
    let design = designer.createDesign(requirements)
    return developer.createApp(requirements, design: design)
  }
}

We can finally create an app. Here is how.

let appFactory = AppFactory2012()
let app = appFactory.createApp(requirements)

We succeeded in creating a simple model to create apps. Now let’s move on to 2013.

Code grows

In 2013, Apple released iOS 7. They decided we didn’t want skeumorphic designs anymore. Flat was the new cool kid in town. Here’s the code for our new FlatDesigner.

struct FlatDesigner {
  func createDesign(requirements: Requirements) -> Design {
    //Draws some flat boxes, applies less color, returns Design
 }
}

Since our AppFactory2012 is using a SkeumorphicDesigner, we need to create another AppFactory, let’s say AppFactory2013

class AppFactory2013 {
  private let developer:ObjectiveCDeveloper
  private let designer:FlatDesigner

  init() {
    developer = ObjectiveCDeveloper()
    designer = FlatDesigner()
  }

  public func createApp(requirements:Requirements) -> App {
    let design = designer.createDesign(requirements)
    return developer.createApp(requirements, design: design)
  }
}

Using this new factory would look something like this:

let appFactory = AppFactory2013()
let app = appFactory.createApp(requirements)

There hardly is a difference between AppFactory2012 and AppFactory2013. We basically copy-pasted and changed the type of the designer…

Code keeps growing!

Let’s move on to 2014 and beyond. Apple now wants us to write code in Swift, instead of ObjectiveC. In comes the SwiftDeveloper.

struct SwiftDeveloper {
  func createApp(requirements: Requirements,
                       design: Design) -> App {
    //Does some code without semi-colons, applies the
    //design using StoryBoards, returns App
}

Again, a new factory is needed, since the already existing factories have a so called tight coupling with their developers and designers.

class AppFactory2014 {
  private let developer:SwiftDeveloper
  private let designer:FlatDesigner

  init() {
    developer = SwiftDeveloper()
    designer = FlatDesigner()
  }

  public func createApp(requirements:Requirements) -> App {
    let design = designer.createDesign(requirements)
    return developer.createApp(requirements, design: design)
  }
}

And its usage:

let appFactory = AppFactory2014()
let app = appFactory.createApp(requirements)

Why this is bad

Needless to say, all this code copying-and-pasting leads to typical copy-paste problems: Fixing an issue in one factory, doesn’t immediately fix it in the other factories. This code will become unmaintainable pretty fast when we allow it to go on like this. So let’s take a look at where the problem lies, and what we can do about it.

Improving the situation

First of all we see that designers and developers have the same functions. That’s a good thing, we can formalize this fact using protocols. Since our developer and designer types already have these functions in place, we can just use an extension to make them adopt the specific protocols.

protocol Designer {
    func createDesign(requirements: Requirements) -> Design
}

protocol Developer {
    func createApp(requirements: Requirements,
                         design: Design) -> App
}

extension SkeumorphicDesigner : Designer {}
extension FlatDesigner : Designer {}
extension ObjectiveCDeveloper : Developer {}
extension SwiftDeveloper : Developer {}

These protocols allow us to refactor our factories. Let’s use these protocols as property types instead of their implementations. We could do this for all our different factories, for brevity we’ll show just the 2012 version.

class AppFactory2012 {
  private let developer:Developer
  private let designer:Designer

  init() {
    developer = ObjectiveCDeveloper()
    designer = SkeumorphicDesigner()
  }

  public func createApp(requirements:Requirements) -> App {
    let design = designer.createDesign(requirements)
    return developer.createApp(requirements, design: design)
  }
}

//This change is applied to all factories

Although a very small improvement has been made, we still have the same duplication as before. To really solve the problem, we have to get rid of the tight coupling which still exists in the initializers. They still require too much knowledge on how to create designers and developers. They don’t really care if you look at the rest of the code but we forced it on them.

Getting rid of implementation details

So the goal is to remove the tight coupling completely. To do this, we’ll have to remove all direct references to adoptions of Developer and Designer. One way we could do this is, instead of creating new instances, asking for them instead. A type can ask for instances of other types through its setters or through its initializer(s). We refactor the initializer like this:

//...
    init(developer: Developer, designer: Designer) {
        self.developer = developer
        self.designer = designer
    }
//...

If we apply this to all of our factories, we see that they now all have the exact same code! So we can get rid of all the duplication and just stick with one:

class AppFactory {
    private let developer:Developer
    private let designer:Designer

    init(developer: Developer, designer: Designer) {
        self.developer = developer
        self.designer = designer
    }


    func createApp(requirements:Requirements) -> App {
        let design = designer.createDesign(requirements)
        return developer.createApp(requirements, design: design)
    }
}

Now, to use our factory, we’ll need to pass in its dependencies:

let developer = ObjectiveCDeveloper()
let designer = SkeumorphicDesigner()
let factoryLike2012 = AppFactory(developer: developer,
                                   designer: designer)
factoryLike2012.createApp(requirements)

And we can reuse it in other combinations:

let developer = JavaDeveloper()
let designer = MaterialDesigner()
let factoryForAndroid = AppFactory(developer: developer,
                                   designer: designer)
factoryForAndroid.createApp(requirements)

Wait a minute! Did we just create an Android app?

Dependency Injection

To solve our code duplication problems, we applied dependency injection: we injected instances of a specific type into a loosely coupled initializer. We’ll never have an excuse again to copy-paste because of new combinations of developers and designers.

Furthermore, we are now sure that if the internals of a developer or designer change, the AppFactory can still remain untouched.

Testing becomes easier too. Imagine creating a unit test for the AppFactory2012 class. A real unit test only tests a single unit, but this proves to be hard when it comes to our AppFactory2012 because, without us wanting to, it needs to call the ObjectiveCDeveloper and SkeumorphicDesigner whenever we call the createApp function in our tests.

It becomes easier with our AppFactory, because we can inject any developer and designer, even fake ones (mocks, stubs anyone?)

We’ll cover the subject of testing in more dept later in this series!

Conclusion

Dependency Injection allows us to write better readable, more maintainable and better testable code. It isn’t hard to do, you just need to look at your code from a different perspective. Look for tight coupled dependencies in your code and remove them by simply hiding the implementation details behind a protocol, as we did with our Designers and Developers. Then, inject those properties via setters or via an initializer.

While you’re at it, take a look at our Dependency Injection framework, called Reliant. Many more examples on how to do DI are available there.

Continue reading

View all articles

GearMonster Introduction & Roadmap

GearMonster is a pet project at AppFoundry… it is a case for continuous research & development and evaluation of our software factory for Android.[...]

How we do iOS apps

At AppFoundry we love crafting software that fits the needs of our customers. Since all apps are different, they often require different solutions for different problems. There are, however, some basic principles and solutions that we apply in almost all of our apps.[...]