Split Phase
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”เมื่อ code บล็อกหนึ่งทำงานที่ต่างกันจริง ๆ สองอย่างเรียงกัน เช่น parse ข้อมูลเข้าแล้วค่อย คำนวณ ผลลัพธ์ต่อ ให้ซอยออกเป็นสอง phase โดย phase แรกสร้างโครงสร้างข้อมูลกลางที่ชัดเจนออกมา ส่วน phase ที่สองรับเฉพาะโครงสร้างนั้นไปใช้
Code Smell
หัวข้อที่มีชื่อว่า “Code Smell”นี่คือวิธีรักษา tangled responsibilities ภายใน code ช่วงเดียว การ parse และการคำนวณถูกสอดประสานกัน ดังนั้นการเปลี่ยน format ของ input ก็บังคับให้คุณต้องไปแตะคณิตศาสตร์ และการเปลี่ยนคณิตศาสตร์ก็บังคับให้คุณต้องคิดถึง string ทั้งสองความกังวลใช้ตัวแปรร่วมกัน และผู้อ่านไม่สามารถโฟกัสทีละอย่างได้เลย การตั้งชื่อโครงสร้างกลางขีดเส้นที่สะอาดระหว่างทั้งสอง
ก่อน → หลัง
หัวข้อที่มีชื่อว่า “ก่อน → หลัง”function ตัวนี้ parse string "name:qty:price" ดิบ ๆ แล้วคำนวณยอดรวมรวดเดียวจบ เราซอยออกเป็นสองขั้น คือ parse ให้เป็น order object ก่อน แล้วค่อยคิดราคาจาก order นั้น
// Beforefunction orderTotal(raw: string): number { const parts = raw.split(':'); const qty = Number(parts[1]); const price = Number(parts[2]); return qty > 100 ? qty * price * 0.9 : qty * price;}
// Afterinterface ParsedOrder { name: string; qty: number; price: number;}
function parseOrder(raw: string): ParsedOrder { const [name, qty, price] = raw.split(':'); return { name, qty: Number(qty), price: Number(price) };}
function priceOrder(order: ParsedOrder): number { const base = order.qty * order.price; return order.qty > 100 ? base * 0.9 : base;}
function orderTotal(raw: string): number { return priceOrder(parseOrder(raw));}# Beforedef order_total(raw): parts = raw.split(":") qty = int(parts[1]) price = float(parts[2]) return qty * price * 0.9 if qty > 100 else qty * price
# Afterfrom dataclasses import dataclass
@dataclassclass ParsedOrder: name: str qty: int price: float
def parse_order(raw): name, qty, price = raw.split(":") return ParsedOrder(name, int(qty), float(price))
def price_order(order): base = order.qty * order.price return base * 0.9 if order.qty > 100 else base
def order_total(raw): return price_order(parse_order(raw))// Beforefunc OrderTotal(raw string) float64 { parts := strings.Split(raw, ":") qty, _ := strconv.Atoi(parts[1]) price, _ := strconv.ParseFloat(parts[2], 64) if qty > 100 { return float64(qty) * price * 0.9 } return float64(qty) * price}
// Aftertype ParsedOrder struct { Name string Qty int Price float64}
func parseOrder(raw string) ParsedOrder { parts := strings.Split(raw, ":") qty, _ := strconv.Atoi(parts[1]) price, _ := strconv.ParseFloat(parts[2], 64) return ParsedOrder{Name: parts[0], Qty: qty, Price: price}}
func priceOrder(order ParsedOrder) float64 { base := float64(order.Qty) * order.Price if order.Qty > 100 { return base * 0.9 } return base}
func OrderTotal(raw string) float64 { return priceOrder(parseOrder(raw))}// Beforefn order_total(raw: &str) -> f64 { let parts: Vec<&str> = raw.split(':').collect(); let qty: i32 = parts[1].parse().unwrap(); let price: f64 = parts[2].parse().unwrap(); if qty > 100 { qty as f64 * price * 0.9 } else { qty as f64 * price }}
// Afterstruct ParsedOrder { name: String, qty: i32, price: f64,}
fn parse_order(raw: &str) -> ParsedOrder { let parts: Vec<&str> = raw.split(':').collect(); ParsedOrder { name: parts[0].to_string(), qty: parts[1].parse().unwrap(), price: parts[2].parse().unwrap(), }}
fn price_order(order: &ParsedOrder) -> f64 { let base = order.qty as f64 * order.price; if order.qty > 100 { base * 0.9 } else { base }}
fn order_total(raw: &str) -> f64 { price_order(&parse_order(raw))}flowchart LR R["Raw input<br/>(string)"] --> P1["Phase 1: Parse"] P1 --> I["Intermediate data<br/>(structured order)"] I --> P2["Phase 2: Calculate"] P2 --> O["Result<br/>(price)"]
กลไกการทำงาน
หัวข้อที่มีชื่อว่า “กลไกการทำงาน”- extract phase ที่สอง (การคำนวณ) ออกมาเป็น function ของตัวเอง รัน test
- สร้างโครงสร้างข้อมูลกลางขึ้นมา แล้วส่งเข้าไปเป็น argument ให้ function ของ phase ที่สอง
- ตรวจสอบแต่ละค่าที่ phase ที่สองอ่านจาก phase แรก ย้ายแต่ละค่าไปไว้บนโครงสร้างกลางทีละค่า โดยรัน test หลังการย้ายแต่ละครั้ง
- extract phase แรก (การ parse) ออกมาเป็น function ของตัวเองที่คืนโครงสร้างกลาง
- ยืนยันว่า function ระดับบนสุดตอนนี้เป็นเพียง phase แรกป้อนเข้า phase ที่สอง แล้ว รันชุด test ทั้งหมด
ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”ใช้ Split Phase เมื่อบล็อกหนึ่งจัดการสองขั้นตอนที่แยกกันชัดเจน เช่น parse แล้ว compute, validate แล้ว act หรือ fetch แล้ว format และสองขั้นนั้นแทบไม่มีอะไรร่วมกันนอกจากการส่งค่าต่อ โครงสร้างข้อมูลกลางจะกลายเป็น contract ที่มีชื่อคั่นระหว่างสองขั้น แต่ละ phase จึงเปลี่ยนได้อิสระและ test แยกกันได้
ราคาที่ต้องจ่ายคือ data type พิเศษหนึ่งอันและ indirection เล็ก ๆ ถ้าสอง “phase” พันกันแน่นหรือสั้นแบบไม่สำคัญ รอยต่อก็เป็นของปลอม — ปล่อยให้ code เป็นชิ้นเดียวไว้
เนื้อหาที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เนื้อหาที่เกี่ยวข้อง”| ใช้ Split Phase เมื่อ | หลีกเลี่ยงเมื่อ |
|---|---|
| function ทำสองขั้นตอนที่แยกกันชัดเจน | สองขั้นตอนนั้นพันกันจนแยกไม่ได้ง่าย |
| data ที่ phase แรกสร้างขึ้น ถูกใช้โดย phase สอง | แต่ละ phase มีแค่ 2-3 บรรทัด — overhead ไม่คุ้ม |
| ต้องการ test แต่ละ phase แยกกัน | กำลัง optimize performance — การแยก phase อาจเพิ่ม pass |
⚠️ ไม่ควร Split Phase เมื่อ:
- function นั้นทำขั้นตอนเดียวที่ชัดเจน — การแยกจะสร้าง complexity ไม่จำเป็น
- สองส่วนแชร์ state เยอะเกินไปจนต้องส่ง parameter มากมาย
- ทีมยังไม่คุ้นกับ data-transfer object pattern