การออกแบบ “สัญญา (Contract)” ให้ระบบทำงานแทนกันได้

บทนำ: ทำไมต้องมี “สัญญา” ในโปรแกรม?

ลองนึกภาพว่า คุณกำลังสร้าง app ที่ต้องส่งการแจ้งเตือนให้ผู้ใช้ ในตอนแรก คุณใช้ Email เพื่อส่งการแจ้งเตือน ทุกอย่างทำงานปกติ

แต่วันหนึ่ง คุณต้องการเพิ่มการส่ง SMS, Push Notification, หรือแม้แต่ Discord message แล้ว? ถ้า code ของคุณผูกติดกับ EmailService ตรงๆ คุณจะต้องไปแก้โค้ดทั่วระบบ ซึ่งเสี่ยงต่อการเกิด bug

วิธีแก้: สร้าง “สัญญา” (Contract) ว่า “ใครก็ได้ที่อยากส่งการแจ้งเตือน ต้องมี method นี้” แล้วให้ class ต่างๆ follow สัญญานี้ เมื่อนั้น เราสามารถเปลี่ยนวิธีส่งได้อย่างง่าย


“สัญญา” คืออะไร?

“สัญญา” (Contract) ในโปรแกรม คือ set ของ methods ที่ต้องมี ซึ่งถูก define ในรูปของ interface หรือ abstract class

ใครก็ตาม (class ไหน ก็ตาม) ที่ต้องการทำงานกับส่วนนี้ของระบบ ต้อง เข้าสัญญานี้ (implement interface นี้)

java// "สัญญา": ใคร implement ต้องมี method send()
public interface Notifier {
    void send(String recipient, String message);
}

ตัวอย่างที่ 1: ระบบส่งการแจ้งเตือน

มาเขียน code ที่แสดงวิธีออกแบบสัญญาให้ระบบส่งการแจ้งเตือน

java// ==== STEP 1: สัญญา (Contract) ====
public interface NotificationChannel {
    void send(String recipient, String message);
}

// ==== STEP 2: Implementation 1 - Email ====
public class EmailNotifier implements NotificationChannel {
    @Override
    public void send(String recipient, String message) {
        System.out.println("📧 ส่ง Email ไปยัง: " + recipient);
        System.out.println("   เนื้อความ: " + message);
    }
}

// ==== STEP 3: Implementation 2 - SMS ====
public class SMSNotifier implements NotificationChannel {
    @Override
    public void send(String recipient, String message) {
        System.out.println("📱 ส่ง SMS ไปยัง: " + recipient);
        System.out.println("   เนื้อความ: " + message);
    }
}

// ==== STEP 4: Implementation 3 - Push Notification ====
public class PushNotifier implements NotificationChannel {
    @Override
    public void send(String recipient, String message) {
        System.out.println("🔔 ส่ง Push Notification ไปยัง: " + recipient);
        System.out.println("   เนื้อความ: " + message);
    }
}

// ==== STEP 5: ระบบที่ใช้สัญญา ====
public class NotificationService {
    private NotificationChannel channel;
    
    // Constructor: รับ channel ใดๆ ที่ follow สัญญา
    public NotificationService(NotificationChannel channel) {
        this.channel = channel;
    }
    
    public void notifyUser(String userId, String message) {
        System.out.println(">>> กำลังส่งการแจ้งเตือน...");
        channel.send(userId, message);
        System.out.println();
    }
}

// ==== การใช้งาน ====
public class Main {
    public static void main(String[] args) {
        String userID = "john_doe";
        String message = "คำสั่งซื้อของคุณสำเร็จแล้ว";
        
        // ใช้ Email
        System.out.println("=== ส่งผ่าน Email ===");
        NotificationService emailService = new NotificationService(new EmailNotifier());
        emailService.notifyUser(userID, message);
        
        // ใช้ SMS
        System.out.println("=== ส่งผ่าน SMS ===");
        NotificationService smsService = new NotificationService(new SMSNotifier());
        smsService.notifyUser(userID, message);
        
        // ใช้ Push
        System.out.println("=== ส่งผ่าน Push Notification ===");
        NotificationService pushService = new NotificationService(new PushNotifier());
        pushService.notifyUser(userID, message);
    }
}

Output:

text=== ส่งผ่าน Email ===
>>> กำลังส่งการแจ้งเตือน...
📧 ส่ง Email ไปยัง: john_doe
   เนื้อความ: คำสั่งซื้อของคุณสำเร็จแล้ว

=== ส่งผ่าน SMS ===
>>> กำลังส่งการแจ้งเตือน...
📱 ส่ง SMS ไปยัง: john_doe
   เนื้อความ: คำสั่งซื้อของคุณสำเร็จแล้ว

=== ส่งผ่าน Push Notification ===
>>> กำลังส่งการแจ้งเตือน...
🔔 ส่ง Push Notification ไปยัง: john_doe
   เนื้อความ: คำสั่งซื้อของคุณสำเร็จแล้ว

คำอธิบาย:

  • NotificationChannel คือสัญญา – ทุก class ที่ implement ต้องมี method send()
  • EmailNotifierSMSNotifierPushNotifier ต่างกเขาสัญญา
  • NotificationService ไม่ต้องรู้ว่ากำลังใช้ channel ไหน มันเพียง follow สัญญา
  • เพิ่ม channel ใหม่ (เช่น Discord) ได้ง่ายๆ โดยไม่ต้องแก้ NotificationService

ตัวอย่างที่ 2: ระบบการจ่ายเงิน

java// ==== สัญญา: วิธี Payment ต้องมี method process() ====
public interface PaymentMethod {
    boolean process(double amount);
    String getMethodName();
}

// ==== Implementation 1: บัตรเครดิต ====
public class CreditCardPayment implements PaymentMethod {
    private String cardNumber;
    
    public CreditCardPayment(String cardNumber) {
        this.cardNumber = cardNumber;
    }
    
    @Override
    public boolean process(double amount) {
        System.out.println("💳 ประมวลผล: บัตรเครดิต " + cardNumber);
        System.out.println("   จำนวนเงิน: " + amount + " บาท");
        return true;
    }
    
    @Override
    public String getMethodName() {
        return "Credit Card";
    }
}

// ==== Implementation 2: บัญชีธนาคาร ====
public class BankTransferPayment implements PaymentMethod {
    private String accountNumber;
    
    public BankTransferPayment(String accountNumber) {
        this.accountNumber = accountNumber;
    }
    
    @Override
    public boolean process(double amount) {
        System.out.println("🏦 ประมวลผล: โอนเงินธนาคาร " + accountNumber);
        System.out.println("   จำนวนเงิน: " + amount + " บาท");
        return true;
    }
    
    @Override
    public String getMethodName() {
        return "Bank Transfer";
    }
}

// ==== Implementation 3: Digital Wallet ====
public class DigitalWalletPayment implements PaymentMethod {
    private String walletID;
    
    public DigitalWalletPayment(String walletID) {
        this.walletID = walletID;
    }
    
    @Override
    public boolean process(double amount) {
        System.out.println("💰 ประมวลผล: Digital Wallet " + walletID);
        System.out.println("   จำนวนเงิน: " + amount + " บาท");
        return true;
    }
    
    @Override
    public String getMethodName() {
        return "Digital Wallet";
    }
}

// ==== ระบบ Checkout ====
public class CheckoutSystem {
    private PaymentMethod paymentMethod;
    
    public CheckoutSystem(PaymentMethod method) {
        this.paymentMethod = method;
    }
    
    public void checkout(double totalPrice) {
        System.out.println("=== Checkout ===");
        System.out.println("วิธีการจ่าย: " + paymentMethod.getMethodName());
        System.out.println("ราคารวม: " + totalPrice + " บาท");
        
        if (paymentMethod.process(totalPrice)) {
            System.out.println("✅ ชำระเงินสำเร็จ!\n");
        } else {
            System.out.println("❌ ชำระเงินล้มเหลว\n");
        }
    }
}

// ==== การใช้งาน ====
public class Main {
    public static void main(String[] args) {
        double purchaseTotal = 2500;
        
        // วิธี 1: ใช้บัตรเครดิต
        System.out.println("--- วิธีที่ 1 ---");
        CheckoutSystem checkout1 = new CheckoutSystem(
            new CreditCardPayment("1234-5678-9012-3456")
        );
        checkout1.checkout(purchaseTotal);
        
        // วิธี 2: ใช้โอนเงินธนาคาร
        System.out.println("--- วิธีที่ 2 ---");
        CheckoutSystem checkout2 = new CheckoutSystem(
            new BankTransferPayment("123-456-789")
        );
        checkout2.checkout(purchaseTotal);
        
        // วิธี 3: ใช้ Digital Wallet
        System.out.println("--- วิธีที่ 3 ---");
        CheckoutSystem checkout3 = new CheckoutSystem(
            new DigitalWalletPayment("WALLET_12345")
        );
        checkout3.checkout(purchaseTotal);
    }
}

Output:

text--- วิธีที่ 1 ---
=== Checkout ===
วิธีการจ่าย: Credit Card
ราคารวม: 2500.0 บาท
💳 ประมวลผล: บัตรเครดิต 1234-5678-9012-3456
   จำนวนเงิน: 2500.0 บาท
✅ ชำระเงินสำเร็จ!

--- วิธีที่ 2 ---
=== Checkout ===
วิธีการจ่าย: Bank Transfer
ราคารวม: 2500.0 บาท
🏦 ประมวลผล: โอนเงินธนาคาร 123-456-789
   จำนวนเงิน: 2500.0 บาท
✅ ชำระเงินสำเร็จ!

--- วิธีที่ 3 ---
=== Checkout ===
วิธีการจ่าย: Digital Wallet
ราคารวม: 2500.0 บาท
💰 ประมวลผล: Digital Wallet WALLET_12345
   จำนวนเงิน: 2500.0 บาท
✅ ชำระเงินสำเร็จ!

คำอธิบาย:

  • PaymentMethod เป็นสัญญา – ทุก payment method ต้อง implement
  • CheckoutSystem ไม่ต้องรู้ว่าใช้บัตร, โอนเงิน, หรือ wallet
  • เพิ่ม payment method ใหม่ (เช่น Cryptocurrency) ไม่ต้องแก้ CheckoutSystem

ตัวอย่างที่ 3: ระบบเก็บข้อมูล (Database Abstraction)

java// ==== สัญญา: Repository ต้องสามารถ save/load ====
public interface UserRepository {
    void saveUser(User user);
    User loadUser(String id);
    boolean userExists(String id);
}

// ==== User class ====
public class User {
    private String id;
    private String name;
    private String email;
    
    public User(String id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
    
    @Override
    public String toString() {
        return "User{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                '}';
    }
}

// ==== Implementation 1: MySQL Database ====
public class MySQLUserRepository implements UserRepository {
    @Override
    public void saveUser(User user) {
        System.out.println("💾 บันทึก MySQL: " + user);
    }
    
    @Override
    public User loadUser(String id) {
        System.out.println("📖 โหลดจาก MySQL: " + id);
        return new User(id, "MySQL User", "[email protected]");
    }
    
    @Override
    public boolean userExists(String id) {
        System.out.println("🔍 ตรวจสอบ MySQL: " + id);
        return true;
    }
}

// ==== Implementation 2: MongoDB ====
public class MongoDBUserRepository implements UserRepository {
    @Override
    public void saveUser(User user) {
        System.out.println("💾 บันทึก MongoDB: " + user);
    }
    
    @Override
    public User loadUser(String id) {
        System.out.println("📖 โหลดจาก MongoDB: " + id);
        return new User(id, "MongoDB User", "[email protected]");
    }
    
    @Override
    public boolean userExists(String id) {
        System.out.println("🔍 ตรวจสอบ MongoDB: " + id);
        return false;
    }
}

// ==== Implementation 3: File Storage ====
public class FileUserRepository implements UserRepository {
    @Override
    public void saveUser(User user) {
        System.out.println("💾 บันทึกลงไฟล์: " + user);
    }
    
    @Override
    public User loadUser(String id) {
        System.out.println("📖 โหลดจากไฟล์: " + id);
        return new User(id, "File User", "[email protected]");
    }
    
    @Override
    public boolean userExists(String id) {
        System.out.println("🔍 ตรวจสอบไฟล์: " + id);
        return true;
    }
}

// ==== User Service (Business Logic) ====
public class UserService {
    private UserRepository repository;
    
    public UserService(UserRepository repo) {
        this.repository = repo;
    }
    
    public void registerUser(User user) {
        if (!repository.userExists(user.id)) {
            repository.saveUser(user);
            System.out.println("✅ ลงทะเบียนสำเร็จ\n");
        } else {
            System.out.println("❌ ผู้ใช้นี้มีอยู่แล้ว\n");
        }
    }
    
    public void getUser(String id) {
        User user = repository.loadUser(id);
        System.out.println("ข้อมูล: " + user + "\n");
    }
}

// ==== การใช้งาน ====
public class Main {
    public static void main(String[] args) {
        User newUser = new User("U001", "สมชาย", "[email protected]");
        
        // ใช้ MySQL
        System.out.println("=== ใช้ MySQL ===");
        UserService mysqlService = new UserService(new MySQLUserRepository());
        mysqlService.registerUser(newUser);
        mysqlService.getUser("U001");
        
        // ใช้ MongoDB
        System.out.println("=== ใช้ MongoDB ===");
        UserService mongoService = new UserService(new MongoDBUserRepository());
        mongoService.registerUser(newUser);
        mongoService.getUser("U001");
        
        // ใช้ File Storage
        System.out.println("=== ใช้ File Storage ===");
        UserService fileService = new UserService(new FileUserRepository());
        fileService.registerUser(newUser);
        fileService.getUser("U001");
    }
}

Output:

text=== ใช้ MySQL ===
🔍 ตรวจสอบ MySQL: U001
💾 บันทึก MySQL: User{id='U001', name='สมชาย', email='[email protected]'}
✅ ลงทะเบียนสำเร็จ

📖 โหลดจาก MySQL: U001
ข้อมูล: User{id='U001', name='MySQL User', email='[email protected]'}

=== ใช้ MongoDB ===
🔍 ตรวจสอบ MongoDB: U001
💾 บันทึก MongoDB: User{id='U001', name='สมชาย', email='[email protected]'}
✅ ลงทะเบียนสำเร็จ

📖 โหลดจาก MongoDB: U001
ข้อมูล: User{id='U001', name='MongoDB User', email='[email protected]'}

=== ใช้ File Storage ===
🔍 ตรวจสอบไฟล์: U001
💾 บันทึกลงไฟล์: User{id='U001', name='สมชาย', email='[email protected]'}
✅ ลงทะเบียนสำเร็จ

📖 โหลดจากไฟล์: U001
ข้อมูล: User{id='U001', name='File User', email='[email protected]'}

คำอธิบาย:

  • UserRepository เป็นสัญญา – ทุก repository ต้อง implement
  • UserService ไม่ต้องรู้ว่าใช้ database ไหน แค่ follow สัญญา
  • เปลี่ยน database ทำได้ง่าย: เพียง pass ต่างๆ repository เข้าไป

ข้อดีของการออกแบบสัญญา

ข้อดีคำอธิบาย
Flexibilityเปลี่ยน implementation ได้ง่าย โดยไม่ต้องแก้ code ใหญ่
Extensibilityเพิ่ม implementation ใหม่ได้อย่างง่ายดาย
Testabilityง่ายต่อการเขียน test โดยใช้ mock objects
Separation of ConcernsBusiness logic แยกจาก implementation details
ReusabilityComponent สามารถใช้ในบริบทต่างๆได้

ข้อควรระวัง

❌ อย่าทำ: Tight Coupling

java// ❌ ไม่ดี - ผูกติดกับ EmailService ตรงๆ
public class OrderService {
    private EmailService emailService = new EmailService();
    
    public void placeOrder(String email) {
        emailService.sendEmail(email, "Order confirmed");
    }
}

✓ ทำนี้: Loose Coupling

java// ✓ ดี - ใช้สัญญา ยืดหยุ่นได้
public class OrderService {
    private NotificationChannel channel;
    
    public OrderService(NotificationChannel channel) {
        this.channel = channel;
    }
    
    public void placeOrder(String recipient) {
        channel.send(recipient, "Order confirmed");
    }
}

สรุป

การออกแบบ “สัญญา (Contract)” เป็นศิลปะของการเขียน code ที่ยืดหยุ่นและยั่งยืน ด้วยการสร้าง interface ที่ชัดเจน เราบอกให้ระบบรู้ว่า “ใครก็ตามที่อยากทำงานกับฉัน ต้องมี methods เหล่านี้”

เมื่อทำแบบนี้ code ของเรากลายเป็น “plugin system” ที่ยอมรับ “เสียบต่อ” (plug-in) component ใหม่ๆได้ตราบใดที่มันเข้าสัญญา ไม่ว่าจะเป็นการเปลี่ยน notification channel จาก Email เป็น SMS, หรือเปลี่ยน database จาก MySQL เป็น MongoDB—ระบบยังทำงานได้เหมือนเดิม

นี่คือประโยชน์สูงสุดของการเขียน OOP ที่ดี: ระบบที่ยืดหยุ่น ดูแลรักษาง่าย และขยายได้โดยไม่ต้องกังวลว่าจะทำให้ส่วนอื่นพังหรือไม่