Design Pattern 101 — Strategy Pattern
Strategy Pattern เป็น pattern ตัวสุดท้ายของกลุ่ม Behavioral pattern ของ GoF ที่ผมจะมาทบทวนในซีรี่ย์นี้ เป็น pattern ที่ง่ายและมีประโยชน์มากสำหรับการประยุกต์ใช้งาน กับระบบที่ต้องการความหลากหลายของวิธีแต่ไม่ต้องแก้ไข เสมอๆ ถ้ามีวิธีการใหม่ๆ ที่ต้องใช้ ในบทความนี้ จะทบทวนและใช้ Strategy Pattern แก้ไขปัญหาอะไรได้บ้าง
Strategy Pattern
Problem
ถ้าเราต้องพัฒนาระบบซอฟต์แวร์ชำระเงินสำหรับ Shopping Cart ที่รองรับการชำระเงินสินค้าด้วย “เงินสด”
ดังนั้นเราก็สร้าง classPayment
สำหรับชำระเงินด้วยเงินสด โดยที่รับ parameter เป็น จำนวนเงินที่ชำระ และแสดงรายละเอียดการชำระเงินบนหน้าจอ
public class Payment {
public void pay(double amount) {
System.out.println(amount + " paid with cash");
}
}
เอา Payment
ไปใช้ใน ShoppingCart
ที่มีรายการของ Item และสามารถ เพิ่ม/ลบ Item แล้วสามาถคำนวนราคาของ Item ทั้งหมดในรายการ ใช้ method pay()
เรียกใช้งาน Payment
public class ShoppingCart {
private List<Item> items;
private Payment payment;
public ShoppingCart() {
items = new ArrayList<>();
payment = new Payment();
}
public void addItem(Item item) {
items.add(item);
}
public void removeItem(Item item) {
items.remove(item);
}
public double totalPrice() {
return items.stream()
.mapToDouble(Item::getPrice)
.sum();
}
public void pay() {
double amount = totalPrice();
payment.pay(amount);
}
}
และใช้ ShoppingCart
โดยเพิ่ม Item เข้าไปใน Shopping Cart และ ใช้ method pay()
เพื่อชำระเงิน
ShoppingCart cart = new ShoppingCart();
Item item1 = new Item("ABC1234", 100.50);
Item item2 = new Item("ABC2345", 50);
cart.addItem(item1);
cart.addItem(item2);
cart.pay();
อยู่มาวันหนึ่ง requirement ต้องการเพิ่มรูปแบบการชำระเงินด้วย Credit Card ด้วย ดั้งน้ันเราต้องแก้ class Payment
ให้รองรับ Credit Card
แต่ Credit Card ต้องมีขอมูลบัตร เช่น หมายเลขบัตร (number), วัน/เดือน หมดอายุ (mm/yy) และ CVV ดังนั้นจะต้องรับข้อมูลนี้จากผู้ชำระเงิน
ดังนั้นจะต้องกำหนดชนิดของ การชำระเงิน (type) credit_card
สำหรับ Credit Card และอื่นๆ เป็นการชำระด้วยเงินสด
public class Payment {
private Scanner in = new Scanner(System.in);
private String type = "cash";
public void setType(String type) {
this.type = type;
}
public void pay(double amount) {
if(type.equals("credit_card")) {
System.out.println("Enter Card number: ");
String number = in.nextLine();
System.out.println("Enter expiration date mm/yy: ");
String date = in.nextLine();
System.out.println("Enter CVV code: ");
String cvv = in.nextLine();
CreditCard card = new CreditCard(number, date, cvv);
if(card.validate()) {
card.setAmount(amount);
System.out.println("Paid with " + card);
} else {
System.out.println("Payment Credit Card failed");
}
} else {
System.out.println("paid with cash " + amount);
}
}
}
ใน ShoppingCard
จะต้องรับเป็น type
ของการชำระเงินและเรียก setType()
เพื่อกำหนดวิธีการชำระเงิน ใน method pay()
public class ShoppingCart {
// not change
public void pay(String type) {
double amount = totalPrice();
payment.setType(type);
payment.pay(amount);
}
}
แต่อยู่มาอีกหลายวัน requirement ต้องการเพิ่มวิธีการชำระเงินด้วย PayPal และ PromptPay เอาแล้วสิ งั้นเราก็ต้องเพิ่มเงื่อนไขใน Payment
ให้รองรับการชำระเงินเพิ่มเติม
จะเห็นได้ว่าวิธีการชำระเงินจะขึ้นอยู่กับ (Tight Coupling) Payment
ตลอดเวลา
Solution
ถึงเวลาของการใช้ Pattern มาแก้ไขปัญหานี้แล้ว เรามาใช้ Strategy Pattern มาใช้กับปัญหานี้ โดยสร้าง interface PayStrategy
สำหรับแต่ละวิธีของการชำระเงิน
public interface PayStrategy {
void pay(double amount);
}
และ implement สำหรับการชำระเงินด้วยเงินสด CashPay
และ การขำระเงินด้วยบัตรเครดิต CreditCardPay
public class CashPay implements PayStrategy {
@Override
public void pay(double amount) {
System.out.println("paid with cash " + amount);
}
}public class CreditCardPay implements PayStrategy {
private Scanner in = new Scanner(System.in);
private CreditCard card;
@Override
public void pay(double amount) {
getPaymentDetail();
card.setAmount(amount);
if (card.validate()) {
System.out.println("Paid with " + card);
} else {
System.out.println("Payment Credit Card failed");
}
}
public void getPaymentDetail() {
System.out.println("Enter Card number: ");
String number = in.nextLine();
System.out.println("Enter expiration date mm/yy: ");
String date = in.nextLine();
System.out.println("Enter CVV code: ");
String cvv = in.nextLine();
card = new CreditCard(number, date, cvv);
}
}
แก้ไข class Payment
ให้ไม่ขึ้นอยู่กับวิธีการชำระเงิน แต่ให้ขึ้นอยู่กับ PayStrategy
แทน
public class Payment {
private PayStrategy type = new CashPay();
public void setType(PayStrategy type) {
this.type = type;
}
public void pay(double amount) {
this.type.pay(amount);
}
}
รวมทั้งใน ShoppingCart
ด้วย
public class ShoppingCart {
// not change
public void pay(PayStrategy type) {
double amount = totalPrice();
payment.setType(type);
payment.pay(amount);
}
}
เราก็เพิ่มวิธีการชำระเงินแบบอื่นทั้ง PayPal และ PromptPay ได้แล้ว
public class PayPalPay implements PayStrategy {
private Scanner in = new Scanner(System.in);
private PayPalPayment payment;
@Override
public void pay(double amount) {
getPaymentDetail();
payment.setAmount(amount);
if(payment.verify()) {
System.out.println("Paid with " + payment);
} else {
System.out.println("Payment PayPal failed");
}
}
public void getPaymentDetail() {
String email;
String password;
System.out.println("Enter user's email: ");
email = in.nextLine();
System.out.println("Enter password: ");
password = in.nextLine();
payment = new PayPalPayment(email, password);
}
}public class PromptPay implements PayStrategy {
private Scanner in = new Scanner(System.in);
private PromptPayPayment payment;
@Override
public void pay(double amount) {
getPaymentDetail();
payment.setAmount(amount);
if(payment.validate()) {
System.out.println("Paid with " + payment);
} else {
System.out.println("Payment PromptPay failed");
}
}
public void getPaymentDetail() {
String promptPayId;
System.out.println("Enter PromptPay ID: ");
promptPayId = in.nextLine();
payment = new PromptPayPayment(promptPayId);
}
}
แต่จะเห็นได้ว่า CreditCardPay
, PayPalPay
และ PromptPay
มี logic ของ method pay()
ที่เหมือนกัน ดังนั้น จะดึงส่วนนี้ออกมาเป็น abstract class PayWithDetail
ที่ implement PayStrategy
และมี abstract method getPaymentDetail()
public abstract class PayWithDetail implements PayStrategy {
protected Scanner in = new Scanner(System.in);
protected PaymentVendor payment;
@Override
public void pay(double amount) {
getPaymentDetail();
payment.setAmount(amount);
if (payment.validate()) {
System.out.println("Paid with " + payment);
} else {
System.out.println("Payment failed");
}
}
abstract void getPaymentDetail();
}
และ เปลี่ยน class CreditCardPay
, PayPalPay
และ PromptPay
ให้ extend มาจาก PayWithDetail
public class CreditCardPay extends PayWithDetail {
@Override
public void getPaymentDetail() {
String number;
String date;
String cvv;
System.out.println("Enter Card number: ");
number = in.nextLine();
System.out.println("Enter expiration date mm/yy: ");
date = in.nextLine();
System.out.println("Enter CVV code: ");
cvv = in.nextLine();
payment = new CreditCardVendor(number, date, cvv);
}
}public class PayPalPay extends PayWithDetail {
@Override
public void getPaymentDetail() {
String email;
String password;
System.out.println("Enter user's email: ");
email = in.nextLine();
System.out.println("Enter password: ");
password = in.nextLine();
payment = new PayPalVendor(email, password);
}
}public class PromptPay extends PayWithDetail {
@Override
public void getPaymentDetail() {
String promptPayId;
System.out.println("Enter PromptPay ID: ");
promptPayId = in.nextLine();
payment = new PromptPayVendor(promptPayId);
}
}
และนำวิธีการชำระเงินไปใช้ด้วยการส่ง instance ของ PayStrategy
เข้าไปใน method pay()
ของ ShoppingCart
ที่กำหนดค่าของ type
ซึ่งเป็น PayStrategy
ใน Payment
และทำการชำระเงิน (Pay) ด้วย strategy ที่กำหนดไว้
ShoppingCart cart = new ShoppingCart();
Item item1 = new Item("ABC1234", 100.50);
Item item2 = new Item("ABC2345", 50);
cart.addItem(item1);
cart.addItem(item2);
cart.pay(new CreditCardPay());
cart.pay(new PayPalPay());
cart.pay(new PromptPay());
Strategy Pattern เป็นการกำหนดกลุ่มของ algorithm (family of algorithm) ที่ซ้อน algorithm นั้นๆ ไว้ และทำให้มันสามาถเปลี่ยนแปลงได้ Strategy ทำให้ algorithm เปลี่ยนแปลงอย่างอิสระ จากการใช้งานของ client
จะเห็นได้ว่า Strategy Pattern เราสามาถเปลี่ยนแปลง Strategy ได้ในขณะทำงาน (Runtime)
สรุป
จากการที่ได้ทบทวนและใช้งาน Strategy Pattern ในบทความนี้จะเห็นได้ว่า pattern นี้ใช้หลักการของ OOP ที่ encapsulate algorithm ของการทำงานที่หลากหายไว้ และเปลียนแปลงได้ในขณะทำงานเลย ใน Head First Design Pattern จะเป็นบทแรก แต่ใน series Design Pattern 101 นี้เป็นบทความสุดท้าย สวัสดี 👨🏻💻