Dependency Inversion (หลักการย่อยของ SOLID)

บทนำ: ปัญหา “ขึ้นอยู่กับ Implementation”

ในการเขียนโปรแกรม บ่อยครั้งที่ code ของเรา “ขึ้นอยู่โดยตรง” กับ implementation ของ class อื่นๆ ซึ่งทำให้ระบบแข็ง (rigid) และเปลี่ยนยากเหลือเกิน

java// ❌ ตัวอย่างปัญหา: High-level module ขึ้นอยู่กับ low-level module
public class EmailService {
    public void sendEmail(String recipient, String message) {
        System.out.println("ส่งอีเมล: " + message);
    }
}

public class OrderService {
    private EmailService emailService;  // ← ขึ้นอยู่กับ EmailService โดยตรง!
    
    public OrderService() {
        this.emailService = new EmailService();
    }
    
    public void placeOrder(String customerEmail, String orderDetails) {
        // ตรรมชาติดำเนินการหลักอันดับแรก
        System.out.println("สั่งซื้อ: " + orderDetails);
        
        // ถ้าต้องการเปลี่ยนจาก Email เป็น SMS แล้ว? 
        // ต้องไปแก้ OrderService เพราะมันขึ้นอยู่กับ EmailService ตรงๆ
        emailService.sendEmail(customerEmail, "สั่งซื้อสำเร็จ");
    }
}

ปัญหา:

  • OrderService “ขึ้นอยู่” กับ EmailService ตรงๆ
  • ถ้าอยากเปลี่ยนจาก Email เป็น SMS/Notification ต้องแก้ OrderService
  • ไม่ยืดหยุ่น ไม่ reusable

Dependency Inversion คืออะไร?

Dependency Inversion = หลักการที่บอกว่า “อย่าให้ high-level module ขึ้นอยู่กับ low-level module” แต่ให้ ทั้งสองขึ้นอยู่กับ abstraction (interface)

textก่อน (Bad):
OrderService → EmailService (ขึ้นอยู่ตรงๆ)

หลัง (Good):
OrderService → NotificationService (interface)
             ← EmailService implements
             ← SMSService implements

กล่าวอีกแบบ:

  • เขียน code ให้ “เรียกใช้” interface ไม่ใช่ implementation class ที่ชัดเจน
  • ที่ใครเป็น implementer นั้น ตัดสินใจตอน runtime (หรือ configuration)

ตัวอย่างที่ 1: ก่อน vs หลังใช้ Dependency Inversion

❌ ก่อน: ขึ้นอยู่โดยตรง (Bad)

java// Low-level: Email sender
public class EmailService {
    public void sendEmail(String recipient, String message) {
        System.out.println("[Email] ส่งไปยัง " + recipient + ": " + message);
    }
}

// High-level: Business logic ขึ้นอยู่กับ EmailService
public class OrderService {
    private EmailService emailService = new EmailService();
    
    public void placeOrder(String customer, String order) {
        System.out.println("✓ สั่งซื้อ: " + order);
        emailService.sendEmail(customer, "สั่งซื้อสำเร็จ");
    }
}

public class Main {
    public static void main(String[] args) {
        OrderService service = new OrderService();
        service.placeOrder("[email protected]", "MacBook Pro");
    }
}

ปัญหา: OrderService ผูกติดกับ EmailService เฉพาะเจาะจง ไม่ยืดหยุ่น


✓ หลัง: ตามหลักการ Dependency Inversion (Good)

java// สัญญา (Abstraction): ทุก notification service ต้อง implement นี้
public interface NotificationService {
    void sendNotification(String recipient, String message);
}

// Implementation 1: Email
public class EmailService implements NotificationService {
    @Override
    public void sendNotification(String recipient, String message) {
        System.out.println("[Email] ส่งไปยัง " + recipient + ": " + message);
    }
}

// Implementation 2: SMS
public class SMSService implements NotificationService {
    @Override
    public void sendNotification(String recipient, String message) {
        System.out.println("[SMS] ส่ง SMS ไปยัง " + recipient + ": " + message);
    }
}

// High-level: Business logic ขึ้นอยู่กับ "สัญญา" ไม่ใช่ implementation
public class OrderService {
    private NotificationService notificationService;  // ← ใช้ interface!
    
    // Constructor Injection: นอกระบบตัดสินใจว่าใช้ service ไหน
    public OrderService(NotificationService service) {
        this.notificationService = service;
    }
    
    public void placeOrder(String customer, String order) {
        System.out.println("✓ สั่งซื้อ: " + order);
        // ไม่สนใจว่าเบื้องหลังใช้ Email หรือ SMS
        notificationService.sendNotification(customer, "สั่งซื้อสำเร็จ");
    }
}

public class Main {
    public static void main(String[] args) {
        // ✓ ใช้ Email
        OrderService emailOrder = new OrderService(new EmailService());
        emailOrder.placeOrder("[email protected]", "MacBook Pro");
        
        // ✓ เปลี่ยนเป็น SMS ได้ง่ายๆ
        OrderService smsOrder = new OrderService(new SMSService());
        smsOrder.placeOrder("0812345678", "iPhone 15");
    }
}

Output:

text✓ สั่งซื้อ: MacBook Pro
[Email] ส่งไปยัง [email protected]: สั่งซื้อสำเร็จ
✓ สั่งซื้อ: iPhone 15
[SMS] ส่ง SMS ไปยัง 0812345678: สั่งซื้อสำเร็จ

ข้อดี:

  • OrderService ไม่ต้องรู้ว่า implementation เป็น Email หรือ SMS
  • เปลี่ยน notification service ไม่ต้องแก้ OrderService
  • เพิ่ม notification type ใหม่ (Line, Telegram) ง่ายๆ

ตัวอย่างที่ 2: ระบบ Payment Processing

java// สัญญา (Abstraction)
public interface PaymentProcessor {
    boolean processPayment(double amount);
}

// Implementation 1: Credit Card
public class CreditCardProcessor implements PaymentProcessor {
    @Override
    public boolean processPayment(double amount) {
        System.out.println("💳 ประมวลผลบัตรเครดิต: " + amount + " บาท");
        return true;
    }
}

// Implementation 2: Bank Transfer
public class BankTransferProcessor implements PaymentProcessor {
    @Override
    public boolean processPayment(double amount) {
        System.out.println("🏦 ประมวลผลโอนเงินธนาคาร: " + amount + " บาท");
        return true;
    }
}

// Implementation 3: Cryptocurrency
public class CryptoProcessor implements PaymentProcessor {
    @Override
    public boolean processPayment(double amount) {
        System.out.println("🪙 ประมวลผลสกุลเงินดิจิทัล: " + amount + " บาท");
        return true;
    }
}

// High-level: E-Commerce ขึ้นอยู่กับ "สัญญา"
public class ECommerceStore {
    private PaymentProcessor paymentProcessor;
    
    public ECommerceStore(PaymentProcessor processor) {
        this.paymentProcessor = processor;
    }
    
    public void checkout(double totalAmount) {
        System.out.println("🛒 ชำระเงินสำหรับ: " + totalAmount + " บาท");
        
        if (paymentProcessor.processPayment(totalAmount)) {
            System.out.println("✅ ชำระเงินสำเร็จ!\n");
        } else {
            System.out.println("❌ ชำระเงินล้มเหลว\n");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        double purchaseAmount = 5000;
        
        // ✓ ใช้ Credit Card
        System.out.println("--- วิธีที่ 1: Credit Card ---");
        ECommerceStore store1 = new ECommerceStore(new CreditCardProcessor());
        store1.checkout(purchaseAmount);
        
        // ✓ เปลี่ยนเป็น Bank Transfer
        System.out.println("--- วิธีที่ 2: Bank Transfer ---");
        ECommerceStore store2 = new ECommerceStore(new BankTransferProcessor());
        store2.checkout(purchaseAmount);
        
        // ✓ เปลี่ยนเป็น Cryptocurrency
        System.out.println("--- วิธีที่ 3: Cryptocurrency ---");
        ECommerceStore store3 = new ECommerceStore(new CryptoProcessor());
        store3.checkout(purchaseAmount);
    }
}

Output:

text--- วิธีที่ 1: Credit Card ---
🛒 ชำระเงินสำหรับ: 5000.0 บาท
💳 ประมวลผลบัตรเครดิต: 5000.0 บาท
✅ ชำระเงินสำเร็จ!

--- วิธีที่ 2: Bank Transfer ---
🛒 ชำระเงินสำหรับ: 5000.0 บาท
🏦 ประมวลผลโอนเงินธนาคาร: 5000.0 บาท
✅ ชำระเงินสำเร็จ!

--- วิธีที่ 3: Cryptocurrency ---
🛒 ชำระเงินสำหรับ: 5000.0 บาท
🪙 ประมวลผลสกุลเงินดิจิทัล: 5000.0 บาท
✅ ชำระเงินสำเร็จ!

คำอธิบาย:

  • ECommerceStore ไม่ต้องรู้ว่า payment processor ตัวไหนทำงาน
  • ทำให้ระบบ flexible และ easy to maintain
  • เพิ่มวิธี payment ใหม่ไม่ต้องแก้ ECommerceStore

ตัวอย่างที่ 3: Database Connection

java// สัญญา (Abstraction)
public interface Database {
    void connect();
    void saveData(String data);
}

// Implementation 1: MySQL
public class MySQLDatabase implements Database {
    @Override
    public void connect() {
        System.out.println("🔗 เชื่อมต่อ MySQL...");
    }
    
    @Override
    public void saveData(String data) {
        System.out.println("💾 บันทึก MySQL: " + data);
    }
}

// Implementation 2: MongoDB
public class MongoDBDatabase implements Database {
    @Override
    public void connect() {
        System.out.println("🔗 เชื่อมต่อ MongoDB...");
    }
    
    @Override
    public void saveData(String data) {
        System.out.println("💾 บันทึก MongoDB: " + data);
    }
}

// High-level: Business logic
public class UserRepository {
    private Database database;
    
    public UserRepository(Database db) {
        this.database = db;
    }
    
    public void saveUser(String userData) {
        database.connect();
        database.saveData(userData);
    }
}

public class Main {
    public static void main(String[] args) {
        // ✓ ใช้ MySQL
        UserRepository mysqlRepo = new UserRepository(new MySQLDatabase());
        mysqlRepo.saveUser("{'name': 'Alice', 'age': 25}");
        
        System.out.println();
        
        // ✓ เปลี่ยนเป็น MongoDB ได้ง่าย
        UserRepository mongoRepo = new UserRepository(new MongoDBDatabase());
        mongoRepo.saveUser("{'name': 'Bob', 'age': 30}");
    }
}

Output:

text🔗 เชื่อมต่อ MySQL...
💾 บันทึก MySQL: {'name': 'Alice', 'age': 25}

🔗 เชื่อมต่อ MongoDB...
💾 บันทึก MongoDB: {'name': 'Bob', 'age': 30}

ข้อดี:

  • เปลี่ยน database ไม่ต้องแก้ UserRepository
  • ง่ายสำหรับ testing (สามารถสร้าง mock database ได้)
  • ขยายเพิ่มหลายฐานข้อมูลได้พร้อมๆกัน

Dependency Injection: วิธีการ “ให้” Dependency

3 วิธี inject dependency

1. Constructor Injection (แนะนำ)

javapublic class Service {
    private Dependency dep;
    
    // ✓ วิธีนี้ชัดเจนที่สุด
    public Service(Dependency dep) {
        this.dep = dep;
    }
}

2. Setter Injection

javapublic class Service {
    private Dependency dep;
    
    public void setDependency(Dependency dep) {
        this.dep = dep;
    }
}

3. Interface Injection (ไม่ค่อยใช้)

javapublic interface DependencyInjector {
    void inject(Service service);
}

หลักการสำคัญของ Dependency Inversion

text1. High-level modules ไม่ควรขึ้นอยู่กับ low-level modules
   → ทั้งสองควรขึ้นอยู่กับ abstraction (interface)

2. Abstraction ไม่ควรขึ้นอยู่กับ details
   → Details ควรขึ้นอยู่กับ abstraction

3. Depend on abstraction, not on concrete implementation
   → เขียน code กับ interface ไม่ใช่ class ชัดเจน

Benefits ของ Dependency Inversion

ข้อดีคำอธิบาย
Flexibilityเปลี่ยน implementation ง่าย
Testabilityง่ายต่อการเขียน unit test (ใช้ mock objects)
MaintainabilityCode จัดเรียบร้อย ง่ายต่อการดูแลรักษา
ReusabilityClass สามารถ reuse ในบริบท (context) ต่างๆได้
Loose CouplingComponents ไม่ผูกกันแน่นแบบตายตัว

ตัวอย่างสุดท้าย: Authentication System

java// สัญญา (Abstraction)
public interface AuthService {
    boolean authenticate(String username, String password);
}

// Implementation 1: Database Authentication
public class DatabaseAuth implements AuthService {
    @Override
    public boolean authenticate(String username, String password) {
        System.out.println("🔐 ตรวจสอบผ่านฐานข้อมูล: " + username);
        return username.equals("admin") && password.equals("password123");
    }
}

// Implementation 2: OAuth (Google, Facebook)
public class OAuthService implements AuthService {
    @Override
    public boolean authenticate(String username, String password) {
        System.out.println("🔐 ตรวจสอบผ่าน OAuth: " + username);
        return true;  // Assume OAuth always succeeds
    }
}

// Implementation 3: LDAP
public class LDAPAuth implements AuthService {
    @Override
    public boolean authenticate(String username, String password) {
        System.out.println("🔐 ตรวจสอบผ่าน LDAP: " + username);
        return username.length() > 0;
    }
}

// High-level: Application ขึ้นอยู่กับ "สัญญา"
public class Application {
    private AuthService authService;
    
    public Application(AuthService service) {
        this.authService = service;
    }
    
    public void login(String user, String pass) {
        if (authService.authenticate(user, pass)) {
            System.out.println("✅ Login สำเร็จ!");
        } else {
            System.out.println("❌ Login ล้มเหลว");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println("--- วิธี 1: Database Auth ---");
        Application app1 = new Application(new DatabaseAuth());
        app1.login("admin", "password123");
        
        System.out.println("\n--- วิธี 2: OAuth ---");
        Application app2 = new Application(new OAuthService());
        app2.login("john", "");
        
        System.out.println("\n--- วิธี 3: LDAP ---");
        Application app3 = new Application(new LDAPAuth());
        app3.login("employee", "ldappass");
    }
}

Output:

text--- วิธี 1: Database Auth ---
🔐 ตรวจสอบผ่านฐานข้อมูล: admin
✅ Login สำเร็จ!

--- วิธี 2: OAuth ---
🔐 ตรวจสอบผ่าน OAuth: john
✅ Login สำเร็จ!

--- วิธี 3: LDAP ---
🔐 ตรวจสอบผ่าน LDAP: employee
✅ Login สำเร็จ!

สรุป

Dependency Inversion เป็นหลักการสำคัญที่บอกให้เรา เขียน code ให้ยืดหยุ่น โดยการ ไม่ผูกติดกับ implementation เฉพาะเจาะจง แต่ให้ code ของเรา ขึ้นอยู่กับ abstraction (interface) แทน

ด้วยการใช้ Dependency Inversion จึงทำให้:

  • โปรแกรมยืดหยุ่น – เปลี่ยน implementation ได้ง่าย
  • ง่ายต่อการ test – สามารถใช้ mock objects
  • ง่ายต่อการดูแล – ลดการขึ้นต่อกันระหว่าง classes
  • สามารถ reuse ได้ – Classes ทำงานได้ในหลายบริบท

หลักการนี้เป็นส่วนหนึ่งของ SOLID principles ที่ออกแบบมาเพื่อให้โค้ดมีคุณภาพสูง ไม่ว่าจะพูดถึงโปรแกรมเล็ก หรือ enterprise applications