EnriqueMark
Design Patterns & Architecture

Inversion of Control

2026-04 English

If you don’t get IoC you’ll only ever use DI as a bloated factory pattern. Take converting between XML, JSON and TXT, where you new up a pile of things inside and switch on an enum to decide. But IoC would work fine here, define a generic, let the outside hand it in, and new T inside, no enum needed at all. Some people say IoC is what DI is really about, and I don’t think they’re wrong.

Take the code below. It injects, but the control still sits inside, since the outside uses an enum to control what the conversion method does internally. It looks like control is on the outside, but this is indirect, fake control. The real control is still inside, and all you handed over is a switching signal.

// Injected, but control is not inverted
class ConversionService {
    public string Convert(string input, FormatEnum format) {
        switch (format) {
            case FormatEnum.XML:  return new XmlConverter().Convert(input);
            case FormatEnum.JSON: return new JsonConverter().Convert(input);
            // Every new format means editing here. Open/closed principle broken outright.
        }
    }
}

Below is real IoC with a generic, where control is fully inverted.

interface IConverter {
    string Convert(string input);
}

class ConversionService<T> where T : IConverter, new() {
    public string Convert(string input) {
        var converter = new T(); // the concrete type is decided from outside
        return converter.Convert(input);
    }
}

// caller
var svc = new ConversionService<XmlConverter>();

I think the important thing here is that remote-controlling it and handing T in from outside both look like control, but there’s a critical difference. The first is selection. The boundary is set inside the method, all you can do is pick from the options that already exist, and the control really is still inside. IoC lets go completely. It doesn’t care what you hand in, doesn’t care about the boundary, and answers only to the interface (the structure). XML, JSON, TXT are three things, and the remote control can only pick those three. IoC is unlimited, and as long as you comply you can pass anything. That’s real inversion of control, letting go and answering only to the interface.

To put it as a kitchen and a cook, the first one is a kitchen that comes with cooks, but only three of them, Sichuan, Hunan and Shanghainese. What if I want Henan food? Sorry, not on offer. IoC is kitchen provided, bring your own cook. Henan food? No problem. Northeastern? No problem. Western food? Of course, as long as you use Chinese kitchen tools, since the kitchen (the interface) is fixed. That’s where the key to inversion of control is. It gives you the most flexibility and abstraction you can get, but it isn’t unconstrained, and defining the interface is critical.

This also shows where interface design matters. Give too much and anything goes, behavior becomes unpredictable, and you get coupling out of it instead. The problem is the same as with inheritance, implicit dependencies. You have no idea what the subclass used, the base class changes something that looks harmless, and everything below blows up. So interface design is a trade-off between flexibility and constraint, and a good interface has to have a clear contract and clear boundaries. Under a vague interface design, IoC won’t save you from coupling either.

There’s an even more extreme use, combining it with reflection, where you don’t have to specify the type at compile time at all. That has a cost. If I write where T : IConverter, at least I’ve specified what the interface structure looks like, and if I get it wrong the compiler tells me. With reflection all I can do is a cast. I’m agreeing with you that it needs to be this type here, but whether you actually use that type is something I can’t enforce.

Type type = Type.GetType("MyApp.XmlConverter");
IConverter converter = (IConverter)Activator.CreateInstance(type);
// If XmlConverter doesn't implement IConverter at all, this throws InvalidCastException right here

Here, if what comes in doesn’t match the contract, you only find out at run time. With a generic the compiler errors out and tells me, and it never gets to that last stage. So reflection does have a bit of a dynamic-language flavor to it, like Python, where you don’t know whether it will throw until it runs. That’s a trade-off between flexibility and stability. Some situations do need the flexibility, but you have to know its cost and its boundaries. If you can, wrap the errors around that flexibility up front. Even if the result is unpredictable, keep the damage contained and as small as possible.


Translation note. I wrote this in Chinese. This English version is an LLM translation, so the wording is not mine even though the thinking is. Original: 控制反轉.