Bỏ qua

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:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. 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: UserService gắn chặt với MySQLDatabase
  • 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: UserService chỉ 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!