Inversion of Control is About Choice

Specifically inversion of control is about not making a choice in one place and forcing that choice to be made elsewhere.

Take a library that talks to a database. Should that library make a choice on how to connect to the datatabase? If it does, that’s a huge set of things to support and more choices to make. Should a database abstraction layer (DBAL) be included in the library? What happens if database dialects differ? How much should the library support?

On the flip side, if the library author choses to not make that choice and instead depend on an abstraction of a database — either something built into the language or another library — a whole set of problems disappears. No more need for a DB abstraction layer. Different database dialects are not a concern as the abstraction should handle that.

The chosen DBAL need not even exist. Export an interface from the library that defines it and let the client of the library implement it. Is that more work for the end user? Absolutely. But it’s the right kind of work. The kind of work that gives them confidence in the library because they built and thoroughly tested the point at which it interacts with the outside world.

How about a modern JavaScript example: a React component that needs to update an entire applications state to point to a new current entity (like a current poject or current folder) and reorganize the UI accordingly. Should this little component need to understand the entire application?

class ProjectSelector extends Component {
    // probably a *bunch* of stuff here to handle state and such
    
    onChange(event) {
        this.setState({
            currentProject: event.target.value,
        });
        this.updateMenus();
        this.updateMainContent();
        // etc
    }

    render() {
        return (
            <select className="project-selector" onChange={this.onChange} value={this.state.currentProject}>
                <!-- stuff here -->
            </select>
        );
    }
}

The above could work, but what happens when somewhere else in the application needs to know about the current project?

Inversion of control can solve that problem. Instead of managing state internally and controlling the entire UI, accept a callback to fire when the current project changes and a value that defines what the current project is. Shift the choice of how to manage the current project up a level into the application as a whole. The callback could kick off an action if using some sort of store or update state in a component higher in the chain. The key is that the selector component no longer cares what’s going on with the rest of the UI.

function ProjectSelector({ projects, onProjectChange, currentProject }) {
    return (
        <select className="project-selector" onChange={e => onProjectChange(e.target.value, currentProject, projects)} value={currentProject}>
            <!-- stuff here rendered from `projects` -->
        </select>
    );
}

Think carefully about when choices can be avoided and where it makes sense to shift that choice to the client of a piece of code — be that a library or a small piece of a larger application. Often times avoiding a choice and depending on an abstraction can solve a lot of problems.