Dependency Injection trong Java: Hiểu và Áp Dụng

Dependency Injection là gì?

Dependency Injection (DI) là một design pattern quan trọng trong lập trình Java, giúp giảm sự phụ thuộc giữa các class và tăng tính linh hoạt của code. Thay vì một class tự tạo các đối tượng mà nó cần, các đối tượng này sẽ được “inject” (tiêm) vào từ bên ngoài.

Vấn đề khi không dùng DI

Hãy xem ví dụ sau:

public class UserService {
    private UserRepository repository = new UserRepository();
    
    public User getUser(int id) {
        return repository.findById(id);
    }
}

Vấn đề:

  • UserService phụ thuộc chặt chẽ vào UserRepository
  • Khó test vì không thể mock UserRepository
  • Khó thay đổi implementation của UserRepository

Các cách implement DI

1. Constructor Injection (Khuyên dùng)

public class UserService {
    private final UserRepository repository;
    
    public UserService(UserRepository repository) {
        this.repository = repository;
    }
    
    public User getUser(int id) {
        return repository.findById(id);
    }
}

Ưu điểm:

  • Dependencies rõ ràng
  • Có thể sử dụng final để đảm bảo immutability
  • Dễ test

2. Setter Injection

public class UserService {
    private UserRepository repository;
    
    public void setRepository(UserRepository repository) {
        this.repository = repository;
    }
    
    public User getUser(int id) {
        return repository.findById(id);
    }
}

Ưu điểm:

  • Linh hoạt, có thể thay đổi dependency sau khi khởi tạo

Nhược điểm:

  • Không thể dùng final
  • Object có thể ở trạng thái không hợp lệ nếu quên set dependency

3. Interface Injection

public interface RepositoryInjector {
    void injectRepository(UserRepository repository);
}

public class UserService implements RepositoryInjector {
    private UserRepository repository;
    
    @Override
    public void injectRepository(UserRepository repository) {
        this.repository = repository;
    }
}

DI với Spring Framework

Spring Framework tự động quản lý DI thông qua IoC Container:

@Service
public class UserService {
    private final UserRepository repository;
    
    @Autowired
    public UserService(UserRepository repository) {
        this.repository = repository;
    }
    
    public User getUser(int id) {
        return repository.findById(id);
    }
}

@Repository
public class UserRepository {
    public User findById(int id) {
        // Implementation
    }
}

Lưu ý: Từ Spring 4.3+, nếu class chỉ có 1 constructor, @Autowired có thể bỏ qua.

Lợi ích của DI

1. Loose Coupling

// Interface
public interface PaymentService {
    void processPayment(double amount);
}

// Implementations
public class CreditCardPayment implements PaymentService {
    public void processPayment(double amount) {
        // Credit card logic
    }
}

public class PayPalPayment implements PaymentService {
    public void processPayment(double amount) {
        // PayPal logic
    }
}

// Service
public class OrderService {
    private final PaymentService paymentService;
    
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
    
    public void checkout(double amount) {
        paymentService.processPayment(amount);
    }
}

2. Dễ Test

public class UserServiceTest {
    @Test
    public void testGetUser() {
        // Mock repository
        UserRepository mockRepo = mock(UserRepository.class);
        when(mockRepo.findById(1)).thenReturn(new User(1, "John"));
        
        // Inject mock
        UserService service = new UserService(mockRepo);
        
        // Test
        User user = service.getUser(1);
        assertEquals("John", user.getName());
    }
}

3. Tăng tính tái sử dụng

Dependencies có thể được tái sử dụng ở nhiều nơi mà không cần tạo lại.

DI thủ công vs Framework

DI thủ công

public class Application {
    public static void main(String[] args) {
        UserRepository repository = new UserRepository();
        UserService service = new UserService(repository);
        
        User user = service.getUser(1);
        System.out.println(user.getName());
    }
}

Với Spring

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
        // Spring tự động tạo và inject dependencies
    }
}

Best Practices

  1. Ưu tiên Constructor Injection cho required dependencies
  2. Dùng Interface thay vì concrete class
  3. Tránh Field Injection trong production code (khó test)
  4. Giữ dependencies ít nhất có thể - nếu class cần quá nhiều dependencies, có thể cần refactor
  5. Sử dụng final để đảm bảo immutability

Kết luận

Dependency Injection là một pattern quan trọng giúp code Java của bạn:

  • Dễ test hơn
  • Linh hoạt hơn
  • Dễ maintain hơn
  • Tuân thủ SOLID principles

Với Spring Framework, DI trở nên đơn giản và mạnh mẽ hơn. Hãy bắt đầu áp dụng DI vào dự án của bạn ngay hôm nay!