Dependency Injection in Practice

Last updated 93 days ago by Josh Adams

swift

Dependency injection makes unit testing possible and development easier. This post describes the process of preparing an app for dependency injection, as well as implementing three approaches to dependency injection: constructor injection, Swinject, and The World.

Definition

This post is about implementing dependency injection, but before I dive into the nuts and bolts, likely not from a great height, I would like to provide a definition to readers who are unfamiliar with dependency injection. Here you go:

Dependency injection is the practice of taking away from objects the job of acquiring their dependencies. A dependency is an object that another object relies on to achieve its business purpose.

Although this definition is correct, it does not convey the value proposition of dependency injection. I consider value propositions key to understanding software-development concepts, and I therefore find this definition incomplete. I will remedy this by describing dependency injection’s value proposition or, in less jargony terms, the problem that dependency injection solves.

Value Proposition

Imagine a struct whose purpose is to turn a String like 5000 into a String formatted as currency, $5,000.00. Here is an implementation:

``` struct SimpleCurrencyFormatter { private let formatter: NumberFormatter

init() { formatter = NumberFormatter() formatter.usesGroupingSeparator = true formatter.numberStyle = .currency }

func formatCurrency(string: String) -> String? { guard let doubleValue = Double(string) else { return nil } return formatter.string(from: NSNumber(value: doubleValue)) } } ```

Here is an example use of SimpleCurrencyFormatter:

``` let errorString = "ERROR" let rawCurrencyString = "5000"

let simpleCurrencyFormatter = SimpleCurrencyFormatter() print(simpleCurrencyFormatter.formatCurrency(string: rawCurrencyString) ?? errorString) ```

As Jon Reid observed, “[a] robust suite of unit tests acts as a safety harness, giving you courage to make bold changes.” Desiring this benefit, I would indubitably unit test SimpleCurrencyFormatter were I to use it in production. Here is a unit test:

``` class SimpleCurrencyFormatterTests: XCTestCase { func testSimpleCurrencyFormatter() { let rawCurrency = "5000" let simpleCurrencyFormatter = SimpleCurrencyFormatter()

guard let formattedCurrency = simpleCurrencyFormatter.formatCurrency(string: rawCurrency) else {
  XCTFail("formattedCurrency was nil.")
  return
}

XCTAssertEqual(formattedCurrency, "$5,000.00")

} } ```

Although this unit test works on my laptop, there is a problem. SimpleCurrencyFormatter is responsible for acquiring a key dependency, the Locale, an object that “encapsulates information about linguistic, cultural, and technological conventions and standards”, in this case number-and-currency formatting. Because SimpleCurrencyFormatter specifies no Locale for its NumberFormatter, SimpleCurrencyFormatter chooses the default Locale for NumberFormatter, which, in my case, is the United Statesian Locale. But other developers have different Locales. A developer whose locale is French would see the unit test fail with this error: XCTAssertEqual failed: ("€5 000,00") is not equal to ("$5,000.00")

Read full Article