Dependency Management

บทนำ: “Dependency” คืออะไร?

Dependency = Relationship ระหว่าง classes โดย class หนึ่ง depend บน class อื่น

textตัวอย่างง่ายๆ:

Class A needs Class B to work
    ↓
A depends on B
    ↓
B is a dependency of A

❌ ปัญหา: Tight Coupling
- A hardcoded to use B
- ต้อง change A ถ้า B เปลี่ยน
- ต้อง create B ก่อนใช้ A
- ยากต่อการ test

✓ วิธีแก้: Loose Coupling + Dependency Injection
- A depends on abstraction (interface)
- B implements interface
- Someone else provides B to A
- Easy to swap B สำหรับ mock version

Dependency Management = Organizing and controlling how classes depend on each other


The Dependency Problem

❌ Problem 1: Tight Coupling (Hard Dependencies)

java// ❌ ไม่ดี: OrderService "hardcoded" ต้อง EmailService
public class OrderService {
    
    public void processOrder(Order order) {
        // Step 1: Calculate total
        double total = calculateTotal(order);
        
        // Step 2: Send confirmation
        // ← Problem: Service creates its own EmailService
        EmailService emailService = new EmailService();
        emailService.send(order.getCustomer().getEmail(), 
                         "Order confirmed: $" + total);
        
        // Step 3: Save order
        // ← Problem: Service creates its own database connection
        OrderRepository repository = new OrderRepositoryImpl();
        repository.save(order);
    }
}

// PROBLEMS:
// ❌ OrderService ต้อง know EmailService exists
// ❌ OrderService ต้อง know how to create EmailService
// ❌ Can't use different email service (SMTP vs SendGrid vs AWS SES)
// ❌ Can't mock EmailService for testing
// ❌ Hard to change email service later
// ❌ ต้อง change OrderService ถ้า EmailService constructor เปลี่ยน

❌ Problem 2: Hard to Test

java// ❌ ยากต่อการ test เพราะ dependencies ยุ่ง
@Test
public void testProcessOrder() {
    OrderService service = new OrderService();
    
    Order order = new Order();
    order.setCustomerId("CUST-001");
    order.setTotal(100.0);
    
    // Test ต้อง:
    // 1. Have real database running
    // 2. Have email service configured
    // 3. Wait for email to send
    // 4. Clean up database after test
    
    service.processOrder(order);
    
    // Can't verify: Did email actually send?
    // Can't test error case: What if email service fails?
    // Test is SLOW and FLAKY
}

❌ Problem 3: Hard to Change

java// Company decides to change from SMTP to SendGrid
// ❌ Problem: Must change OrderService code

public class OrderService {
    public void processOrder(Order order) {
        // ← Had to modify this!
        SendGridService emailService = new SendGridService();
        emailService.send(...);
    }
}

// ❌ Why is this bad?
// - OrderService's responsibility is to process orders
// - But we're changing it because email provider changed
// - This violates Single Responsibility Principle
// - More changes = more chances for bugs

Solution 1: Dependency Injection (Constructor Injection)

✓ Constructor-Based DI

java// ✓ Interface for email service
public interface EmailService {
    void send(String email, String message);
}

// ✓ SMTP implementation
public class SMTPEmailService implements EmailService {
    @Override
    public void send(String email, String message) {
        System.out.println("Sending via SMTP: " + email);
        // SMTP logic
    }
}

// ✓ SendGrid implementation
public class SendGridService implements EmailService {
    @Override
    public void send(String email, String message) {
        System.out.println("Sending via SendGrid: " + email);
        // SendGrid API logic
    }
}

// ✓ Repository interface
public interface OrderRepository {
    void save(Order order);
    Order findById(String id);
}

// ✓ Repository implementation
public class OrderRepositoryImpl implements OrderRepository {
    @Override
    public void save(Order order) {
        // Database logic
    }
    
    @Override
    public Order findById(String id) {
        // Query logic
        return null;
    }
}

// ✓ REFACTORED: Dependency Injection
public class OrderService {
    
    // Dependencies injected via constructor
    private EmailService emailService;
    private OrderRepository orderRepository;
    
    // Constructor injection
    public OrderService(EmailService emailService, OrderRepository orderRepository) {
        this.emailService = emailService;
        this.orderRepository = orderRepository;
    }
    
    public void processOrder(Order order) {
        // Step 1: Calculate total
        double total = calculateTotal(order);
        
        // Step 2: Send confirmation (use injected service)
        emailService.send(
            order.getCustomer().getEmail(),
            "Order confirmed: $" + total
        );
        
        // Step 3: Save order (use injected repository)
        orderRepository.save(order);
    }
    
    private double calculateTotal(Order order) {
        // Business logic
        return order.getItems().stream()
            .mapToDouble(item -> item.getPrice() * item.getQuantity())
            .sum();
    }
}

// ✓ USAGE: Wire up dependencies

// Option 1: Use SMTP
EmailService smtpEmail = new SMTPEmailService();
OrderRepository repository = new OrderRepositoryImpl();
OrderService service = new OrderService(smtpEmail, repository);
service.processOrder(order);

// Option 2: Switch to SendGrid (no code change in OrderService!)
EmailService sendGridEmail = new SendGridService();
OrderService service2 = new OrderService(sendGridEmail, repository);
service2.processOrder(order);

// BENEFITS:
// ✓ OrderService doesn't know which EmailService implementation
// ✓ Easy to swap implementations
// ✓ Easy to test (can inject mocks)

ตัวอย่างที่ 1: Testing with Dependency Injection

java// ✓ Testing with mocks becomes easy

@Test
public void testProcessOrderSendEmail() {
    // Mock email service
    EmailService mockEmail = mock(EmailService.class);
    
    // Mock repository
    OrderRepository mockRepository = mock(OrderRepository.class);
    
    // Create service with mocks
    OrderService service = new OrderService(mockEmail, mockRepository);
    
    // Create test order
    Order order = new Order();
    order.setCustomerId("CUST-001");
    Customer customer = new Customer();
    customer.setEmail("[email protected]");
    order.setCustomer(customer);
    
    OrderItem item = new OrderItem();
    item.setPrice(50.0);
    item.setQuantity(2);
    order.setItems(List.of(item));
    
    // Execute
    service.processOrder(order);
    
    // Verify: Email was sent
    verify(mockEmail).send(
        "[email protected]",
        contains("Order confirmed: $100.0")
    );
    
    // Verify: Order was saved
    verify(mockRepository).save(order);
}

@Test
public void testProcessOrderEmailFails() {
    // Mock email service that throws exception
    EmailService mockEmail = mock(EmailService.class);
    doThrow(new RuntimeException("Email service down"))
        .when(mockEmail).send(anyString(), anyString());
    
    OrderRepository mockRepository = mock(OrderRepository.class);
    
    OrderService service = new OrderService(mockEmail, mockRepository);
    Order order = new Order();
    
    // Should handle email failure gracefully
    try {
        service.processOrder(order);
        fail("Should throw exception");
    } catch (Exception e) {
        assertEquals("Email service down", e.getMessage());
    }
    
    // Order should NOT be saved if email fails
    verify(mockRepository, never()).save(order);
}

// BENEFITS OF TESTING:
// ✓ Fast: No real email sent, no real database
// ✓ Reliable: Not dependent on external services
// ✓ Focused: Test OrderService logic only
// ✓ Complete: Can test error cases easily

Solution 2: Setter Injection

java// Alternative: Setter-based DI

public class OrderService {
    
    private EmailService emailService;
    private OrderRepository orderRepository;
    
    // Setter injection
    public void setEmailService(EmailService emailService) {
        this.emailService = emailService;
    }
    
    public void setOrderRepository(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
    
    public void processOrder(Order order) {
        if (emailService == null || orderRepository == null) {
            throw new IllegalStateException("Dependencies not set");
        }
        
        // Process order...
    }
}

// Usage
OrderService service = new OrderService();
service.setEmailService(new SMTPEmailService());
service.setOrderRepository(new OrderRepositoryImpl());
service.processOrder(order);

// COMPARE: Constructor vs Setter
// Constructor:
//   ✓ Mandatory dependencies
//   ✓ Immutable
//   ✗ Many parameters → constructor becomes long
//
// Setter:
//   ✓ Optional dependencies
//   ✓ Flexible
//   ✗ Object can be incomplete
//   ✗ Multiple steps to initialize

Solution 3: Service Locator (Anti-Pattern)

java// ⚠️ Service Locator: Works but NOT recommended

public class ServiceLocator {
    private static Map<Class<?>, Object> services = new HashMap<>();
    
    public static void register(Class<?> serviceClass, Object implementation) {
        services.put(serviceClass, implementation);
    }
    
    public static <T> T getService(Class<T> serviceClass) {
        return (T) services.get(serviceClass);
    }
}

// Usage (not recommended)
public class OrderService {
    
    public void processOrder(Order order) {
        // Fetch dependencies from locator
        EmailService emailService = ServiceLocator.getService(EmailService.class);
        OrderRepository repository = ServiceLocator.getService(OrderRepository.class);
        
        // Process order...
    }
}

// Setup
ServiceLocator.register(EmailService.class, new SMTPEmailService());
ServiceLocator.register(OrderRepository.class, new OrderRepositoryImpl());

// ⚠️ PROBLEMS:
// ✗ Hidden dependencies (not obvious what OrderService needs)
// ✗ Hard to test (must configure ServiceLocator)
// ✗ Coupling to ServiceLocator
// ✗ Global state (problematic for parallel testing)
// ✓ Better: Use Dependency Injection Frameworks

ตัวอย่างที่ 2: Dependency Chain

java// Real-world: Dependencies have their own dependencies

// ✓ Dependency hierarchy

public class EmailService {
    // EmailService depends on SMTPClient
    private SMTPClient smtpClient;
    
    public EmailService(SMTPClient smtpClient) {
        this.smtpClient = smtpClient;
    }
    
    public void send(String email, String message) {
        smtpClient.connect();
        smtpClient.sendMessage(email, message);
        smtpClient.disconnect();
    }
}

public class SMTPClient {
    // SMTPClient depends on SSLConnection
    private SSLConnection sslConnection;
    
    public SMTPClient(SSLConnection sslConnection) {
        this.sslConnection = sslConnection;
    }
    
    public void connect() {
        sslConnection.initialize();
    }
}

public class OrderService {
    private EmailService emailService;
    private OrderRepository repository;
    
    public OrderService(EmailService emailService, OrderRepository repository) {
        this.emailService = emailService;
        this.repository = repository;
    }
}

// ✓ Manual wiring (complex!)
SSLConnection sslConnection = new SSLConnection();
SMTPClient smtpClient = new SMTPClient(sslConnection);
EmailService emailService = new EmailService(smtpClient);
OrderRepository repository = new OrderRepositoryImpl();
OrderService service = new OrderService(emailService, repository);

// ❌ Problem: Too many lines just to create service
// ✓ Solution: Dependency Injection Framework (Spring, Guice, etc.)

Solution 4: Dependency Injection Framework (Spring)

java// ✓ With Spring Framework (DI handled automatically)

@Component
public class SMTPEmailService implements EmailService {
    @Override
    public void send(String email, String message) {
        System.out.println("Sending via SMTP");
    }
}

@Repository
public class OrderRepositoryImpl implements OrderRepository {
    @Override
    public void save(Order order) {
        System.out.println("Saving to database");
    }
}

@Service
public class OrderService {
    
    // Dependencies auto-injected by Spring
    private EmailService emailService;
    private OrderRepository orderRepository;
    
    @Autowired
    public OrderService(EmailService emailService, OrderRepository orderRepository) {
        this.emailService = emailService;
        this.orderRepository = orderRepository;
    }
    
    public void processOrder(Order order) {
        emailService.send(order.getCustomer().getEmail(), "Order confirmed");
        orderRepository.save(order);
    }
}

@RestController
public class OrderController {
    
    // Spring automatically injects OrderService
    private OrderService orderService;
    
    @Autowired
    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }
    
    @PostMapping("/orders")
    public void createOrder(@RequestBody Order order) {
        orderService.processOrder(order);
    }
}

// ✓ BENEFITS:
// ✓ Spring creates and wires everything automatically
// ✓ Just mark classes with annotations
// ✓ No manual wiring
// ✓ Easy to swap implementations (just change configuration)
// ✓ Easy to mock in tests

ตัวอย่างที่ 3: Dependency Configuration

java// Different configurations for different environments

// Configuration 1: Production
@Configuration
public class ProductionConfig {
    
    @Bean
    public EmailService emailService() {
        // Use real SMTP in production
        return new SMTPEmailService();
    }
    
    @Bean
    public OrderRepository orderRepository() {
        // Use real database in production
        return new OrderRepositoryImpl();
    }
}

// Configuration 2: Testing
@Configuration
public class TestingConfig {
    
    @Bean
    public EmailService emailService() {
        // Use mock email in testing
        return mock(EmailService.class);
    }
    
    @Bean
    public OrderRepository orderRepository() {
        // Use in-memory repository in testing
        return new InMemoryOrderRepository();
    }
}

// Usage: Spring switches config automatically
// @SpringBootTest
// public class OrderServiceTest {
//     
//     @Autowired
//     private OrderService orderService;
//     
//     @Autowired
//     private EmailService emailService;
//     
//     @Test
//     public void test() {
//         // emailService is mock version from TestingConfig
//     }
// }

// BENEFITS:
// ✓ Same code, different behaviors for prod vs test
// ✓ Easy to add new environments
// ✓ Configuration centralized

Best Practices: Dependency Management

textWHEN TO INJECT:
  ☑ Required dependencies (always needed)
  ☑ Dependencies that might change
  ☑ Dependencies that need to be mocked
  ☑ External services (database, API, email)

WHEN NOT TO INJECT:
  ☐ Stateless utility classes
  ☐ Primitive values
  ☐ Collections/data structures
  ☐ Value objects (Email, PhoneNumber)

INJECTION METHODS (Priority):
  1. Constructor injection (preferred)
     - Mandatory dependencies
     - Immutable
     - Clear dependencies
  
  2. Setter injection
     - Optional dependencies
     - Flexible
     - More steps to initialize
  
  3. Field injection (least preferred)
     - Less testable
     - Hidden dependencies
     - Not immutable

RULES:
  ✓ Depend on abstractions (interfaces)
  ✓ Use DI containers/frameworks
  ✓ Centralize configuration
  ✓ Avoid circular dependencies
  ✓ Keep dependency graphs shallow

MISTAKES:
  ✗ Hardcoding dependencies
  ✗ Using global state
  ✗ Service Locator pattern
  ✗ Circular dependencies
  ✗ Too many constructor parameters (> 5)
  ✗ Depending on concrete classes

Identifying Dependency Issues

textRED FLAGS:

❌ new keyword inside class
   UserService userService = new UserService();
   → Should be injected

❌ Tight coupling
   Only works with specific implementation
   → Should depend on interface

❌ Hard to test
   Must set up external services
   → Missing dependency injection

❌ Hard to configure
   Hardcoded URLs, credentials
   → Should use configuration

❌ Multiple constructors
   Different ways to create object
   → Unclear which one to use

GREEN FLAGS:

✓ Dependencies passed to constructor
  public MyService(Dependency dep) { ... }

✓ No new keyword for dependencies
  Only for creating business objects

✓ Depends on interfaces
  private EmailService emailService;

✓ Easy to mock in tests
  Can pass mock implementation

✓ Easy to configure
  Change behavior via config

สรุป

Dependency Management ไม่ใช่ “advanced topic” แต่เป็น fundamental practice ที่ separate amateur code จาก professional code:

Core Concept:

  • Dependency = Class A ต้อง Class B
  • Tight Coupling = A knows exactly how to create B (BAD)
  • Loose Coupling = A depends on abstraction (GOOD)
  • Dependency Injection = Someone else provides B to A

3 Injection Methods:

  1. Constructor (recommended) – Mandatory dependencies
  2. Setter (alternative) – Optional dependencies
  3. Service Locator (anti-pattern) – Avoid!

Benefits of Good Dependency Management:

  • Flexibility – Easy to swap implementations
  • Testability – Easy to mock dependencies
  • Maintainability – Change implementation ไม่ affect code
  • Scalability – Multiple teams can work independently

Principles:

  • Depend on abstractions (interfaces)
  • Use Dependency Injection
  • Keep configuration centralized
  • Avoid circular dependencies

Dependency Management คือ “discipline” ที่ pay off enormously – code ที่ manage dependencies อย่างดี เป็น code ที่ easy to understand, easy to test, easy to change และ easy to reuse ผลประโยชน์นี้รวมกันแล้ว ทำให้ project succeed long-term แทนที่จะ struggle with technical debt