Template Method
จุดประสงค์
หัวข้อที่มีชื่อว่า “จุดประสงค์”Template Method นิยามโครงสร้างโดยรวมของอัลกอริทึมไว้ที่เดียว คือ template method แล้วเลื่อนขั้นตอนที่เลือกไว้ให้ subclass โครงร่าง รวมถึงลำดับขั้นตอนที่ตรึงไว้ อยู่ใน class หลัก ส่วนที่ผันแปรคือ hook ที่ override ได้ subclass เปลี่ยน อะไร ที่เกิดขึ้นในแต่ละขั้นโดยไม่เปลี่ยน ลำดับ
งานหลายอย่างมีรูปร่างเดียวกันแต่ต่างกันที่รายละเอียด pipeline ประมวลผลข้อมูลมักจะอ่าน input แจงข้อมูล แปลงเรคคอร์ด แล้วเขียนผลลัพธ์เสมอ แต่แหล่งข้อมูล CSV กับแหล่ง JSON แจงต่างกัน และรายงานหนึ่งอาจกรองแถวที่อีกรายงานเก็บไว้ การคัดลอก pipeline ทั้งชุดสำหรับแต่ละตัวแปรทำให้โครงนั่งร้านของการอ่าน-เขียนซ้ำซ้อน และชวนให้สำเนาเหล่านั้นแยกออกจากกันเมื่อเวลาผ่านไป
ส่วนที่ตรึงไว้ในที่นี้คือ ลำดับของขั้นตอน ส่วนที่ผันแปรคือ เนื้อหา ของบางขั้น Template Method จับลำดับไว้ครั้งเดียวใน method ฐานที่เรียกขั้นตอนแต่ละขั้นตามลำดับ subclass override เพียงขั้นตอนที่ต่างกัน การควบคุมการไหลเป็นของ class หลัก ดังนั้นทุกตัวแปรจึงรันขั้นตอนในลำดับที่พิสูจน์แล้วเหมือนกัน เป็นตัวอย่างของการกลับด้านการควบคุม (inversion of control) แบบ “อย่าโทรหาเรา เราจะโทรหาคุณเอง”
โครงสร้าง
หัวข้อที่มีชื่อว่า “โครงสร้าง”classDiagram
class Pipeline {
<<abstract>>
+run(raw) string
#parse(raw)*
#transform(rows)
#format(rows)*
}
class CsvPipeline {
#parse(raw)
#format(rows)
}
class JsonPipeline {
#parse(raw)
#format(rows)
}
Pipeline <|-- CsvPipeline
Pipeline <|-- JsonPipeline - Abstract class — นิยาม template method ที่เรียกขั้นตอนตามลำดับตายตัว บางขั้นตอนเป็น abstract (ต้อง override) บางขั้นตอนเป็นค่าดีฟอลต์ที่เป็นรูปธรรม (อาจ override ได้) และตัว template method เองมักไม่ถูก override
- Concrete class — override ขั้นตอนที่ผันแปรได้เพื่อเจาะจงอัลกอริทึม
- Client — เรียก template method แล้วได้อัลกอริทึมทั้งชุด ที่ถูกปรับแต่งโดย subclass ที่ใช้
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”pipeline ประมวลผลข้อมูลที่ method run ตรึงลำดับ read-parse-transform-format ไว้ ส่วน subclass จัดหาขั้นตอน parse และ format ภาษาที่ไม่มีการสืบทอดแบบคลาสสิกแสดงแนวคิดเดียวกันด้วยการประกอบ (composition) หรือฟิลด์ฟังก์ชัน
abstract class Pipeline { // The template method: a fixed sequence of steps. run(raw: string): string { const rows = this.parse(raw); const kept = this.transform(rows); return this.format(kept); }
protected abstract parse(raw: string): string[];
// A default step subclasses may override. protected transform(rows: string[]): string[] { return rows.filter((r) => r.length > 0); }
protected abstract format(rows: string[]): string;}
class CsvPipeline extends Pipeline { protected parse(raw: string): string[] { return raw.split('\n'); } protected format(rows: string[]): string { return rows.join(','); }}
const csv = new CsvPipeline();console.log(csv.run('a\nb\n\nc')); // "a,b,c"from abc import ABC, abstractmethod
class Pipeline(ABC): def run(self, raw: str) -> str: rows = self.parse(raw) kept = self.transform(rows) return self.format(kept)
@abstractmethod def parse(self, raw: str) -> list[str]: ...
def transform(self, rows: list[str]) -> list[str]: return [r for r in rows if r]
@abstractmethod def format(self, rows: list[str]) -> str: ...
class CsvPipeline(Pipeline): def parse(self, raw: str) -> list[str]: return raw.split("\n")
def format(self, rows: list[str]) -> str: return ",".join(rows)
csv = CsvPipeline()print(csv.run("a\nb\n\nc")) # "a,b,c"package main
import ( "fmt" "strings")
// Go has no inheritance, so the varying steps are function fields and// the fixed sequence lives in Run.type Pipeline struct { Parse func(raw string) []string Format func(rows []string) string}
func (p Pipeline) transform(rows []string) []string { kept := rows[:0] for _, r := range rows { if r != "" { kept = append(kept, r) } } return kept}
func (p Pipeline) Run(raw string) string { rows := p.Parse(raw) kept := p.transform(rows) return p.Format(kept)}
func main() { csv := Pipeline{ Parse: func(raw string) []string { return strings.Split(raw, "\n") }, Format: func(rows []string) string { return strings.Join(rows, ",") }, } fmt.Println(csv.Run("a\nb\n\nc")) // "a,b,c"}// The trait holds the fixed run() as a default method that calls the// overridable steps. Implementors supply only parse and format.trait Pipeline { fn parse(&self, raw: &str) -> Vec<String>; fn format(&self, rows: &[String]) -> String;
fn transform(&self, rows: Vec<String>) -> Vec<String> { rows.into_iter().filter(|r| !r.is_empty()).collect() }
fn run(&self, raw: &str) -> String { let rows = self.parse(raw); let kept = self.transform(rows); self.format(&kept) }}
struct CsvPipeline;impl Pipeline for CsvPipeline { fn parse(&self, raw: &str) -> Vec<String> { raw.split('\n').map(str::to_string).collect() } fn format(&self, rows: &[String]) -> String { rows.join(",") }}
fn main() { let csv = CsvPipeline; println!("{}", csv.run("a\nb\n\nc")); // "a,b,c"}ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ / ข้อแลกเปลี่ยน”- ข้อดี: ขจัดความซ้ำซ้อนโดยเก็บโครงร่างร่วมไว้ที่เดียว
- ข้อดี: class หลักบังคับลำดับขั้นตอนที่ถูกต้อง subclass จึงไม่สามารถจัดลำดับใหม่โดยบังเอิญได้
- ข้อดี: ตัวแปรใหม่ต้อง override เพียงขั้นตอนที่ต่างกันเท่านั้น
- ข้อเสีย: pattern นี้พึ่งพาการสืบทอดซึ่งแข็งทื่อ subclass ถูกล็อกไว้กับอัลกอริทึมฐานตัวเดียว
- ข้อเสีย: hook มากเกินไปทำให้ method ฐานติดตามได้ยาก และการ override ขั้นตอนผิดอาจทำให้สัญญาเสียหายอย่างแนบเนียน
pattern ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “pattern ที่เกี่ยวข้อง”- Strategy บรรลุความผันแปรผ่านการประกอบ (composition) แทนการสืบทอด จึงสลับอัลกอริทึมทั้งชุดได้ในขณะรัน ขณะที่ Template Method ทำให้ขั้นตอนที่ตรึงไว้ผันแปร ณ เวลาสร้าง class ลูก
- Factory Method มักเป็นขั้นตอน ภายใน template method โดยอัลกอริทึมฐานเรียกขั้นตอนการสร้างที่ override ได้
| Template Method | Strategy | Factory Method | |
|---|---|---|---|
| mechanism | inheritance | composition | inheritance |
| เปลี่ยนส่วนไหน | step ย่อยใน algorithm | algorithm ทั้งชุด | ขั้นตอนการสร้าง object |
| เวลาที่เลือก | compile time (subclass) | runtime (inject) | compile time (subclass) |
| ตัวอย่าง | test setUp/tearDown, build pipeline | pricing, sort, compression | LoggerFactory, RouterFactory |
💡 หมายเหตุสำหรับ developer
Pattern นี้พบได้บ่อยใน:
- JUnit / Jest lifecycle hooks (
setUp/beforeEach,tearDown/afterEach) — framework กำหนดโครงของ test แต่ปล่อยให้ subclass เติมรายละเอียด- Django class-based views — base view กำหนดขั้นตอนของ request handling ทั่วไป ให้ subclass override เฉพาะส่วนที่ต่าง
- Spring
JdbcTemplate— จัดการ connection/transaction ให้อัตโนมัติ เหลือแค่ query logic ให้ผู้ใช้เติม