บทนำ: “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:
- Constructor (recommended) – Mandatory dependencies
- Setter (alternative) – Optional dependencies
- 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
