Inversion of Control (IoC) is a powerful concept that allows us to decouple the flow of control from the flow of dependency in software systems. By understanding and leveraging IoC, we can achieve more flexible and maintainable code.
It took me a while to understand this concept because I only viewed it from the lens of interfaces as a tool in statically typed languages.
In this article, we’ll explore the problem solved by IoC using a simple example of a login system, and discuss how to separate the concepts of flow of control and flow of dependency for improved code organization.
What does it mean to be Tightly Coupled?
Consider a scenario where we have a basic login system. In this system, we typically have a main file, a database driver, and the actual database. The flow of control occurs when a user logs in and an endpoint is hit, triggering the processing of the login request. The response, using the POST HTTP verb, involves fetching the user’s information from the database and validating it.
In this simple system, the flow of control and the flow of dependency are tightly coupled. The login file, responsible for the flow of control, directly imports and depends on the database driver file. This direct coupling poses challenges when changes to the database or dependencies are required. For instance, if we were to switch from MySQL to Postgres, we would need to modify both the login file and the driver file, introducing unnecessary complexities.
Separating Flow of Control and Dependency with Interfaces
To address the tightly coupled flow of control and dependency, we introduce the concept of interfaces or contracts. Instead of directly importing and depending on specific files, the login file will utilize an interface as a contract for its required functionalities. This way, the flow of control can remain unchanged while the flow of dependency becomes more flexible.
Here’s how the solution unfolds:
1. Defining an Interface:
We create an interface that represents the required functionality for the database driver. This interface acts as a contract that the driver must implement. It defines the methods and behaviors needed by the login file.
interface DatabaseDriver {
function fetchUserDetails(username: string): User;
// Other required methods...
}
2. Implementing the Interface:
The driver file, responsible for interacting with the actual database, implements the interface defined earlier. It ensures that the required functionality is available to fulfill the contract.
class MySQLDriver implements DatabaseDriver {
function fetchUserDetails(username: string): User {
// Implementation specific to MySQL
}
// Other methods implementation...
}
3. Utilizing the Interface:
In the login file, instead of importing the actual driver file, we import the interface or pass it as a parameter. This way, the login file doesn’t depend on the specific implementation but relies on the contract defined by the interface.
function loginUser(driver: DatabaseDriver, username: string, password: string) {
const userDetails = driver.fetchUserDetails(username);
// Validate user details...
// Perform login operations...
}
While we introduced a simple example of a login system, the principles of IoC and interface-based development apply to more complex scenarios.
Did this help you understand the concept a bit more? Talk to me in the comment section below, or on my twitter @jchex






