Dependency Inversion Principle (DIP)¶
Khái niệm¶
Dependency Inversion Principle là nguyên lý cuối cùng và quan trọng nhất trong bộ nguyên lý SOLID. Đây là nền tảng của kiến trúc phần mềm hiện đại và các pattern như Dependency Injection.
Định nghĩa chính thức¶
DIP bao gồm hai phần:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
Giải thích đơn giản¶
- Phụ thuộc vào abstraction (interface), không phụ thuộc vào implementation cụ thể
- Class cấp cao (business logic) không nên phụ thuộc vào class cấp thấp (infrastructure)
- Cả hai nên phụ thuộc vào interface chung
Tại sao DIP quan trọng?¶
- Giảm coupling: Modules độc lập với nhau
- Tăng flexibility: Dễ dàng thay đổi implementation
- Cải thiện testability: Mock dependencies dễ dàng
- Hỗ trợ maintainability: Code ổn định và dễ bảo trì
- Enable Dependency Injection: Nền tảng cho IoC containers
Module Hierarchy và Dependency Direction¶
Traditional Dependency (Sai)¶
┌─────────────────┐
│ High-level │
│ (UserService) │ ────────┐
└─────────────────┘ │
│ depends on
▼
┌─────────────────┐
│ Low-level │
│ (MySQLDB) │
└─────────────────┘
Inverted Dependency (Đúng)¶
┌─────────────────┐ ┌─────────────────┐
│ High-level │ │ Abstraction │
│ (UserService) │ ────────▶ │ (Database) │
└─────────────────┘ └─────────────────┘
▲
│ implements
┌─────────────────┐ │
│ Low-level │ ─────────────────┘
│ (MySQLDB) │
└─────────────────┘
Ví dụ vi phạm DIP¶
Đây là cách implement SAI - vi phạm nguyên lý DIP:
// Low-level module (Infrastructure layer)
class MySQLDatabase {
public void save(String data) {
System.out.println("Connecting to MySQL...");
System.out.println("Executing INSERT query...");
System.out.println("Lưu vào MySQL: " + data);
System.out.println("Closing MySQL connection...");
}
public String find(String id) {
System.out.println("Executing SELECT query...");
return "User data from MySQL for ID: " + id;
}
}
// High-level module (Business logic layer)
class UserService {
private MySQLDatabase database; // TRỰC TIẾP phụ thuộc vào concrete class
public UserService() {
this.database = new MySQLDatabase(); // HARD-CODED dependency
}
public void createUser(String userData) {
// Business logic
if (userData == null || userData.trim().isEmpty()) {
throw new IllegalArgumentException("User data cannot be empty");
}
// Phụ thuộc trực tiếp vào MySQL
database.save(userData);
}
public String getUser(String userId) {
// Business logic
if (userId == null) {
throw new IllegalArgumentException("User ID cannot be null");
}
// Phụ thuộc trực tiếp vào MySQL
return database.find(userId);
}
}
// Application layer
class UserController {
private UserService userService;
public UserController() {
this.userService = new UserService(); // Cũng hard-coded
}
public void handleCreateUser(String userData) {
userService.createUser(userData);
}
}
Vấn đề nghiêm trọng:¶
- Tight coupling:
UserServicegắn chặt vớiMySQLDatabase - Hard to test: Không thể mock database cho unit testing
- Inflexible: Muốn đổi sang PostgreSQL phải sửa code
UserService - Violates OCP: Thêm database mới phải modify existing code
- Hard to maintain: Thay đổi database infrastructure ảnh hưởng business logic
- No reusability:
UserServicechỉ hoạt động với MySQL
Giải pháp tuân thủ DIP¶
Bước 1: Tạo abstraction (Interface)¶
// Abstraction - High-level policy
interface Database {
void save(String data);
String find(String id);
void delete(String id);
void update(String id, String data);
}
// Có thể có thêm interface cho các concerns khác
interface Logger {
void log(String message);
}
interface EmailService {
void sendEmail(String to, String subject, String body);
}
Bước 2: Implement concrete classes¶
// Low-level module - MySQL implementation
class MySQLDatabase implements Database {
@Override
public void save(String data) {
System.out.println("Connecting to MySQL server...");
System.out.println("Executing INSERT INTO users VALUES ('" + data + "')");
System.out.println("Data saved to MySQL successfully");
}
@Override
public String find(String id) {
System.out.println("Executing SELECT * FROM users WHERE id = '" + id + "'");
return "MySQL User: " + id;
}
@Override
public void delete(String id) {
System.out.println("Executing DELETE FROM users WHERE id = '" + id + "'");
}
@Override
public void update(String id, String data) {
System.out.println("Executing UPDATE users SET data = '" + data + "' WHERE id = '" + id + "'");
}
}
// Low-level module - PostgreSQL implementation
class PostgreSQLDatabase implements Database {
@Override
public void save(String data) {
System.out.println("Connecting to PostgreSQL server...");
System.out.println("Executing INSERT INTO users (data) VALUES ($1)");
System.out.println("Data saved to PostgreSQL successfully");
}
@Override
public String find(String id) {
System.out.println("Executing SELECT * FROM users WHERE id = $1");
return "PostgreSQL User: " + id;
}
@Override
public void delete(String id) {
System.out.println("Executing DELETE FROM users WHERE id = $1");
}
@Override
public void update(String id, String data) {
System.out.println("Executing UPDATE users SET data = $1 WHERE id = $2");
}
}
// Low-level module - In-memory implementation (for testing)
class InMemoryDatabase implements Database {
private Map<String, String> storage = new HashMap<>();
private AtomicInteger idCounter = new AtomicInteger(1);
@Override
public void save(String data) {
String id = String.valueOf(idCounter.getAndIncrement());
storage.put(id, data);
System.out.println("Data saved to memory with ID: " + id);
}
@Override
public String find(String id) {
return storage.getOrDefault(id, "User not found");
}
@Override
public void delete(String id) {
storage.remove(id);
System.out.println("User " + id + " deleted from memory");
}
@Override
public void update(String id, String data) {
if (storage.containsKey(id)) {
storage.put(id, data);
System.out.println("User " + id + " updated in memory");
}
}
}
// Additional implementations
class ConsoleLogger implements Logger {
@Override
public void log(String message) {
System.out.println("[LOG] " + LocalDateTime.now() + ": " + message);
}
}
class SMTPEmailService implements EmailService {
@Override
public void sendEmail(String to, String subject, String body) {
System.out.println("Sending email via SMTP to: " + to);
System.out.println("Subject: " + subject);
System.out.println("Body: " + body);
}
}
Bước 3: High-level module phụ thuộc vào abstraction¶
// High-level module - Business logic
class UserService {
private final Database database;
private final Logger logger;
private final EmailService emailService;
// Dependency Injection through constructor
public UserService(Database database, Logger logger, EmailService emailService) {
this.database = database;
this.logger = logger;
this.emailService = emailService;
}
public void createUser(String userData) {
try {
// Business logic validation
if (userData == null || userData.trim().isEmpty()) {
throw new IllegalArgumentException("User data cannot be empty");
}
// Log the operation
logger.log("Creating user with data: " + userData);
// Save to database (không quan tâm loại database nào)
database.save(userData);
// Send welcome email
emailService.sendEmail("user@example.com", "Welcome!", "Welcome to our service!");
logger.log("User created successfully");
} catch (Exception e) {
logger.log("Error creating user: " + e.getMessage());
throw e;
}
}
public String getUser(String userId) {
try {
if (userId == null) {
throw new IllegalArgumentException("User ID cannot be null");
}
logger.log("Retrieving user with ID: " + userId);
String userData = database.find(userId);
logger.log("User retrieved successfully");
return userData;
} catch (Exception e) {
logger.log("Error retrieving user: " + e.getMessage());
throw e;
}
}
public void updateUser(String userId, String newData) {
logger.log("Updating user " + userId);
database.update(userId, newData);
logger.log("User updated successfully");
}
public void deleteUser(String userId) {
logger.log("Deleting user " + userId);
database.delete(userId);
emailService.sendEmail("admin@example.com", "User Deleted", "User " + userId + " has been deleted");
logger.log("User deleted successfully");
}
}
Bước 4: Dependency Injection và Configuration¶
// Configuration class (Dependency Injection Container)
class ApplicationConfig {
public static UserService createUserService(String databaseType) {
Database database = createDatabase(databaseType);
Logger logger = new ConsoleLogger();
EmailService emailService = new SMTPEmailService();
return new UserService(database, logger, emailService);
}
private static Database createDatabase(String type) {
switch (type.toLowerCase()) {
case "mysql":
return new MySQLDatabase();
case "postgresql":
return new PostgreSQLDatabase();
case "memory":
return new InMemoryDatabase();
default:
throw new IllegalArgumentException("Unknown database type: " + type);
}
}
}
// Application entry point
public class Main {
public static void main(String[] args) {
// Production - sử dụng MySQL
UserService mysqlUserService = ApplicationConfig.createUserService("mysql");
mysqlUserService.createUser("John Doe");
System.out.println(mysqlUserService.getUser("1"));
System.out.println("\n" + "=".repeat(50) + "\n");
// Development - sử dụng In-memory
UserService memoryUserService = ApplicationConfig.createUserService("memory");
memoryUserService.createUser("Jane Smith");
System.out.println(memoryUserService.getUser("1"));
System.out.println("\n" + "=".repeat(50) + "\n");
// Staging - sử dụng PostgreSQL
UserService postgresUserService = ApplicationConfig.createUserService("postgresql");
postgresUserService.createUser("Bob Johnson");
// Dễ dàng switch giữa các implementations
System.out.println("\nSwitching databases without changing UserService code!");
}
}
Advanced: Framework-style Dependency Injection¶
Sử dụng Annotations (giống Spring Framework)¶
// Annotation để đánh dấu dependencies
@interface Inject {}
@interface Component {}
@interface Service {}
// Service classes
@Service
class UserService {
@Inject
private Database database;
@Inject
private Logger logger;
// Business logic methods...
}
// Simple DI Container
class DIContainer {
private Map<Class<?>, Object> services = new HashMap<>();
public <T> void register(Class<T> serviceClass, T implementation) {
services.put(serviceClass, implementation);
}
@SuppressWarnings("unchecked")
public <T> T get(Class<T> serviceClass) {
return (T) services.get(serviceClass);
}
public <T> T createInstance(Class<T> clazz) {
try {
T instance = clazz.getDeclaredConstructor().newInstance();
// Inject dependencies using reflection
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Inject.class)) {
field.setAccessible(true);
Object dependency = services.get(field.getType());
if (dependency != null) {
field.set(instance, dependency);
}
}
}
return instance;
} catch (Exception e) {
throw new RuntimeException("Failed to create instance", e);
}
}
}
Testing với DIP¶
Unit Testing trở nên dễ dàng¶
// Mock implementations cho testing
class MockDatabase implements Database {
private List<String> saveOperations = new ArrayList<>();
private Map<String, String> findResults = new HashMap<>();
@Override
public void save(String data) {
saveOperations.add(data);
}
@Override
public String find(String id) {
return findResults.getOrDefault(id, "Mock user: " + id);
}
@Override
public void delete(String id) {
// Mock implementation
}
@Override
public void update(String id, String data) {
// Mock implementation
}
// Test helpers
public List<String> getSaveOperations() {
return saveOperations;
}
public void setFindResult(String id, String result) {
findResults.put(id, result);
}
}
class MockLogger implements Logger {
private List<String> logs = new ArrayList<>();
@Override
public void log(String message) {
logs.add(message);
}
public List<String> getLogs() {
return logs;
}
}
// Unit test
class UserServiceTest {
@Test
public void testCreateUser() {
// Arrange
MockDatabase mockDatabase = new MockDatabase();
MockLogger mockLogger = new MockLogger();
EmailService mockEmailService = (to, subject, body) -> {}; // Lambda mock
UserService userService = new UserService(mockDatabase, mockLogger, mockEmailService);
// Act
userService.createUser("Test User");
// Assert
assertEquals(1, mockDatabase.getSaveOperations().size());
assertEquals("Test User", mockDatabase.getSaveOperations().get(0));
assertTrue(mockLogger.getLogs().stream()
.anyMatch(log -> log.contains("Creating user")));
}
@Test
public void testCreateUserWithEmptyData() {
// Arrange
MockDatabase mockDatabase = new MockDatabase();
MockLogger mockLogger = new MockLogger();
EmailService mockEmailService = (to, subject, body) -> {};
UserService userService = new UserService(mockDatabase, mockLogger, mockEmailService);
// Act & Assert
assertThrows(IllegalArgumentException.class, () -> {
userService.createUser("");
});
assertEquals(0, mockDatabase.getSaveOperations().size());
}
}
DIP trong các Architecture Patterns¶
Clean Architecture¶
┌─────────────────────────────────────────────────────────────┐
│ Frameworks & Drivers │
│ (Web, Database, External) │
└──────────────────────┬──────────────────────────────────────┘
│ implements
┌─────────────────────────────────────────────────────────────┐
│ Interface Adapters │
│ (Controllers, Gateways, Presenters) │
└──────────────────────┬──────────────────────────────────────┘
│ implements
┌─────────────────────────────────────────────────────────────┐
│ Application Business Rules │
│ (Use Cases) │
└──────────────────────┬──────────────────────────────────────┘
│ depends on
┌─────────────────────────────────────────────────────────────┐
│ Enterprise Business Rules │
│ (Entities) │
└─────────────────────────────────────────────────────────────┘
Hexagonal Architecture (Ports & Adapters)¶
// Port (Interface) - định nghĩa contract
interface UserRepository {
void save(User user);
User findById(String id);
}
// Core Domain (không phụ thuộc infrastructure)
class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void createUser(User user) {
// Business logic
userRepository.save(user);
}
}
// Adapter (Implementation) - infrastructure concern
class MySQLUserRepository implements UserRepository {
// MySQL specific implementation
}
class MongoUserRepository implements UserRepository {
// MongoDB specific implementation
}
Benefits của DIP¶
Loose Coupling¶
- Modules độc lập với nhau
- Thay đổi implementation không ảnh hưởng business logic
High Testability¶
- Mock dependencies dễ dàng
- Unit test isolated và fast
Flexibility & Maintainability¶
- Swap implementations without code changes
- Add new implementations theo OCP
Reusability¶
- Business logic reuse với different infrastructures
- Portable across different environments
Support for Modern Architectures¶
- Microservices
- Cloud-native applications
- Clean Architecture
Common Patterns với DIP¶
1. Dependency Injection¶
- Constructor Injection
- Setter Injection
- Interface Injection
2. Service Locator¶
- Registry pattern cho dependencies
- Runtime resolution
3. Factory Pattern¶
- Abstract Factory cho families of objects
- Factory Method cho single objects
4. Strategy Pattern¶
- Interchangeable algorithms
- Runtime strategy selection
Kết luận¶
Dependency Inversion Principle là nền tảng của:
- Modern software architecture
- Testable code design
- Flexible and maintainable systems
- Dependency Injection frameworks
Key takeaway: "Depend on abstractions, not concretions" - đây là nguyên tắc vàng để xây dựng phần mềm có khả năng mở rộng và bảo trì tốt!
SOLID Summary¶
Với DIP, chúng ta đã hoàn thành bộ nguyên lý SOLID:
- SRP: One reason to change
- OCP: Open for extension, closed for modification
- LSP: Substitutable subclasses
- ISP: Focused interfaces
- DIP: Depend on abstractions
Tất cả 5 nguyên lý này kết hợp tạo nên foundation cho clean, maintainable, và scalable software!