Skip to content

Instantly share code, notes, and snippets.

@stepanhruda
Last active February 11, 2016 22:17
Show Gist options
  • Save stepanhruda/ae5798882e5865bbcfa6 to your computer and use it in GitHub Desktop.
Save stepanhruda/ae5798882e5865bbcfa6 to your computer and use it in GitHub Desktop.
Dependency injection

Two simple types using dependency injection.

struct CarKey {
  let ownerInitial: String
  let serialNumber: String

  init(ownerInitial: String, serialNumberGenerator: SerialNumberGenerator = SecureAndExpensiveSerialNumberGenerator()) {
    self.ownerInitial = ownerInitial
    self.serialNumber = serialNumberGenerator.generate()
  }
}

class SecureAndExpensiveSerialNumberGenerator: SerialNumberGenerator {
  func generate() -> String {
    // tested in SecureAndExpensiveSerialNumberGenerator, not desired to be called from elsewhere
  }
}

In CarKeySpec I can inject a test generator to prevent tests from needing the real SecureAndExpensiveSerialNumberGenerator, while still making sure the serial number gets generated via the intended API. All good so far.


Let's expand our model layer with an additional type.

struct Car {
  let owner: Person
  let brand: Brand
  let key: CarKey
  
  init(owner: Person, brand: Brand) {
     self.owner = owner
     self.brand = brand
     self.key = CarKey(ownerInitial: owner.name)
  }
}

When testing this class (and any other class that isn't CarKey), my goal is to absolutely avoid using SecureAndExpensiveSerialNumberGenerator under the hood. (There are many classes I frequently want to avoid when running tests in iOS simulator. E.g. the keychain, NSUserDefaults, NSURLSession, NSDate etc.)

Below is the best solution that comes to mind.

init(
  owner: Person,
  brand: Brand,
  keyInitializer: (ownerInitial: String, serialNumberGenerator: SerialNumberGenerator) -> Key = Key.init(ownerInitial:serialNumberGenerator:)) {
    self.owner = owner
    self.brand = brand
    self.key = keyInitializer(ownerInitial: owner.name, serialNumberGenerator: SecureAndExpensiveSerialNumberGenerator())
}
  1. the code is harder to understand – I'd argue testability hurts production code readability here (btw, this is a Swift 2.2 version with labeled selectors)
  2. we lost the entire power of default arguments by having to pass SecureAndExpensiveSerialNumberGenerator() from this layer again

If selector definitions could take default arguments into account (currently not available in Swift 2.2, not sure if possible), the latter point would go away:

keyInitializer: (ownerInitial: String) -> Key = Key.init(ownerInitial:)) {

This is definitely nicer, but can quickly get hairy again if your type has more than one property, so the first point still stands. When using Objective-C + Kiwi, I would stub Key.init and make the initializer return whatever key I wanted to test with.

What is your usual approach in Swift?

@luisobo
Copy link

luisobo commented Feb 11, 2016

Previous conversation here

@stepanhruda: how is injecting provider different from injecting a closure as I do in the bottom

They are the same thing, except that you'd keep the provider around to generate multiple instances. The difference is that you are discarding the provider (keyInitializer) after generating a single instance of Key. You'd be better off injecting that Key directly via the constructor.

Now, that moves the problem of generating Keys somewhere else, yes. You want to push it as high in the dependency DAG as possible to reduce coupling. If we are taking about a static part of the DAG, i.e. you only have a single instance of Key in your dependency DAG, you'd move the init of that Key to the main() function/test.

Let's suppose you have a dynamic number of Keys, which seems to be the case. Seems that the Key is not the only dynamically-allocated object, seems like Car would be too (one car has a predefined number of Keys). You need to find the top-most object in the dep DAG responsible for allocating things dynamically, in this case it'd be a CarFactory (a CarFactory would create a dynamic number of Cars). You'd inject a provider of Keys and a provider of cars (or other parts, really) into this CarFactory, so it can assemble Cars.

Not sure what is your case but you either inject everything statically from the top if your dep DAG is static or you inject a provider, but not that deep in the hierarchy as in your example.

Hope it makes sense! 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment