Dependency Injection (DI) & Inversion of Control (IoC)

บทนำ: ปัญหาของ 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);
    }
}

ปัญหาของแบบนี้:

  1. Tight Coupling – UserService tied to FileLogger
  2. Hard to Test – ไม่สามารถ mock Logger ได้ ต้องใช้ FileLogger จริง
  3. Hard to Change – ต้องแก้ UserService ถ้าต้องเปลี่ยนจาก FileLogger เป็น DatabaseLogger
  4. 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 InjectionSetter Injection
Dependencies mandatoryDependencies optional
ImmutableMutable
Clear dependenciesHidden dependencies
Can’t create incomplete objectCan create partial object
Better for required depsBetter 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

ข้อดี:

  1. Loose Coupling – classes ไม่รู้เกี่ยวกับ concrete implementations
  2. Easier Testing – mock dependencies ได้ง่าย
  3. Easy to Switch – เปลี่ยน implementation โดยไม่แก้ code
  4. Centralized Configuration – dependencies อยู่ที่เดียว
  5. Explicit Dependencies – ดู constructor/setters รู้ว่าต้องอะไร

ข้อควรระวัง:

  1. Learning Curve – ต้องเข้าใจ concept นี้
  2. Initial Complexity – Setup ครั้งแรกซับซ้อนกว่า
  3. Performance – reflection ใช้เวลา (แต่ usually ไม่สำคัญ)
  4. Magic – บางครั้ง framework “เวทมนต์” ทำอะไร ยากจะ debug
  5. 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:

  1. Depend on abstractions – ใช้ interfaces ไม่ใช่ concrete classes
  2. Keep dependencies simple – ไม่ต้อง inject ทั้ง universe
  3. Validate dependencies – ตรวจสอบ null
  4. 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 ได้ดี