บทนำ: ปัญหาของ Tight Coupling
ในบทเรียนที่ผ่านมา เราเห็นว่า Factory Pattern ช่วยลดการพึ่งพา (coupling) ระหว่าง classes แต่ยังมีปัญหาที่ลึกขึ้น
ลองพิจารณา application ที่มีการบันทึก logs เราคิดว่า code นี้ยืดหยุ่น:
javapublic interface Logger {
void log(String message);
}
public class FileLogger implements Logger {
@Override
public void log(String message) {
System.out.println("Logging to file: " + message);
}
}
public class UserService {
private Logger logger; // ← Dependency
public UserService() {
// ❌ ปัญหา: UserService "สร้าง" Logger เอง
this.logger = new FileLogger();
}
public void createUser(String name) {
logger.log("Creating user: " + name);
System.out.println("User created: " + name);
}
}
ปัญหาของแบบนี้:
- Tight Coupling – UserService tied to FileLogger
- Hard to Test – ไม่สามารถ mock Logger ได้ ต้องใช้ FileLogger จริง
- Hard to Change – ต้องแก้ UserService ถ้าต้องเปลี่ยนจาก FileLogger เป็น DatabaseLogger
- Responsibility Mixing – UserService ต้องรู้ว่า Logger ใช้ concrete class ไหน
Inversion of Control (IoC): เปลี่ยนทิศทางการไหลของการควบคุม
แนวคิดพื้นฐาน
Inversion of Control หมายถึงการเปลี่ยนทิศทางการไหลของการควบคุม object ปกติแล้ว object เป็น “ผู้ควบคุม” และสร้างสิ่งที่มันต้องการเอง แต่กับ IoC object ให้ “ผู้ควบคุม” ภายนอก (เรียกว่า Container หรือ Framework) ที่จัดการ
ลองนึกการเปรียบเทียบ:
Without IoC (Traditional):
textUserService: "ฉันต้อง Logger ฉันจะสร้างเองเลย"
↓
UserService: new FileLogger();
↓
UserService: มี FileLogger ตอนนี้
With IoC:
textContainer: "วันนี้ใครต้อง Logger บอกฉันก่อน"
↓
UserService: "ฉันต้อง Logger"
↓
Container: "นี่ Logger ที่ฉันเตรียมให้"
↓
Container: inject Logger ไป UserService
↓
UserService: มี Logger ที่ Container ให้
Control ผ่านมาจาก object (UserService) ไป Container แล้ว นี่คือ “Inversion”
Dependency Injection: การใช้ IoC
Dependency Injection (DI) คือ implementation หนึ่งของ IoC โดยการ “inject” dependencies ที่ object ต้องการ
มีวิธี 3 วิธีในการทำ DI:
ตัวอย่างที่ 1: Constructor Injection
วิธีที่สามารถคาดการณ์ได้
java// Interface สำหรับ logger
public interface Logger {
void log(String message);
}
// Implementations
public class FileLogger implements Logger {
@Override
public void log(String message) {
System.out.println("[FILE] " + message);
}
}
public class ConsoleLogger implements Logger {
@Override
public void log(String message) {
System.out.println("[CONSOLE] " + message);
}
}
public class DatabaseLogger implements Logger {
@Override
public void log(String message) {
System.out.println("[DATABASE] " + message);
}
}
// Service ที่ inject dependency ผ่าน constructor
public class UserService {
private Logger logger; // ← Dependency
// Constructor Injection: dependency ผ่านเข้ามาที่นี่
public UserService(Logger logger) {
this.logger = logger; // ← Receive dependency
}
public void createUser(String name) {
logger.log("Creating user: " + name);
System.out.println("User created: " + name);
}
public void deleteUser(String name) {
logger.log("Deleting user: " + name);
System.out.println("User deleted: " + name);
}
}
// การใช้งาน: Caller (หรือ Container) ตัดสินใจว่า Logger ชนิดไหน
public class ConstructorInjectionDemo {
public static void main(String[] args) {
System.out.println("=== With FileLogger ===");
UserService fileService = new UserService(new FileLogger());
fileService.createUser("Alice");
fileService.deleteUser("Alice");
System.out.println("\n=== With ConsoleLogger ===");
UserService consoleService = new UserService(new ConsoleLogger());
consoleService.createUser("Bob");
consoleService.deleteUser("Bob");
System.out.println("\n=== With DatabaseLogger ===");
UserService dbService = new UserService(new DatabaseLogger());
dbService.createUser("Charlie");
dbService.deleteUser("Charlie");
}
}
// OUTPUT:
// === With FileLogger ===
// [FILE] Creating user: Alice
// User created: Alice
// [FILE] Deleting user: Alice
// User deleted: Alice
//
// === With ConsoleLogger ===
// [CONSOLE] Creating user: Bob
// User created: Bob
// [CONSOLE] Deleting user: Bob
// User deleted: Bob
//
// === With DatabaseLogger ===
// [DATABASE] Creating user: Charlie
// User created: Charlie
// [DATABASE] Deleting user: Charlie
// User deleted: Charlie
ข้อดีของ Constructor Injection:
- Dependencies ชัดเจน – ดู constructor รู้ว่า service ต้องอะไร
- Immutable – dependencies ไม่เปลี่ยนแปลงหลัง construction
- Mandatory – ถ้าลืม pass dependency compile ไม่ผ่าน
- Testable – inject mock objects ได้ง่าย
Test กับ Mock Objects
java// Mock logger สำหรับ testing
public class MockLogger implements Logger {
private StringBuilder logs = new StringBuilder();
@Override
public void log(String message) {
logs.append(message).append("\n");
}
public String getAllLogs() {
return logs.toString();
}
}
// Unit test
public class UserServiceTest {
@org.junit.Test
public void testCreateUser() {
// สร้าง mock logger
MockLogger logger = new MockLogger();
// inject mock ไปยัง service
UserService service = new UserService(logger);
// ใช้ service
service.createUser("TestUser");
// verify behavior ผ่าน mock
assert logger.getAllLogs().contains("Creating user: TestUser");
System.out.println("✓ Test passed!");
}
}
ตัวอย่างที่ 2: Setter Injection
วิธีที่ยืดหยุ่น
นอกจาก constructor injection ยังมีอีกวิธีคือ setter injection:
java// Service ที่ใช้ setter injection
public class OrderService {
private Logger logger;
private NotificationService notificationService;
// Constructor ไม่ต้องมี dependencies ทั้งหมด
public OrderService() {
// Default: ไม่มี dependencies
}
// Setter ให้ set dependencies หลังจาก construction
public void setLogger(Logger logger) {
this.logger = logger;
}
public void setNotificationService(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void createOrder(String orderId) {
// ตรวจสอบว่า dependencies ที่ set แล้ว
if (logger != null) {
logger.log("Creating order: " + orderId);
}
if (notificationService != null) {
notificationService.notify("Order created: " + orderId);
}
System.out.println("Order created: " + orderId);
}
}
// Notification interface
public interface NotificationService {
void notify(String message);
}
public class EmailNotificationService implements NotificationService {
@Override
public void notify(String message) {
System.out.println("[EMAIL] " + message);
}
}
// ใช้งาน
public class SetterInjectionDemo {
public static void main(String[] args) {
// สร้าง service
OrderService service = new OrderService();
// Set dependencies หลังจาก creation
service.setLogger(new ConsoleLogger());
service.setNotificationService(new EmailNotificationService());
// ใช้งาน
service.createOrder("ORD-001");
}
}
// OUTPUT:
// [CONSOLE] Creating order: ORD-001
// [EMAIL] Order created: ORD-001
// Order created: ORD-001
Constructor vs Setter:
| Constructor Injection | Setter Injection |
|---|---|
| Dependencies mandatory | Dependencies optional |
| Immutable | Mutable |
| Clear dependencies | Hidden dependencies |
| Can’t create incomplete object | Can create partial object |
| Better for required deps | Better for optional deps |
Best practice: ใช้ constructor injection สำหรับ required dependencies และ setter injection สำหรับ optional
ตัวอย่างที่ 3: Interface Injection
วิธีที่ explicit
java// Interface บ่งชี้ว่า class ต้อง logger
public interface LoggerInjectable {
void setLogger(Logger logger);
}
// Service implement interface
public class PaymentService implements LoggerInjectable {
private Logger logger;
@Override
public void setLogger(Logger logger) {
this.logger = logger;
}
public void processPayment(String transactionId, double amount) {
if (logger != null) {
logger.log("Processing payment: " + transactionId + " - $" + amount);
}
System.out.println("Payment processed");
}
}
// Container ที่ inject dependencies
public class DIContainer {
public void injectDependencies(Object object, Logger logger) {
// ตรวจสอบว่า object implement LoggerInjectable
if (object instanceof LoggerInjectable) {
((LoggerInjectable) object).setLogger(logger);
}
}
}
// ใช้งาน
public class InterfaceInjectionDemo {
public static void main(String[] args) {
DIContainer container = new DIContainer();
PaymentService service = new PaymentService();
// Container inject logger ผ่าน interface
container.injectDependencies(service, new ConsoleLogger());
service.processPayment("TXN-001", 99.99);
}
}
// OUTPUT:
// [CONSOLE] Processing payment: TXN-001 - $99.99
// Payment processed
ตัวอย่างที่ 4: IoC Container – Dependency Management
Centralized Dependency Management
IoC containers เช่น Spring ทำให้การ manage dependencies ตัวอักษรและง่าย:
java// สมมติว่าเราทำ simple IoC container (Spring ทำแบบนี้ แต่ซับซ้อนกว่า)
// Annotation เพื่อ mark injectable
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.CONSTRUCTOR)
public @interface Inject {
}
// Service classes
public class UserRepository {
public void save(String user) {
System.out.println("Saving user: " + user);
}
}
public class UserService {
private UserRepository repository;
private Logger logger;
// Constructor marked with @Inject
@Inject
public UserService(UserRepository repository, Logger logger) {
this.repository = repository;
this.logger = logger;
}
public void createUser(String name) {
logger.log("Creating user: " + name);
repository.save(name);
}
}
// Simple IoC Container
public class SimpleIoCContainer {
private Map<Class<?>, Object> singletons = new HashMap<>();
// Register singleton
public <T> void register(Class<T> type, T instance) {
singletons.put(type, instance);
}
// Get instance - ถ้ายังไม่มี สร้างใหม่
@SuppressWarnings("unchecked")
public <T> T get(Class<T> type) {
if (singletons.containsKey(type)) {
return (T) singletons.get(type);
}
// Autowire: สร้างด้วยการ inject dependencies
T instance = autowire(type);
singletons.put(type, instance);
return instance;
}
@SuppressWarnings("unchecked")
private <T> T autowire(Class<T> type) {
try {
// หา constructor ที่มี @Inject
java.lang.reflect.Constructor<?>[] constructors = type.getDeclaredConstructors();
for (java.lang.reflect.Constructor<?> constructor : constructors) {
if (constructor.isAnnotationPresent(Inject.class)) {
Class<?>[] paramTypes = constructor.getParameterTypes();
Object[] params = new Object[paramTypes.length];
// ให้ได้ dependencies ทั้งหมด
for (int i = 0; i < paramTypes.length; i++) {
params[i] = get(paramTypes[i]);
}
constructor.setAccessible(true);
return (T) constructor.newInstance(params);
}
}
// ถ้าไม่มี @Inject ให้ใช้ default constructor
return type.newInstance();
} catch (Exception e) {
throw new RuntimeException("Failed to autowire " + type.getName(), e);
}
}
}
// ใช้งาน
public class IoCContainerDemo {
public static void main(String[] args) {
// สร้าง container
SimpleIoCContainer container = new SimpleIoCContainer();
// Register dependencies
container.register(Logger.class, new ConsoleLogger());
container.register(UserRepository.class, new UserRepository());
// ขอ UserService - container จะ autowire dependencies
UserService service = container.get(UserService.class);
// ใช้งาน
service.createUser("Alice");
}
}
// OUTPUT:
// [CONSOLE] Creating user: Alice
// Saving user: Alice
IoC Container ให้ประโยชน์:
- Centralized – manage dependencies ที่เดียว
- Automatic – container จัดการ wiring
- Flexible – เปลี่ยน implementation โดยไม่แก้ code
- Scalable – handle complex dependency graphs
ตัวอย่างที่ 5: Spring Framework – Production IoC
Real-world Example
ในการพัฒนาจริง ใช้ framework เช่น Spring แทน:
java// Spring Annotations
import org.springframework.stereotype.Service;
import org.springframework.stereotype.Repository;
import org.springframework.beans.factory.annotation.Autowired;
// Interface
public interface UserRepository {
void save(String user);
String findById(String id);
}
// Repository implementation
@Repository
public class UserRepositoryImpl implements UserRepository {
@Override
public void save(String user) {
System.out.println("Saving user to database: " + user);
}
@Override
public String findById(String id) {
return "User: " + id;
}
}
// Logger service
@Service
public class AuditLogger {
public void log(String action) {
System.out.println("[AUDIT] " + action);
}
}
// Business service
@Service
public class UserService {
// @Autowired ให้ Spring inject dependencies
@Autowired
private UserRepository userRepository;
@Autowired
private AuditLogger auditLogger;
// ไม่ต้อง constructor - Spring จัดการ
public void createUser(String name) {
auditLogger.log("Creating user: " + name);
userRepository.save(name);
System.out.println("User created successfully");
}
public void getUser(String id) {
auditLogger.log("Getting user: " + id);
String user = userRepository.findById(id);
System.out.println("Retrieved: " + user);
}
}
// Application configuration
public class Application {
public static void main(String[] args) {
// Spring Application Context (container)
// ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
// หรือ ใช้ annotation-based configuration
// ขอ service จาก Spring
// UserService service = context.getBean(UserService.class);
// Spring อัตโนมัติ:
// 1. สร้าง UserRepositoryImpl
// 2. สร้าง AuditLogger
// 3. Inject ทั้ง 2 ไปยัง UserService
// 4. Return service ที่พร้อมใช้งาน
// service.createUser("Alice");
// service.getUser("001");
}
}
Dependencies Flow: จากลงไป
ตัวอย่างสถาปัตยกรรมของ Application
┌─────────────────────────────────────────┐
│ Application Layer │
│ ┌─────────────────────────────────┐ │
│ │ Spring Application Context │ │
│ │ (IoC Container) │ │
│ │ │ │
│ │ 1. Scan classes (หา @Service) │ │
│ │ 2. Detect @Autowired │ │
│ │ 3. Resolve dependencies │ │
│ │ 4. Wire dependencies │ │
│ │ 5. Create managed beans │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
│
├─── Injects ───┐
│ └─────────┐
│ │
┌──────────────┐ ┌──────────────────────┐
│ UserService │ │ UserRepositoryImpl │
│ │ │ │
│ @Autowired │ │ @Repository │
│ repository │◄───│ │
│ logger │ │ save() │
│ │ │ findById() │
└──────────────┘ └──────────────────────┘
│
└─── Injects ───┐
│
┌──────────────┐
│ AuditLogger │
│ │
│ @Service │
│ │
│ log() │
└──────────────┘
Benefits & Trade-offs
ประโยชน์ของ DI/IoC
ข้อดี:
- Loose Coupling – classes ไม่รู้เกี่ยวกับ concrete implementations
- Easier Testing – mock dependencies ได้ง่าย
- Easy to Switch – เปลี่ยน implementation โดยไม่แก้ code
- Centralized Configuration – dependencies อยู่ที่เดียว
- Explicit Dependencies – ดู constructor/setters รู้ว่าต้องอะไร
ข้อควรระวัง:
- Learning Curve – ต้องเข้าใจ concept นี้
- Initial Complexity – Setup ครั้งแรกซับซ้อนกว่า
- Performance – reflection ใช้เวลา (แต่ usually ไม่สำคัญ)
- Magic – บางครั้ง framework “เวทมนต์” ทำอะไร ยากจะ debug
- Over-engineering – สำหรับ simple applications อาจ overkill
Best Practices
เมื่อใช้ DI/IoC
ใช้ Constructor Injection สำหรับ:
- Required dependencies
- Immutable objects
- When you want explicit declarations
ใช้ Setter Injection สำหรับ:
- Optional dependencies
- When object can function without them
- When you need flexibility
Avoid:
- Circular dependencies (A needs B, B needs A)
- Too many constructor parameters (> 5 แนะนำใช้ Builder)
- Injecting concrete classes (inject interfaces)
- Mutating injected dependencies
Design Tips:
- Depend on abstractions – ใช้ interfaces ไม่ใช่ concrete classes
- Keep dependencies simple – ไม่ต้อง inject ทั้ง universe
- Validate dependencies – ตรวจสอบ null
- Document dependencies – ให้ชัดเจนว่าต้องอะไร
สรุป
Dependency Injection & Inversion of Control เป็น fundamental concepts ในการออกแบบ software ที่ maintainable:
Core Concepts:
- IoC – Control ไม่ stay ใน object แต่ไป container
- DI – Container “inject” dependencies ที่ object ต้องการ
- Loose Coupling – Objects ไม่ coupled กับ concrete implementations
- Testability – Mock objects ได้ง่ายสำหรับ testing
วิธี 3 แบบ:
- Constructor Injection – Most explicit, best for required deps
- Setter Injection – Flexible, best for optional deps
- Interface Injection – Formal, less common
Real-world:
- Spring Framework implement IoC/DI at scale
- @Autowired, @Component, @Service annotations
- Handle complex dependency graphs automatically
- Industry standard ใน Java enterprise applications
ตัวอย่างการใช้:
- UserService depends on UserRepository
- Instead of creating it, receive it via constructor
- Testing: inject MockRepository instead of real
- Changing implementations: change container config, not code
Dependency Injection และ IoC ไม่ใช่ mandatory แต่strongly recommended ในการพัฒนา applications ของขนาด medium หรือ larger มันทำให้ code maintainable, testable, และ flexible ที่สำคัญ สามารถ scale ได้ดี
