Card 04/ 09
Refactoring to Dependency Inversion, Step by Step
Define the interface on the service's side, implement it, then inject it.
Inverting a dependency means introducing an interface that the high-level class owns, implementing it in the low-level code, and injecting the implementation. This card takes the ReminderService that created its own database through those steps, and settles the question people get wrong most often: where the interface belongs.
Inverting ReminderService's dependency on MySQL
Write down what the policy needs
Read ReminderService and list what it asks the database for. It needs one thing: the email address for a user id. It does not need connections, tables or SQL.
Define the interface beside the policy
Create UserSource with one method, email_for(user_id), in the same module or package as ReminderService. It is named after the service's need, not after MySQL, and it lives with the code that owns it.
Implement it in the detail
Write MySQLUserSource, which implements UserSource by running the query. All the SQL moves here. The MySQL package now depends on the reminder package, because it implements the reminder package's interface.
Inject the implementation
Change ReminderService to take a UserSource in its constructor and delete the line that created the database. The application's start-up code creates MySQLUserSource and passes it in.
The final code below wires the real implementation in. The query it prints is a stand-in for one sent to a database.
ReminderService with an injected UserSource
from typing import Protocol
class UserSource(Protocol):
def email_for(self, user_id: int) -> str: ...
class ReminderService:
def __init__(self, users: UserSource):
self.users = users
def remind(self, user_id: int) -> str:
return f"MAIL to {self.users.email_for(user_id)}: reset your password"
class MySQLUserSource:
def email_for(self, user_id: int) -> str:
print(f"SELECT email FROM users WHERE id = {user_id}")
return "[email protected]"
if __name__ == "__main__":
service = ReminderService(MySQLUserSource())
print(service.remind(7))SELECT email FROM users WHERE id = 7
MAIL to [email protected]: reset your passwordpublic class DipInverted {
public static void main(String[] args) {
ReminderService service = new ReminderService(new MySQLUserSource());
System.out.println(service.remind(7));
}
}
interface UserSource {
String emailFor(int userId);
}
class ReminderService {
private final UserSource users;
ReminderService(UserSource users) {
this.users = users;
}
String remind(int userId) {
return "MAIL to " + users.emailFor(userId) + ": reset your password";
}
}
class MySQLUserSource implements UserSource {
public String emailFor(int userId) {
System.out.println("SELECT email FROM users WHERE id = " + userId);
return "[email protected]";
}
}SELECT email FROM users WHERE id = 7
MAIL to [email protected]: reset your passwordinterface UserSource {
emailFor(userId: number): string;
}
class ReminderService {
constructor(private readonly users: UserSource) {}
remind(userId: number): string {
return `MAIL to ${this.users.emailFor(userId)}: reset your password`;
}
}
class MySQLUserSource implements UserSource {
emailFor(userId: number): string {
console.log(`SELECT email FROM users WHERE id = ${userId}`);
return "[email protected]";
}
}
const service = new ReminderService(new MySQLUserSource());
console.log(service.remind(7));SELECT email FROM users WHERE id = 7
MAIL to [email protected]: reset your passwordThe second step is where the refactoring usually goes wrong. Putting UserSource in the MySQL package, next to its implementation, makes the service import from the MySQL package again: the arrow is back where it started. Putting it in a shared interfaces module owned by nobody invites everyone to add methods to it. The interface belongs to the policy, because the policy is the code that decides what it must contain.
← → move through sections and cards