Strategy (กลยุทธ์) เป็นรูปแบบการออกแบบเชิงพฤติกรรมที่กำหนดกลุ่มของอัลกอริทึมที่สามารถสับเปลี่ยนกันได้และวางแต่ละอัลกอริทึมในคลาสแยกต่างหาก (Strategy) รูปแบบนี้อนุญาตให้เลือกอัลกอริทึมได้ทันที: โค้ดของไคลเอนต์ทำงานผ่านอินเทอร์เฟซ Strategy ร่วมกัน และการนำไปใช้เฉพาะจะถูกแทนที่ในรันไทม์ ใน iOS รูปแบบนี้ถูกนำไปใช้ผ่าน Protocol + คลาสกลยุทธ์ ใน Android — ผ่าน Interface + การนำไปใช้ Strategy เป็นหนึ่งใน 23 รูปแบบ GoF ใช้กันอย่างแพร่หลายสำหรับการประมวลผลการชำระเงิน การตรวจสอบความถูกต้อง การเรียงลำดับ และการกรองข้อมูล สำหรับรายละเอียดเพิ่มเติม — ดูคำอธิบายดั้งเดิมของ GoF
ประเด็นสำคัญ
Strategy เป็นหนึ่งใน 23 รูปแบบ GoF (Gang of Four) ที่อธิบายไว้ในหนังสือ "Design Patterns: Elements of Reusable Object-Oriented Software" (1994) รูปแบบนี้แก้ปัญหาการเลือกอัลกอริทึมในรันไทม์ แทนที่จะเขียนคลาสเดียวที่มีคำสั่งเงื่อนไขมากมาย (if-else, switch) Strategy เสนอให้แยกแต่ละอัลกอริทึมออกเป็นคลาสแยกต่างหากที่มีอินเทอร์เฟสร่วมกัน บริบท (คลาสที่ใช้กลยุทธ์) เก็บการอ้างอิงไปยังอินเทอร์เฟซ Strategy และมอบหมายการทำงานให้กับกลยุทธ์รูปธรรม
โครงสร้างของรูปแบบ ประกอบด้วยสามองค์ประกอบ: Context (บริบท) เก็บการอ้างอิงไปยัง Strategy และเรียกเมธอดของมัน; Strategy (อินเทอร์เฟซ) ประกาศเมธอดร่วมสำหรับอัลกอริทึมทั้งหมด; ConcreteStrategy (กลยุทธ์รูปธรรม) นำอินเทอร์เฟซไปใช้และมีอัลกอริทึมรูปธรรม ไคลเอนต์สร้างกลยุทธ์ที่ต้องการและส่งต่อไปยังบริบทผ่านคอนสตรัคเตอร์ ตัวตั้งค่า หรือพารามิเตอร์เมธอด บริบทไม่รู้ว่ากลยุทธ์เฉพาะใดกำลังถูกดำเนินการ — มันทำงานกับอินเทอร์เฟซเท่านั้น
| องค์ประกอบ | บทบาท | ตัวอย่าง |
|---|---|---|
| Context | เก็บการอ้างอิงไปยัง Strategy | PaymentProcessor, Sorter |
| Strategy | อินเทอร์เฟสร่วมสำหรับอัลกอริทึม | Protocol PaymentStrategy |
| ConcreteStrategy | การนำอัลกอริทึมไปใช้รูปธรรม | CardPayment, PayPalPayment |
หลักการ Open/Closed — ข้อได้เปรียบหลักของ Strategy ระบบเปิดให้ขยายได้ (สามารถเพิ่มกลยุทธ์ใหม่) และปิดให้แก้ไข (โค้ดบริบทไม่จำเป็นต้องเปลี่ยน) หากไม่มีรูปแบบ การเพิ่มอัลกอริทึมใหม่จำเป็นต้องเปลี่ยนคลาสที่มีอยู่ ซึ่งละเมิด OCP และเพิ่มความเสี่ยงของข้อผิดพลาดจากการถดถอย Strategy ยังลดขนาดของคลาส: แทนที่คลาส 200 บรรทัดที่มี switch-case คุณจะได้ 6 คลาส ๆ ละ 20 บรรทัด
Strategy ใน Swift ถูกนำไปใช้ผ่าน Protocol (อินเทอร์เฟซกลยุทธ์) และคลาสหรือโครงสร้างกลยุทธ์ โปรโตคอลของ Swift รองรับประเภทที่เกี่ยวข้องและข้อจำกัดเจนเนอริก ซึ่งให้ความยืดหยุ่นเมื่อออกแบบกลยุทธ์ บริบทมักจะเป็นคลาส ViewModel หรือบริการที่ยอมรับกลยุทธ์ใน init หรือผ่านคุณสมบัติ รูปแบบนี้ใช้กันอย่างแพร่หลายในโครงการ iOS สำหรับการจัดการเหตุการณ์ อนิเมชัน การจัดรูปแบบข้อมูล และกลยุทธ์ UI
// 1. Protocol Strategy
protocol PaymentStrategy {
func pay(amount: Decimal) async throws -> PaymentResult
}
// 2. Concrete Strategies
struct CardPaymentStrategy: PaymentStrategy {
let cardNumber: String
let cvv: String
func pay(amount: Decimal) async throws -> PaymentResult {
// ส่งคำขอไปยัง API ธนาคาร
return PaymentResult(status: .success, transactionId: "tx_\(UUID())")
}
}
struct PayPalPaymentStrategy: PaymentStrategy {
let email: String
func pay(amount: Decimal) async throws -> PaymentResult {
// เปลี่ยนเส้นทางไปยัง PayPal SDK
return PaymentResult(status: .success, transactionId: "pp_\(UUID())")
}
}
// 3. Context
class PaymentProcessor {
private var strategy: PaymentStrategy
init(strategy: PaymentStrategy) {
self.strategy = strategy
}
func setStrategy(_: PaymentStrategy) {
strategy = strategy
}
func processPayment(amount: Decimal) async throws -> PaymentResult {
return try await strategy.pay(amount: amount)
}
}
// การใช้งาน
let processor = PaymentProcessor(strategy: CardPaymentStrategy(cardNumber: "4111...", cvv: "123"))
let result = try await processor.processPayment(amount: 99.99)
Strategy ใน SwiftUI — รูปแบบผสานรวมกับ MVVM ได้อย่างเป็นธรรมชาติ ViewModel มีคุณสมบัติกลยุทธ์และเรียกเมธอดของมันเมื่อผู้ใช้ดำเนินการ SwiftUI View รับข้อมูลผ่าน @Published หรือ @State — กลยุทธ์ซ่อนรายละเอียดการนำไปใช้จาก View ตัวอย่างเช่น กลยุทธ์การตรวจสอบความถูกต้องของข้อความ (emailValidator, phoneValidator) จะถูกสับเปลี่ยนตามประเภทของฟิลด์อินพุต การรวม Strategy กับ SwiftUI ให้ความยืดหยุ่นโดยไม่ต้องสืบทอดจาก UIKit
Strategy ใน Kotlin ใช้ Interface ในระดับภาษาและอินเทอร์เฟซเชิงฟังก์ชัน (SAM) เพื่อความเรียบง่าย Kotlin รองรับแลมบ์ดา ซึ่งอนุญาตให้ส่งผ่านอัลกอริทึมเป็นฟังก์ชันโดยไม่ต้องประกาศคลาสกลยุทธ์แยกต่างหาก ใน Android รูปแบบนี้ใช้ใน ViewModel และ Use Cases เพื่อแยกอัลกอริทึมการโหลดข้อมูล การแคช และการจัดการข้อผิดพลาด โครงการ Android ที่ใช้ Clean Architecture ใช้ Strategy เพื่อฉีดการนำที่เก็บข้อมูลไปใช้ต่างกันตามแฟล็ก (mock, real, cache)
// 1. Interface Strategy
interface PaymentStrategy {
suspend fun pay(amount: BigDecimal): PaymentResult
}
// 2. Concrete Strategies
class CardPaymentStrategy(
private val cardNumber: String,
private val cvv: String
) : PaymentStrategy {
override suspend fun pay(amount: BigDecimal): PaymentResult {
// API ธนาคารผ่าน Retrofit
return PaymentResult(success = true, transactionId = "tx_${UUID.randomUUID()}")
}
}
class PayPalPaymentStrategy(
private val email: String
) : PaymentStrategy {
override suspend fun pay(amount: BigDecimal): PaymentResult {
// การรวม PayPal SDK
return PaymentResult(success = true, transactionId = "pp_${UUID.randomUUID()}")
}
}
// 3. Context
class PaymentProcessor(
private val strategy: PaymentStrategy
) {
fun setStrategy(strategy: PaymentStrategy): PaymentProcessor {
return PaymentProcessor(strategy)
}
suspend fun processPayment(amount: BigDecimal): PaymentResult {
return strategy.pay(amount)
}
}
// การใช้งานใน ViewModel
class CheckoutViewModel : ViewModel() {
private var processor = PaymentProcessor(CardPaymentStrategy("4111...", "123"))
fun payWithCard() {
viewModelScope.launch {
val result = processor.processPayment(BigDecimal("99.99"))
// การประมวลผลผลลัพธ์
}
}
}
Strategy กับ Hilt/Dagger — ในโครงการ Android กลยุทธ์มักถูกฉีดผ่าน DI Hilt ให้การนำ PaymentStrategy ไปใช้รูปธรรมผ่าน @Binds หรือ @Provides สิ่งนี้อนุญาตให้เปลี่ยนกลยุทธ์โดยไม่ต้องแก้ไขโค้ดบริบท — เพียงเปลี่ยนโมดูล DI สำหรับบิลด์อื่น (debug/release) ตัวอย่างเช่น MockPaymentStrategy ถูกฉีดสำหรับการดีบัก กลยุทธ์ธนาคารจริงสำหรับการผลิต การรวม Strategy + DI ให้ความยืดหยุ่นสูงสุด
Strategy vs State — โครงสร้างรูปแบบเหมือนกัน: ทั้งสองใช้องค์ประกอบกับอินเทอร์เฟซและคลาสรูปธรรม ความแตกต่างอยู่ที่วัตถุประสงค์: Strategy เลือกอัลกอริทึมอิสระ State ควบคุมพฤติกรรมของวัตถุตามสถานะของมัน ใน State บริบทเองเปลี่ยนกลยุทธ์เมื่อสถานะเปลี่ยน; ใน Strategy บริบทไม่ได้ควบคุมการสับเปลี่ยน — ไคลเอนต์กำหนดอัลกอริทึมอย่างชัดเจน กลยุทธ์ไม่รู้จักกันและกัน ในขณะที่สถานะสามารถเปลี่ยนผ่านระหว่างกันได้
Strategy vs Command — Command ห่อหุ้มการกระทำเดียวเป็นวัตถุ Strategy ห่อหุ้มชุดของอัลกอริทึมที่สามารถสับเปลี่ยนกันได้ Command คือ "ทำอะไร" (การเรียก execute ครั้งเดียว) Strategy คือ "ทำอย่างไร" (อัลกอริทึมหลายขั้นตอน) Command ใช้สำหรับคิว การดำเนินการล่าช้า เลิกทำ/ทำซ้ำ Strategy ใช้สำหรับเลือกวิธีทำงานในรันไทม์ คำสั่งสามารถกำหนดพารามิเตอร์ด้วยกลยุทธ์ รวมทั้งสองรูปแบบเข้าด้วยกัน
| คุณลักษณะ | Strategy | State | Command | Template Method |
|---|---|---|---|---|
| วัตถุประสงค์ | อัลกอริทึมที่สับเปลี่ยนกันได้ | พฤติกรรมตามสถานะ | การห่อหุ้มคำขอ | โครงร่างอัลกอริทึม |
| การเปลี่ยน | โดยไคลเอนต์อย่างชัดเจน | โดยบริบทอัตโนมัติ | โดยไคลเอนต์หรือคิว | โดยการสืบทอด |
| ระดับ | วัตถุ (องค์ประกอบ) | วัตถุ (องค์ประกอบ) | วัตถุ | คลาส (การสืบทอด) |
Strategy vs Template Method — ทั้งสองรูปแบบกำหนดอัลกอริทึมแต่ในวิธีที่ต่างกัน Template Method ใช้การสืบทอด: คลาสพื้นฐานกำหนดโครงร่างของอัลกอริทึม (เมธอดเทมเพลต) คลาสย่อยแทนที่ขั้นตอนแต่ละขั้นตอน Strategy ใช้องค์ประกอบ: อัลกอริทึมถูกแยกออกไปยังคลาสแยกต่างหากโดยสมบูรณ์ Template Method ง่ายกว่าสำหรับกรณีที่มีโครงสร้างอัลกอริทึมตายตัว Strategy — เมื่ออัลกอริทึมแตกต่างกันโดยสิ้นเชิงและสามารถเปลี่ยนแบบไดนามิก
การประมวลผลการชำระเงิน — ตัวอย่างคลาสสิกของ Strategy ตะกร้าสินค้าของร้านค้าออนไลน์มีรายการสินค้า และวิธีการชำระเงินถูกเลือกโดยผู้ใช้ แต่ละวิธี (บัตร, PayPal, Apple Pay, Google Pay, สกุลเงินดิจิทัล) เป็นกลยุทธ์แยกต่างหากที่มีลายเซ็น pay(amount) ร่วมกัน บริบท PaymentProcessor ไม่รู้ว่าการชำระเงินถูกประมวลผลอย่างไร — มันเรียกเมธอดร่วม การเพิ่มวิธีการชำระเงินใหม่ไม่จำเป็นต้องเปลี่ยนโค้ดตะกร้าสินค้า
การตรวจสอบความถูกต้องของข้อมูล — Strategy ใช้สำหรับกฎการตรวจสอบความถูกต้องต่างกันของฟิลด์เดียวกัน EmailValidatorStrategy, PhoneValidatorStrategy, AgeValidatorStrategy นำอินเทอร์เฟซ ValidationStrategy ร่วมกับเมธอด validate(input) ไปใช้ ฟอร์มลงทะเบียนใช้ชุดกลยุทธ์เพื่อตรวจสอบแต่ละฟิลด์ กลยุทธ์การตรวจสอบความถูกต้องสามารถรวมในห่วงโซ่ (Chain of Responsibility) หรือใช้ทั้งหมดพร้อมกันในลูป สิ่งนี้แทนที่การตรวจสอบ if-else ยาวด้วยชุดของตัวตรวจสอบความถูกต้องแบบพหุสัณฐาน
// กลยุทธ์การเรียงลำดับ
protocol SortingStrategy {
func sort<T>(_ items: [T]) -> [T] where T: Comparable
}
struct QuickSortStrategy: SortingStrategy {
func sort<T>(_ items: [T]) -> [T] { /* quicksort */ items }
}
struct MergeSortStrategy: SortingStrategy {
func sort<T>(_ items: [T]) -> [T] { /* mergesort */ items }
}
class SortedDataSource<T> {
private var strategy: SortingStrategy
func display(_ items: [T]) { let sorted = strategy.sort(items) }
}
การรับรองความถูกต้อง — ในแอปมือถือ กลยุทธ์การรับรองความถูกต้องจะถูกสับเปลี่ยนตามผู้ให้บริการ AuthStrategy ด้วยเมธอด login(), logout(), getToken() ถูกนำไปใช้สำหรับ EmailPasswordAuth, GoogleAuth, AppleAuth, BiometricAuth บริบท AuthManager ยอมรับกลยุทธ์ผ่าน DI หรือโรงงาน สิ่งนี้อนุญาตให้เพิ่มผู้ให้บริการรับรองความถูกต้องใหม่โดยไม่ต้องเปลี่ยนหน้าจอเข้าสู่ระบบ รูปแบบ Strategy เป็นพื้นฐานสำหรับไลบรารี OAuth จำนวนมากและ Firebase Authentication
คำถามที่พบบ่อย
Strategy เหมาะสมเมื่อคุณมี 3+ อัลกอริทึมที่อาจเปลี่ยนแปลงหรือขยายได้ หากมี 2 อัลกอริทึมและมีความเสถียร — if-else ง่าย ๆ มีต้นทุนน้อยกว่า ใช้ Strategy เมื่ออัลกอริทึมถูกใช้ในส่วนต่าง ๆ ของแอปพลิเคชัน เมื่อคุณต้องสับเปลี่ยนอัลกอริทึมในรันไทม์ หรือเมื่อแต่ละอัลกอริทึมต้องการ dependencies และการทดสอบของตัวเอง
ไม่ มันเป็นรูปแบบที่แตกต่างกันที่มีโครงสร้างคล้ายกัน Strategy — ไคลเอนต์เลือกอัลกอริทึมอย่างชัดเจน และกลยุทธ์เป็นอิสระ State — วัตถุเปลี่ยนพฤติกรรมของมันเองเมื่อสถานะภายในเปลี่ยนแปลง และสถานะสามารถเปลี่ยนผ่านระหว่างกันได้ ใน State บริบทจัดการการเปลี่ยนแปลงสถานะ; ใน Strategy โค้ดไคลเอนต์จัดการ
ได้ ใน Swift และ Kotlin กลยุทธ์สามารถส่งผ่านเป็น closure หรือ lambda Swift: typealias PaymentHandler = (Decimal) async throws -> PaymentResult Kotlin: typealias PaymentFun = suspend (BigDecimal) -> PaymentResult สิ่งนี้ทำให้โค้ดง่ายขึ้นสำหรับกรณีง่าย ๆ แต่สูญเสียการตั้งชื่อและเอกสารประกอบ สำหรับ 1-2 อัลกอริทึม closure ก็เพียงพอ; สำหรับ 4+ ควรใช้คลาสแยกต่างหาก
แต่ละกลยุทธ์ถูกทดสอบด้วยการทดสอบหน่วยแยกต่างหากโดยใช้ dependencies จำลอง บริบทถูกทดสอบด้วยกลยุทธ์จำลอง — ตรวจสอบว่าบริบทเรียกเมธอดของกลยุทธ์และส่งพารามิเตอร์ที่ถูกต้อง ใน Swift ใช้ XCTest + โปรโตคอลสำหรับการจำลอง; ใน Kotlin ใช้ MockK หรือ Mockito ข้อได้เปรียบหลัก: แต่ละกลยุทธ์ถูกทดสอบแยกกันโดยไม่ต้องตั้งค่าที่ซับซ้อน
ใช่ Strategy เป็นหนึ่งใน 23 รูปแบบที่อธิบายไว้ในหนังสือ "Design Patterns: Elements of Reusable Object-Oriented Software" (Gamma, Helm, Johnson, Vlissides, 1994) มันอยู่ในกลุ่มรูปแบบพฤติกรรม ชื่ออื่น: Policy โค้ดตัวอย่างดั้งเดิมใน Smalltalk-80 มีให้ใน GoF ฉบับดั้งเดิม
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ