> ## Content Index
> Fetch the complete content index at: https://williamboles.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Let Dependency Injection Lead You to a Better Design
- URL: https://williamboles.com/let-dependency-injection-lead-you-to-a-better-design/
- Published: 2012-05-18T19:28:00.000Z
- Updated: 2026-09-26T09:08:41.000Z
- Description: Dependency injection is one of the superpowers that developers can utilise to produce code that is more predictable and easier to test. Here, we look at dependency injection - what it is and how we can utilise it.
- Author: William Boles
- Tags: Testing

[Dependency Injection (DI)](https://en.wikipedia.org/wiki/Dependency%5Finjection?ref=williamboles.com) is a powerful design technique that turns hidden, hard-to-control dependencies into explicit components that can be passed into a class or method. By making dependencies injectable, our code gains several advantages:

1. **Testability** \- Unit tests become easier to write because we can provide mock or stub versions of dependencies instead of relying on techniques like method swizzling.
2. **Flexibility** \- Swapping implementations (e.g. a disk cache for some network calls vs an ephemeral cache for other network calls) requires no code changes, just different injected dependencies.
3. **Maintainability** \- Explicit dependencies make code easier to understand, as we can see exactly what a class or method relies on from its interface.
4. **Decoupling** \- By breaking direct ties to specific implementations, our system becomes less brittle. Decoupled code reduces the risk of cascading changes when refactoring and makes introducing new features safer.

![Photo of a man walking down a path](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/2025/09/photo-of-man-walking-down-a-path-min.jpg)

In short, `Dependency Injection` encourages a more modular, testable, adaptable design that can evolve with our application over time.

> I always think of automated testing as a safety net; the larger I can make that safety net, the more comfortable I am with what I've written. `Dependency Injection` is a great way to expand that safety net.

## Dependency Injection Forms

`Dependency Injection` takes three forms:

1. **Initialiser Injection** \- Use init'er parameters to require dependencies before an instance of the type can be initialised.
2. **Property Injection** \- Use properties to set optional dependencies after instance initialisation.
3. **Method Injection** \- Use method parameters to require dependencies to be provided before the operation of the method can be performed.

Together, we are going to refactor a class to utilise the three forms of `Dependency Injection`, resulting in a more decoupled design that is easier to test as it is explicit about its dependencies. Let's look at that class:

```
@interface WBAccount : NSObject

@property (nonatomic, strong) WBUser *user;

@end

@implementation WBAccount

  #pragma mark - Init

  - (instancetype)init
  {
      self = [super init];

      if(self)
      {
          self.user = [WBUser localUser]; //a class method to retrieve the locally signed-in user
      }

      return self;
  }

  #pragma mark - Save

  - (void)saveToCache
  {
      [[WBCache sharedInstance] saveAccount(self)];
      [[WBLogger sharedInstance] accountSaved(self)];
  }
@end
```

`WBAccount` is simple: it holds a local user and provides a way to save that user to cache.

However, that simplicity is sitting uneasily - it feels like `WBAccount` owns the responsibility of creating its own dependencies. With `Dependency Injection`, we will flip this ownership model and instead have `WBAccount` merely receive its dependencies. The result of this ownership flip will be a more straightforward version of `WBAccount` without the unease.

Now let's start its transformation.

### Initialiser Injection

As mentioned, `Initialiser Injection` is where a dependency is passed in as a parameter during the initialisation of an object. Let's look at the initialiser of `WBAccount`:

```
@implementation WBAccount
  // Omitted other code

  - (instancetype)init
  {
      self = [super init];

      if(self)
      {
          self.user = [WBUser localUser]; //a class method to retrieve the locally signed-in user
      }

      return self;
  }
@end
```

In the above code snippet, we can see that `WBAccount` is dependent upon the `WBUser` class. There's nothing wrong with having `WBAccount` dependent on `WBUser`. One type depending on another happens all the time, but let's delve deeper. `WBAccount` isn't only concerned with using a `WBUser` instance, `WBAccount` is also responsible for retrieving the local `WBUser` instance itself. This doesn't feel right as it violates the design principle: [separation of concerns](https://en.wikipedia.org/wiki/Separation%5Fof%5Fconcerns?ref=williamboles.com). If in the future we wanted to tweak `WBAccount` to work with another `WBUser` instance, we would need to modify `WBAccount` itself. Let's tweak `WBAccount` to remove the responsibility of retrieving its `WBUser` instance and so make future tweaks easier by using dependency injection to pass that `WBUser` instance in as a parameter during initialisation:

```
@implementation WBAccount

  - (instancetype)initWithUser:(WBUser *)user
  {
      self = [super init];

      if(self)
      {
          self.user = user;
      }

      return self;
  }
@end
```

With this code change, `WBAccount` no longer has to care about which `WBUser` instance or how that instance is created, `WBAccount` only cares that it always has a `WBUser` instance to work with. By using `Initialiser Injection`, we have reduced the responsibilities of `WBAccount`.

### Property Injection

As mentioned, `Property Injection` is where a dependency is passed in as a property's value. Inside the `saveToCache` method of `WBAccount`, we currently use `WBLogger` to log that a save operation has been completed. `WBLogger` feels like an optional dependency, so let's use `Property Injection` to be explicit about that:

```
@interface WBAccount : NSObject

// Omitted other code

@property (nonatomic, weak) WBLogger *logger;

@end

@implementation WBAccount

  // Omitted other code

  #pragma mark - Save

  - (void)saveToCache
  {
      // Omitted other code
      [logger accountSaved(self)];
  }
@end
```

After an instance of `WBAccount` is created, this `logger` property is set. `WBAccount` is no longer concerned with the creation of a `WBLogger` instance; `WBAccount` is only concerned with what `WBLogger` can do. By using `Property Injection`, we have reduced the responsibilities of `WBAccount`.

### Method Injection

As mentioned before, `Method injection` is where a dependency is passed to a method so that the method can utilise it to fulfil its operation. Inside the `saveToCache` method, we currently retrieve the `WBCache`. Let's instead use `Method Injection` to pass the cache in:

Take the following example of a method:

```
@implementation WBAccount

  // Omitted other code

  #pragma mark - Save

  - (void)saveToCache(WBCache *)cache
  {
      [cache saveAccount(self)];
      // Omitted other code
  }
@end
```

In the above code snippet, we can see that the `saveToCache` method is now dependent on receiving a `WBCache` object to store the account in the cache. This increased flexibility allows us to swap out a `WBCache` instance with another instance in the production target or a test-double in the test target. By using `Method Injection`, we have reduced the responsibilities of `saveToCache`.

#### Special note: Dealing with Singletons

The singleton pattern creates a single, shared instance of a class that's accessible globally, making it ideal for managing centralised state or configuration. The direct use of singletons is a primary candidate for the Dependency Injection forms described above. The direct use of a singleton is a [code smell](https://en.wikipedia.org/wiki/Code%5Fsmell?ref=williamboles.com) that `Dependency Injection` can help to remove.

Take the following example of a method:

```
- (NSString *)welcomeMessage
{
    return [NSString stringWithFormat:@"Welcome to the %@ service", [WBBrandManager sharedInstance].serviceName];
}
```

In the above code snippet, like in all the others, we see an implicit dependency which can be broken down by:

```
- (NSString *)welcomeMessageWithBrand:(NSString *)brand
{
    return [NSString stringWithFormat:@"Welcome to the %@ service", brand];
}
```

Here, we have used `Method Injection` to not only remove the singleton from the method body but also to remove any dependency on the `WBBrandManager` type; instead, we are now using the standard `Foundation` framework class `NSString`, which has resulted in a more straightforward and flexible implementation. This change will also lead to simpler unit tests. In fact, the more unit tests that you write, the more powerful `Dependency Injection` as a technique becomes.

## Won't this lead to more complexity?

Now, the above example is small and the changes straightforward, and you might be wondering:

"What happens when a class starts to accumulate four, five, or even six dependencies? Won't this just lead to more complexity not less?"

At first glance, it may seem that dependency injection adds unnecessary complexity. In reality, this is usually a signal that the class or method is doing too much and is violating the [Single Responsibility Principle](https://en.wikipedia.org/wiki/Single%5Fresponsibility%5Fprinciple?ref=williamboles.com). The solution isn't to abandon dependency injection but to reconsider the design and break the code into smaller, more focused units. In other words, dependency injection doesn't create complexity - it reveals it.

By making dependencies explicit, we make the true shape of our system visible. Hidden dependencies create traps for future developers, where changes in one place unexpectedly break behaviour elsewhere. Explicit dependencies, on the other hand, surface design problems early and encourage the development of modular, testable, and maintainable code.