Design Pattern 101 — Strategy Pattern

Phayao Boonon
5 min readAug 31, 2019

--

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 นี้เป็นบทความสุดท้าย สวัสดี 👨🏻‍💻

--

--

Phayao Boonon
Phayao Boonon

Written by Phayao Boonon

Software Engineer 👨🏻‍💻 Stay Hungry Stay Foolish

No responses yet