บทนำ: ปัญหา “ขึ้นอยู่กับ 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) |
| Maintainability | Code จัดเรียบร้อย ง่ายต่อการดูแลรักษา |
| Reusability | Class สามารถ reuse ในบริบท (context) ต่างๆได้ |
| Loose Coupling | Components ไม่ผูกกันแน่นแบบตายตัว |
ตัวอย่างสุดท้าย: 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
