Card 02/ 09
Spotting a Dependency Inversion Violation
ReminderService creates its own MySQLDatabase, so a test needs a real MySQL server.
A dependency inversion violation shows up as business code that creates its own database connection, mail client or HTTP client. This card shows a ReminderService that does so, in Python, Java and TypeScript, and what happens when a test tries to run it. Each MySQLDatabase here stands in for a real driver and fails the way a driver does when no server is listening.
A ReminderService that creates its own database
class MySQLDatabase:
def __init__(self, url):
raise ConnectionError(f"no MySQL server at {url}")
def find_email(self, user_id): ...
class ReminderService:
def __init__(self):
self.database = MySQLDatabase("db.internal:3306")
def remind(self, user_id):
email = self.database.find_email(user_id)
return f"MAIL to {email}: reset your password"
def test_remind():
service = ReminderService()
assert service.remind(7) == "MAIL to [email protected]: reset your password"
try:
test_remind()
except ConnectionError as error:
print(f"test_remind failed: ConnectionError: {error}")test_remind failed: ConnectionError: no MySQL server at db.internal:3306public class DipViolation {
static void testRemind() {
ReminderService service = new ReminderService();
System.out.println(service.remind(7));
}
public static void main(String[] args) {
try {
testRemind();
} catch (IllegalStateException error) {
System.out.println("testRemind failed: " + error.getMessage());
}
}
}
class MySQLDatabase {
MySQLDatabase(String url) {
throw new IllegalStateException("no MySQL server at " + url);
}
String findEmail(int userId) { return null; }
}
class ReminderService {
private final MySQLDatabase database = new MySQLDatabase("db.internal:3306");
String remind(int userId) {
return "MAIL to " + database.findEmail(userId) + ": reset your password";
}
}testRemind failed: no MySQL server at db.internal:3306class MySQLDatabase {
constructor(url: string) {
throw new Error(`no MySQL server at ${url}`);
}
findEmail(userId: number): string {
return "";
}
}
class ReminderService {
private readonly database = new MySQLDatabase("db.internal:3306");
remind(userId: number): string {
return `MAIL to ${this.database.findEmail(userId)}: reset your password`;
}
}
function testRemind(): void {
const service = new ReminderService();
console.log(service.remind(7));
}
try {
testRemind();
} catch (error) {
console.log(`testRemind failed: ${(error as Error).message}`);
}testRemind failed: no MySQL server at db.internal:3306The test fails in every language, and not because the reminder logic is wrong. It fails because creating a ReminderService creates a MySQLDatabase, and there is no server at db.internal:3306 on the machine running the test. To test one line of business logic, someone has to start a database, create the tables and load a user.
Look at the highlighted line. The new or the constructor call inside the service is the violation: the policy chooses its own detail, by name, and nobody outside can change that choice. Other signs have the same cause. An import of a driver module at the top of a file of business rules, a connection string in a class that computes discounts, or a test suite that only runs on a machine with a database are all the same arrow pointing the wrong way.
The cost is not only in tests. Moving users to another store means editing ReminderService, a class whose rules did not change. Policy and detail are tied together, so a change to either reaches the other.
← → move through sections and cards