Software Design

Dependency Injection (DI): Moving new Outside the Class

We often create required objects right where they are needed in the code. But the moment you try to write tests, you hit a wall. The creation code is embedded inside the class, so you cannot swap it out.

5 min read
Cover image for Dependency Injection (DI): Moving new Outside the Class

We often create required objects right where they are needed in the code. But the moment you try to write tests, you hit a wall. The creation code is embedded inside the class, so you cannot swap it out.

Searching for dependency injection (DI) often brings up terms like “inversion of control” and “IoC container,” which can make things even more confusing.

So today, I’ll explain it as simply as possible. Let’s start with the conclusion.

Dependency injection (DI) means creating an object outside the class instead of inside it, then passing it in. That single movement is the key: move object creation outside the class. That is almost all there is to DI.

By the end of this article, you’ll understand why DI is needed, how the code changes, and why DI libraries such as Swinject handle this for you.

What’s wrong with creating objects inside a class?

Suppose we have a class that processes orders. It needs a payment module to process payments.

The most common beginner code looks like this.

class OrderService {
    // Create the payment object directly inside the class
    private let pay = KakaoPay()

    func order() { pay.pay() }
}

The problem is that OrderService is now tightly coupled to KakaoPay.

What if you want to replace KakaoPay with NaverPay? You have to open the class and change the creation code.

Even if you want to pass a fake payment object during testing, you have no way to do it. The class has already created a real KakaoPay instance internally.

In other words, placing object creation inside a class ties it to a specific implementation.


So we moved object creation outside

The solution is simpler than you might expect. Don’t create it directly; create it outside and just receive it.

class OrderService {
    private let pay: Pay
    // Just receive what was created and passed in from outside
    init(pay: Pay) { self.pay = pay }

    func order() { pay.pay() }
}

Only one thing changed: direct creation disappeared, and the object is now received through the initializer.

OrderService no longer needs to know whether the payment provider is KakaoPay or NaverPay. It only knows that it receives and uses something called Pay.

The essence of DI is not grandiose. It simply moves object creation outside the class and hands the decision about what to provide to the outside.

Passing an object in from outside is called “injection.” You inject the object a class depends on; hence, dependency injection.

The name may sound difficult, but that is all it does.


What are the benefits of DI?

Saying it is beneficial is not enough, so here is what actually changes.

  • Replacement becomes easy: even if you switch KakaoPay to NaverPay, you do not change a single line in OrderService. You only change the object you pass in.
  • Testing becomes easier: you can pass a fake (Mock) payment object instead of a real one, so you can verify only the logic without charging money.
  • Responsibilities are separated: OrderService focuses only on “processing orders,” while the outside decides “which payment method to use.”

The third point resonates with me most. When a class focuses only on its own job, the code becomes much easier to read.

In summary:

Category Creation inside Creation outside (DI)
Replacing payment Class modification required Replace only the provided object
Testing Cannot pass a fake object Mock injection possible
Responsibilities Creation and logic mixed Focus only on logic
Once new is moved outside, the code becomes this clean
Once new is moved outside, the code becomes this clean

Then why do we need DI libraries such as Swinject?

At this point, a natural question comes up: “I understand that it is passed in from outside, but who manages that ‘outside’?”

If there are only a few objects, you can create and pass them in manually. But real-world projects have hundreds of interconnected objects.

Having a person create and pass them in one by one, in the right order, is hell.

That is why tools emerged to handle this “creating and passing objects in from outside.” DI containers such as Swinject and Factory in the Swift ecosystem play exactly that role.

The container creates objects and plugs them in where needed
The container creates objects and plugs them in where needed
Once there are hundreds of objects, you appreciate having a container manage them for you
Once there are hundreds of objects, you appreciate having a container manage them for you

Register the objects you need, and the container creates them and injects them where required. You almost never need to create them yourself.

This is also where the commonly mentioned “inversion of control (IoC)” comes from. It means control over creating and connecting objects has moved from your code to the container.

In short, DI is the concept, and a DI container is the worker that executes it for you.


Frequently asked questions

Q. Are DI and IoC the same thing? No. IoC (inversion of control) is the broad principle of handing over control, while DI is one concrete way to implement it. DI is one type of IoC.

Q. Are there injection methods other than constructors? Yes. There are constructor injection, property injection, and method injection. In Swift, however, constructor injection is recommended. Dependencies can be fixed with let, preventing changes and making testing easier.

Q. Is a library required for something to be DI? No. Passing an object into a constructor manually, as we just did, is fully valid DI. Libraries only automate it.


Today, we explained dependency injection as a very small move: changing where an object is created.

If DI feels difficult, remember just one thing: “Don’t create it inside the class; create it outside and pass it in.” Everything else follows naturally.

If you have felt stuck when writing test code, I recommend manually changing the example from today. It will click much more clearly than simply looking at it.

Continue reading