บทนำ: ปัญหาซ้ำๆ ต้องแก้วิธีเดิมๆ
ในระหว่างการเขียนโปรแกรม เราต่างหน้าสำหรับ ปัญหาบางอย่างที่เกิดขึ้นบ่อย เช่น:
- “จะสร้าง object ประเภทต่างๆ อย่างไร?”
- “จะให้มี object เดียวทั่วทั้งระบบได้อย่างไร?”
- “จะเปลี่ยนพฤติกรรมของ program ได้ง่ายๆ ได้อย่างไร?”
Design Patterns คือ สูตรแบบทั่วไป (templates) ที่มีคนค้นพบแล้วว่าแก้ปัญหาเหล่านี้ได้ดี ในส่วนนี้เราจะเรียนรู้ 3 pattern ที่พื้นฐานและใช้บ่อยที่สุด
1. Factory Pattern: สร้าง Object หลายชนิด
ปัญหา: จะสร้าง Object แบบเลือกได้ยังไง?
ลองนึกว่า คุณมี interface Transport และหลาย implementation (Car, Bike, Truck) คุณต้องการให้ code ที่ใช้ Transport ไม่ต้องรู้จักว่า “วันนี้จะใช้ Car หรือ Truck”
textปัญหา:
└─ ถ้า client หลายๆ ตัวต้องการ interface เดิมแต่ implementation ต่างกัน
└─ ทำยังไง?
✓ วิธีแก้: Factory Pattern
Factory Pattern = สร้าง “โรงงาน (factory)” ที่รับคำขอว่า “ผมต้อง object ประเภท X” แล้ว return object นั้นให้
ตัวอย่างที่ 1: Transport Factory
java// ==== STEP 1: Interface/Abstraction ====
public interface Transport {
void deliver();
}
// ==== STEP 2: Implementations ====
public class Car implements Transport {
@Override
public void deliver() {
System.out.println("🚗 ส่งสินค้าโดยรถยนต์");
}
}
public class Bike implements Transport {
@Override
public void deliver() {
System.out.println("🏍️ ส่งสินค้าโดยมอเตอร์ไซค์");
}
}
public class Truck implements Transport {
@Override
public void deliver() {
System.out.println("🚚 ส่งสินค้าโดยรถบรรทุก");
}
}
// ==== STEP 3: Factory ====
public class TransportFactory {
public static Transport createTransport(String type) {
switch(type.toLowerCase()) {
case "car":
return new Car();
case "bike":
return new Bike();
case "truck":
return new Truck();
default:
throw new IllegalArgumentException("ประเภท: " + type + " ไม่รู้จัก");
}
}
}
// ==== STEP 4: Client ====
public class DeliveryService {
public void shipOrder(String transportType) {
Transport transport = TransportFactory.createTransport(transportType);
transport.deliver();
}
}
// ==== การใช้งาน ====
public class Main {
public static void main(String[] args) {
DeliveryService service = new DeliveryService();
service.shipOrder("car");
service.shipOrder("bike");
service.shipOrder("truck");
}
}
Output:
text🚗 ส่งสินค้าโดยรถยนต์
🏍️ ส่งสินค้าโดยมอเตอร์ไซค์
🚚 ส่งสินค้าโดยรถบรรทุก
คำอธิบาย:
TransportFactory.createTransport()เป็น “โรงงาน” ที่รับคำขอ string แล้ว return object ที่เหมาะสมDeliveryServiceไม่ต้องรู้จักว่า Car, Bike, Truck เป็นอะไร มันเพียงขอจาก factory- เพิ่ม transport type ใหม่ (เช่น Airplane) แค่เพิ่ม case ใหม่ใน factory
ตัวอย่างที่ 2: Database Connection Factory
java// ==== Interface ====
public interface DatabaseConnection {
void connect();
void executeQuery(String sql);
}
// ==== Implementations ====
public class MySQLConnection implements DatabaseConnection {
@Override
public void connect() {
System.out.println("🔗 เชื่อมต่อ MySQL");
}
@Override
public void executeQuery(String sql) {
System.out.println("📊 รัน MySQL query: " + sql);
}
}
public class PostgresConnection implements DatabaseConnection {
@Override
public void connect() {
System.out.println("🔗 เชื่อมต่อ PostgreSQL");
}
@Override
public void executeQuery(String sql) {
System.out.println("📊 รัน PostgreSQL query: " + sql);
}
}
public class MongoDBConnection implements DatabaseConnection {
@Override
public void connect() {
System.out.println("🔗 เชื่อมต่อ MongoDB");
}
@Override
public void executeQuery(String sql) {
System.out.println("📊 รัน MongoDB query: " + sql);
}
}
// ==== Factory ====
public class DatabaseFactory {
public static DatabaseConnection create(String databaseType) {
switch(databaseType.toLowerCase()) {
case "mysql":
return new MySQLConnection();
case "postgres":
return new PostgresConnection();
case "mongodb":
return new MongoDBConnection();
default:
throw new IllegalArgumentException("Database ไม่รู้จัก: " + databaseType);
}
}
}
// ==== การใช้งาน ====
public class UserRepository {
private DatabaseConnection db;
public UserRepository(String databaseType) {
this.db = DatabaseFactory.create(databaseType);
}
public void saveUser(String userName) {
db.connect();
db.executeQuery("INSERT INTO users VALUES ('" + userName + "')");
}
}
public class Main {
public static void main(String[] args) {
UserRepository repo1 = new UserRepository("mysql");
repo1.saveUser("john");
System.out.println();
UserRepository repo2 = new UserRepository("mongodb");
repo2.saveUser("jane");
}
}
Output:
text🔗 เชื่อมต่อ MySQL
📊 รัน MySQL query: INSERT INTO users VALUES ('john')
🔗 เชื่อมต่อ MongoDB
📊 รัน MongoDB query: INSERT INTO users VALUES ('jane')
2. Singleton Pattern: Object เดียวทั่วระบบ
ปัญหา: ต้องการ object เดียวแค่ตัวเดียว
บางครั้ง เราต้องการให้มี object เพียงตัวเดียว ทั่วทั้งระบบ เช่น:
- Database connection (ต้องเดียวตัว)
- Logger (ต้องเดียวตัว)
- Configuration manager (ต้องเดียวตัว)
✓ วิธีแก้: Singleton Pattern
Singleton Pattern = สร้าง class ที่ สามารถสร้าง object ได้เพียงตัวเดียว ไม่ว่าจะ new กี่ครั้ง
ตัวอย่างที่ 3: Logger Singleton
java// ==== Singleton Logger ====
public class Logger {
// Static instance
private static Logger instance;
// Private constructor เพื่อไม่ให้ new ได้ตรงๆ
private Logger() {
}
// Static method เพื่อรับ instance เดียวตัว
public static Logger getInstance() {
if (instance == null) {
instance = new Logger();
}
return instance;
}
public void log(String message) {
System.out.println("[LOG] " + message);
}
}
// ==== การใช้งาน ====
public class ServiceA {
public void doSomething() {
Logger logger = Logger.getInstance();
logger.log("ServiceA ทำงาน");
}
}
public class ServiceB {
public void doSomething() {
Logger logger = Logger.getInstance();
logger.log("ServiceB ทำงาน");
}
}
public class Main {
public static void main(String[] args) {
ServiceA serviceA = new ServiceA();
ServiceB serviceB = new ServiceB();
serviceA.doSomething();
serviceB.doSomething();
// ✓ ตรวจสอบว่า instance เดียวตัว
Logger log1 = Logger.getInstance();
Logger log2 = Logger.getInstance();
System.out.println("log1 == log2? " + (log1 == log2)); // true
}
}
Output:
text[LOG] ServiceA ทำงาน
[LOG] ServiceB ทำงาน
log1 == log2? true
คำอธิบาย:
getInstance()ตรวจสอบว่า instance มีอยู่แล้วหรือไม่- ถ้าไม่มี สร้างตัวเดียว
- ถ้ามีแล้ว return ตัวที่มีอยู่
- ไม่ว่าจะเรียกจากไหน ได้ object เดียวตัว
ตัวอย่างที่ 4: Database Connection Singleton
java// ==== Singleton Database Connection ====
public class DatabaseConnection {
private static DatabaseConnection instance;
private String connectionString;
private DatabaseConnection() {
this.connectionString = "mysql://localhost/myapp";
System.out.println("🔗 เชื่อมต่อฐานข้อมูล: " + connectionString);
}
public static DatabaseConnection getInstance() {
if (instance == null) {
instance = new DatabaseConnection();
}
return instance;
}
public void query(String sql) {
System.out.println("📊 รัน: " + sql);
}
}
// ==== การใช้งาน ====
public class UserDAO {
public void getUser(int id) {
DatabaseConnection db = DatabaseConnection.getInstance();
db.query("SELECT * FROM users WHERE id = " + id);
}
}
public class ProductDAO {
public void getProduct(int id) {
DatabaseConnection db = DatabaseConnection.getInstance();
db.query("SELECT * FROM products WHERE id = " + id);
}
}
public class Main {
public static void main(String[] args) {
UserDAO userDAO = new UserDAO();
ProductDAO productDAO = new ProductDAO();
userDAO.getUser(1); // เชื่อมต่อครั้งเดียว (สร้าง instance)
productDAO.getProduct(1); // ใช้ instance เดิม (ไม่สร้างใหม่)
}
}
Output:
text🔗 เชื่อมต่อฐานข้อมูล: mysql://localhost/myapp
📊 รัน: SELECT * FROM users WHERE id = 1
📊 รัน: SELECT * FROM products WHERE id = 1
ข้อดี:
- แม้ UserDAO และ ProductDAO ต่างกเรียก getInstance() แต่ได้ connection เดียวตัว
- ประหยัด resource (connection)
- ปลอดภัยมากขึ้น
3. Strategy Pattern: สลับพฤติกรรมได้ Runtime
ปัญหา: วิธีการต่างๆ ต้องขึ้นอยู่กับสถานการณ์
บางครั้ง algorithm หรือ strategy ต้อง “สลับได้” ตามสถานการณ์ เช่น:
- วิธีการเรียงลำดับ (ขึ้นอยู่กับข้อมูล)
- วิธีการส่ง (ขึ้นอยู่กับที่ปลายทาง)
- วิธีการคิดราคา (ขึ้นอยู่กับประเภท customer)
✓ วิธีแก้: Strategy Pattern
Strategy Pattern = สร้าง “เทพพอลี่” (family) ของ algorithms และให้ client เลือกได้
ตัวอย่างที่ 5: Sorting Strategy
java// ==== Interface: Strategy ====
public interface SortStrategy {
void sort(int[] array);
}
// ==== Implementation 1: Bubble Sort ====
public class BubbleSort implements SortStrategy {
@Override
public void sort(int[] array) {
System.out.println("🫧 ใช้ Bubble Sort");
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
System.out.println("ผลลัพธ์: " + java.util.Arrays.toString(array));
}
}
// ==== Implementation 2: Quick Sort ====
public class QuickSort implements SortStrategy {
@Override
public void sort(int[] array) {
System.out.println("⚡ ใช้ Quick Sort");
// Simplified
java.util.Arrays.sort(array);
System.out.println("ผลลัพธ์: " + java.util.Arrays.toString(array));
}
}
// ==== Implementation 3: Merge Sort ====
public class MergeSort implements SortStrategy {
@Override
public void sort(int[] array) {
System.out.println("🔀 ใช้ Merge Sort");
// Simplified
java.util.Arrays.sort(array);
System.out.println("ผลลัพธ์: " + java.util.Arrays.toString(array));
}
}
// ==== Context: ใช้ Strategy ====
public class Sorter {
private SortStrategy strategy;
public Sorter(SortStrategy strategy) {
this.strategy = strategy;
}
// สามารถสลับ strategy ได้
public void setStrategy(SortStrategy strategy) {
this.strategy = strategy;
}
public void performSort(int[] array) {
strategy.sort(array);
}
}
// ==== การใช้งาน ====
public class Main {
public static void main(String[] args) {
int[] data = {64, 34, 25, 12, 22, 11, 90};
// ใช้ Bubble Sort
Sorter sorter = new Sorter(new BubbleSort());
sorter.performSort(data);
// เปลี่ยนเป็น Quick Sort ขณะ runtime
sorter.setStrategy(new QuickSort());
sorter.performSort(data);
// เปลี่ยนเป็น Merge Sort
sorter.setStrategy(new MergeSort());
sorter.performSort(data);
}
}
Output:
text🫧 ใช้ Bubble Sort
ผลลัพธ์: [11, 12, 22, 25, 34, 64, 90]
⚡ ใช้ Quick Sort
ผลลัพธ์: [11, 12, 22, 25, 34, 64, 90]
🔀 ใช้ Merge Sort
ผลลัพธ์: [11, 12, 22, 25, 34, 64, 90]
คำอธิบาย:
SortStrategyคือ interface ที่นิยาม strategyBubbleSort,QuickSort,MergeSortคือ strategy ต่างๆSorterเป็น context ที่ใช้ strategy- สามารถเปลี่ยน strategy ขณะ runtime ด้วย
setStrategy()
ตัวอย่างที่ 6: Payment Strategy
java// ==== Interface ====
public interface PaymentStrategy {
void pay(double amount);
}
// ==== Implementation 1: Credit Card ====
public class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("💳 ชำระด้วยบัตรเครดิต: " + amount + " บาท");
}
}
// ==== Implementation 2: Bank Transfer ====
public class BankTransferPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("🏦 โอนเงินธนาคาร: " + amount + " บาท");
}
}
// ==== Implementation 3: E-Wallet ====
public class EWalletPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("💰 ชำระ E-Wallet: " + amount + " บาท");
}
}
// ==== Context ====
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
private double totalPrice;
public ShoppingCart(double price) {
this.totalPrice = price;
}
public void setPaymentMethod(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout() {
if (paymentStrategy == null) {
System.out.println("❌ ยังไม่เลือกวิธีชำระเงิน");
return;
}
paymentStrategy.pay(totalPrice);
}
}
// ==== การใช้งาน ====
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart(1000);
// ลูกค้าเลือกชำระด้วยบัตรเครดิต
System.out.println("--- วิธีที่ 1 ---");
cart.setPaymentMethod(new CreditCardPayment());
cart.checkout();
// เปลี่ยนใจ เลือกโอนเงินธนาคาร
System.out.println("\n--- วิธีที่ 2 ---");
cart.setPaymentMethod(new BankTransferPayment());
cart.checkout();
// เปลี่ยนใจอีก เลือก E-Wallet
System.out.println("\n--- วิธีที่ 3 ---");
cart.setPaymentMethod(new EWalletPayment());
cart.checkout();
}
}
Output:
text--- วิธีที่ 1 ---
💳 ชำระด้วยบัตรเครดิต: 1000.0 บาท
--- วิธีที่ 2 ---
🏦 โอนเงินธนาคาร: 1000.0 บาท
--- วิธีที่ 3 ---
💰 ชำระ E-Wallet: 1000.0 บาท
ข้อดี:
- ShoppingCart ไม่ต้องรู้ว่า payment method ไหนทำงาน
- ลูกค้าสามารถเปลี่ยนวิธีชำระเงินได้ทุกเวลา
- เพิ่ม payment method ใหม่ง่ายๆ
ตารางเปรียบเทียบ 3 Patterns
| Pattern | ปัญหา | วิธีแก้ | ตัวอย่าง |
|---|---|---|---|
| Factory | ต้องสร้าง object ประเภทต่างๆ | สร้าง factory ที่ return object | Transport ต่างชนิด |
| Singleton | ต้องการ object เดียวตัว | ทำให้ constructor private ให้เพียง getInstance() | Database connection |
| Strategy | Algorithm ต้องเปลี่ยนได้ | สร้าง family ของ algorithms ให้เลือก | Payment method ต่างชนิด |
สรุป
Design Patterns เป็นคำแนะนำที่เหมาะสมแล้วสำหรับ ปัญหาทั่วไปในการเขียน OOP
- Factory Pattern – เมื่อต้องการสร้าง object หลายชนิด โดยไม่ให้ client รู้ละเอียด
- Singleton Pattern – เมื่อต้องการให้มี object เพียงตัวเดียวทั่วทั้งระบบ
- Strategy Pattern – เมื่อต้องให้ client เลือก algorithm หรือวิธีการที่แตกต่างกันได้
การใช้ design patterns ให้เหมาะสมจะทำให้ code ของเรา:
- ยืดหยุ่น – เปลี่ยนแปลงได้ง่าย
- ปลอดภัย – ลดข้อผิดพลาด
- ดูแลรักษาได้ง่าย – คนอื่นเข้าใจได้
- scalable – ขยายระบบได้ง่าย
จำไว้ว่า patterns ไม่ใช่กฎข้อบังคับ แต่เป็น “ข้อแนะนำปกติ” ที่คนก่อนหน้าค้นพบแล้วว่าสัก effective ในการแก้ปัญหา ใช้ให้สมควร ไม่ใช้ฝ่ายบังคับ
