- 1. Template Method Pattern คืออะไร
- 2. ปัญหาก่อนใช้แพตเทิร์น
- 3. องค์ประกอบสำคัญ
- 4. โครงสร้างคลาส
- 5. โค้ดตัวอย่างที่รันได้
- 6. วิธีคอมไพล์และรัน
- 7. การทำงานทีละขั้นตอน
- 8. ทำไมต้องเรียกผ่านชนิด ReportGenerator
- 9. ข้อดีและข้อจำกัด
- 10. Template Method เทียบกับ Strategy
- 11. ตัวอย่างแนวคิดใน Java Collections
- 12. ข้อควรระวังในการออกแบบ
- 13. Workshop: เพิ่ม MarkdownReport
- 14. การตรวจสอบตัวอย่าง
- 15. แหล่งอ้างอิง
คลาสแม่กำหนดโครงของอัลกอริทึม และเปิดให้คลาสลูกกำหนดรายละเอียดของบางขั้นตอน
หมวดหมู่: Behavioral Design Pattern
ตัวอย่าง: ระบบสร้างรายงานข้อความและ HTML
พื้นฐาน: Java, inheritance, abstract class, method overriding และ polymorphism
#1. Template Method Pattern คืออะไร

Template Method Pattern เป็นรูปแบบการออกแบบเชิงพฤติกรรมที่กำหนด โครงและลำดับของอัลกอริทึมไว้ในเมธอดของคลาสแม่ แล้วเปิดให้คลาสลูกกำหนดรายละเอียดบางขั้นตอนผ่านการ override โดยใช้ลำดับหลักร่วมกัน
จุดสำคัญอยู่ที่การแยกสองสิ่งนี้ออกจากกัน:
- โครงของงาน: ขั้นตอนใดเกิดก่อน ขั้นตอนใดเกิดหลัง และขั้นตอนใดต้องใช้ร่วมกัน
- รายละเอียดของแต่ละขั้นตอน: คลาสลูกทำขั้นตอนนั้นอย่างไร
ตัวอย่างเช่น การสร้างรายงานหลายรูปแบบอาจใช้ลำดับเดียวกัน ได้แก่ ตรวจข้อมูล สร้างส่วนหัว สร้างเนื้อหา เพิ่มหมายเหตุ และสร้างส่วนท้าย แต่รายงานข้อความกับรายงาน HTML มีวิธีจัดรูปแบบต่างกัน
คำว่า Template ในที่นี้หมายถึงโครงของอัลกอริทึม ไม่ใช่ไฟล์แม่แบบ HTML และไม่ใช่ generic type ของ Java
#2. ปัญหาก่อนใช้แพตเทิร์น
สมมติว่า PlainTextReport และ HtmlReport ต่างมีเมธอด generate() ของตนเอง แต่ละคลาสจึงต้องเขียนการตรวจข้อมูลและลำดับการประกอบรายงานซ้ำ
เมื่อเพิ่มกฎใหม่ เช่น ห้ามสร้างรายงานจากรายการว่าง นักพัฒนาต้องแก้หลายแห่ง หากแก้ไม่ครบ รายงานแต่ละชนิดอาจตรวจข้อมูลต่างกันหรือประกอบส่วนต่าง ๆ คนละลำดับ
Template Method ช่วยย้ายส่วนที่ใช้ร่วมกันไปยังคลาสแม่ ส่วนคลาสลูกเก็บเฉพาะรายละเอียดที่แตกต่างกัน
อย่างไรก็ตาม การมี abstract class เพียงอย่างเดียวยังไม่เพียงพอที่จะเรียกว่า Template Method ต้องมีเมธอดที่กำหนดโครงอัลกอริทึม และเรียกขั้นตอนที่คลาสลูกสามารถกำหนดรายละเอียดได้ด้วย
#3. องค์ประกอบสำคัญ
| องค์ประกอบ | หน้าที่ | ตัวอย่างในบทความ |
|---|---|---|
| Abstract Class | เก็บโครงอัลกอริทึมและโค้ดที่ใช้ร่วมกัน | ReportGenerator |
| Template Method | เรียกขั้นตอนตามลำดับที่กำหนด | generate() |
| Abstract Step | ขั้นตอนที่ concrete subclass ต้องกำหนด | renderHeader(), renderBody(), renderFooter() |
| Concrete Step | ขั้นตอนที่คลาสแม่มี implementation ให้แล้ว | validate() |
| Hook | จุดขยายที่มีพฤติกรรมเริ่มต้นและเลือก override ได้ | renderNote() |
| Concrete Class | กำหนดรายละเอียดของขั้นตอน | PlainTextReport, HtmlReport |
| Client | เรียกใช้รายงานผ่านชนิดของคลาสแม่ | TemplateMethodDemo |
#Abstract method กับ hook ต่างกันอย่างไร
Abstract method ไม่มี implementation คลาสลูกที่เป็น concrete class จึงต้องมี implementation ของเมธอดนั้นเอง หรือรับ implementation ที่สืบทอดมา [1]
Hook มี implementation เริ่มต้นอยู่แล้ว เช่น คืนสตริงว่าง คลาสลูกจึงเลือกใช้ค่าเริ่มต้นหรือ override เพื่อเพิ่มพฤติกรรมได้
ในตัวอย่างนี้ renderNote() ถูกเรียกทุกครั้งที่สร้างรายงานสำเร็จ แต่รายงานข้อความใช้ค่าเริ่มต้นที่คืนสตริงว่าง จึงไม่มีหมายเหตุปรากฏในผลลัพธ์ คำว่าเลือกใช้ได้จึงหมายถึงเลือกเพิ่มพฤติกรรม ไม่ได้หมายความว่าเมธอดจะไม่ถูกเรียก
#จำเป็นต้องใช้ final หรือไม่
ไม่จำเป็นตามนิยามของแพตเทิร์น แต่การประกาศ template method เป็น final ช่วยป้องกันไม่ให้คลาสลูก override เมธอดหลักและเปลี่ยนลำดับงาน [2]
ตัวอย่างนี้ใช้ public final String generate(...) และกำหนด validate() เป็น private เพื่อเก็บกฎตรวจข้อมูลไว้ในคลาสแม่ ส่วนเมธอดที่เป็นจุดขยายใช้ protected
final ป้องกันการ override ตัวเมธอดหลัก แต่ไม่ได้รับประกันว่าคลาสลูกจะทำงานถูกต้องทั้งหมด คลาสลูกยังอาจคืนค่าผิดรูปแบบหรือโยน exception ได้ จึงต้องกำหนดสัญญาของแต่ละขั้นตอนให้ชัดเจน
#4. โครงสร้างคลาส
classDiagram
direction TB
class ReportGenerator {
<<abstract>>
+generate(items) String
-validate(items) void
#renderHeader() String
#renderBody(items) String
#renderNote(items) String
#renderFooter() String
}
class PlainTextReport {
#renderHeader() String
#renderBody(items) String
#renderFooter() String
}
class HtmlReport {
#renderHeader() String
#renderBody(items) String
#renderNote(items) String
#renderFooter() String
}
class TemplateMethodDemo {
+main(args) void
}
ReportGenerator <|-- PlainTextReport
ReportGenerator <|-- HtmlReport
TemplateMethodDemo ..> ReportGenerator : uses
ในแผนภาพ generate() เป็น template method ที่ประกาศ final ส่วน renderHeader(), renderBody() และ renderFooter() เป็น abstract methods รายละเอียด modifier ทั้งหมดแสดงในโค้ดด้านล่าง
#5. โค้ดตัวอย่างที่รันได้
บันทึกโค้ดทั้งหมดเป็นไฟล์ TemplateMethodDemo.java ตัวอย่างรวมหลายคลาสไว้ในไฟล์เดียวเพื่อทดลองได้สะดวก โดยมีเพียง TemplateMethodDemo ที่เป็น public top-level class หากนำไปใช้ในโครงการจริงสามารถแยกแต่ละคลาสเป็นไฟล์ได้
import java.util.List;
import java.util.Objects;
public class TemplateMethodDemo {
public static void main(String[] args) {
List<String> topics = List.of("Java", "JUnit & Playwright");
List<ReportGenerator> reports = List.of(
new PlainTextReport(), new HtmlReport());
for (ReportGenerator report : reports) {
System.out.println("=== " + report.getClass().getSimpleName() + " ===");
System.out.print(report.generate(topics));
}
}
}
abstract class ReportGenerator {
// Template Method: owns the order of operations.
public final String generate(List<String> items) {
Objects.requireNonNull(items, "items must not be null");
List<String> snapshot = List.copyOf(items);
validate(snapshot);
return renderHeader()
+ renderBody(snapshot)
+ renderNote(snapshot)
+ renderFooter();
}
// Shared step: subclasses cannot replace this validation.
private void validate(List<String> items) {
if (items.isEmpty()) {
throw new IllegalArgumentException("items must not be empty");
}
if (items.stream().anyMatch(String::isBlank)) {
throw new IllegalArgumentException("items must not contain blank text");
}
}
protected abstract String renderHeader();
protected abstract String renderBody(List<String> items);
protected abstract String renderFooter();
// Hook: subclasses may keep this default or override it.
protected String renderNote(List<String> items) {
return "";
}
}
final class PlainTextReport extends ReportGenerator {
@Override
protected String renderHeader() {
return "Report\n";
}
@Override
protected String renderBody(List<String> items) {
StringBuilder output = new StringBuilder();
for (String item : items) {
output.append("- ").append(item).append('\n');
}
return output.toString();
}
@Override
protected String renderFooter() {
return "";
}
}
final class HtmlReport extends ReportGenerator {
@Override
protected String renderHeader() {
return "<!doctype html>\n<html lang=\"en\">\n"
+ "<head><meta charset=\"UTF-8\"><title>Report</title></head>\n"
+ "<body>\n<h1>Report</h1>\n";
}
@Override
protected String renderBody(List<String> items) {
StringBuilder output = new StringBuilder("<ul>\n");
for (String item : items) {
output.append("<li>").append(escapeHtml(item)).append("</li>\n");
}
return output.append("</ul>\n").toString();
}
@Override
protected String renderNote(List<String> items) {
return "<p>Items: " + items.size() + "</p>\n";
}
@Override
protected String renderFooter() {
return "</body>\n</html>\n";
}
// Escaping for HTML text nodes, not a general HTML sanitizer.
private static String escapeHtml(String text) {
return text.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
}
#เหตุผลของรายละเอียดในโค้ด
generate()เป็นเจ้าของลำดับงานทั้งหมด คลาสลูกใช้เมธอดนี้ร่วมกันList.copyOf()สร้างรายการที่แก้ไขโครงสร้างผ่าน reference นี้ไม่ได้ และไม่ยอมรับสมาชิกnullช่วยไม่ให้ขั้นตอนของคลาสลูกแก้รายการระหว่างการ rendervalidate()ปฏิเสธรายการว่างและข้อความว่างก่อนเรียกขั้นตอน renderPlainTextReportไม่ overriderenderNote()จึงใช้ hook ที่คืนสตริงว่างHtmlReportoverride hook เพื่อแสดงจำนวนรายการก่อนปิดเอกสาร HTMLescapeHtml()แปลงอักขระพิเศษก่อนใส่ข้อมูลลงใน HTML text node ฟังก์ชันนี้ไม่ใช่ตัวทำความสะอาด HTML ทั่วไป และไม่ครอบคลุมบริบท URL, JavaScript หรือ CSS
ขั้นตอน render ควรคืนสตริงที่ไม่ใช่ null และไม่เปลี่ยนข้อมูลนำเข้า ตัวอย่างนี้สร้างรายงานในหน่วยความจำ จึงไม่ได้เปิดไฟล์หรือการเชื่อมต่อภายนอก
#6. วิธีคอมไพล์และรัน
ต้องติดตั้ง JDK เพื่อให้มีทั้ง javac และ java
javac -encoding UTF-8 TemplateMethodDemo.java
java TemplateMethodDemo
หรือใช้ source-file mode บน JDK 17 ขึ้นไป:
java TemplateMethodDemo.java
ผลลัพธ์:
=== PlainTextReport ===
Report
- Java
- JUnit & Playwright
=== HtmlReport ===
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8"><title>Report</title></head>
<body>
<h1>Report</h1>
<ul>
<li>Java</li>
<li>JUnit & Playwright</li>
</ul>
<p>Items: 2</p>
</body>
</html>
โปรแกรมพิมพ์รายงานออกทาง console หากต้องการเปิด HTML ในเบราว์เซอร์ ให้เขียนเฉพาะผลลัพธ์จาก new HtmlReport().generate(topics) ลงไฟล์ .html ด้วย UTF-8
#7. การทำงานทีละขั้นตอน
เมื่อ client เรียก report.generate(topics):
- คลาสแม่ตรวจว่ารายการไม่ใช่
nullและสร้าง snapshot ของรายการ - คลาสแม่เรียก
validate()เพื่อตรวจรายการว่างและข้อความว่าง - เรียก
renderHeader()ของชนิดออบเจ็กต์จริง - เรียก
renderBody()ของชนิดออบเจ็กต์จริง - เรียก
renderNote()ซึ่งอาจใช้ค่าเริ่มต้นหรือ implementation ที่คลาสลูก override - เรียก
renderFooter()แล้วคืนรายงานที่ประกอบเสร็จ
แม้ตัวแปร report จะมีชนิด ReportGenerator แต่การเรียกขั้นตอนที่ override ได้จะเลือก implementation ตามออบเจ็กต์จริงผ่าน polymorphism
นี่คือตัวอย่างของแนวคิด Inversion of Control ในระดับคลาส: คลาสลูกกำหนดรายละเอียดไว้ แล้วคลาสแม่เป็นผู้เรียกรายละเอียดเหล่านั้นในเวลาที่กำหนด มักอธิบายด้วยคำว่า Hollywood Principle: “Don't call us, we'll call you.”
หากขั้นตอนใดโยน exception เมธอดจะหยุดและไม่เรียกขั้นตอนถัดไป ดังนั้น renderFooter() ในตัวอย่างนี้เป็นส่วนท้ายของเอกสาร ไม่ใช่กลไก cleanup ทรัพยากรที่รับประกันว่าจะทำงานเสมอ
#8. ทำไมต้องเรียกผ่านชนิด ReportGenerator
ReportGenerator report = new HtmlReport();
String result = report.generate(List.of("Java", "Testing"));
Client รู้จักเพียง API หลัก ส่วนชนิดของรายงานกำหนดรายละเอียดการ render เมื่อเพิ่มรายงานชนิดใหม่จึงใช้โครงของคลาสแม่เดิมได้
Client หรือจุดสร้างออบเจ็กต์ยังต้องเลือกว่าจะใช้รายงานชนิดใด การเพิ่มคลาสใหม่จึงไม่ได้หมายความว่าโค้ดทุกส่วนจะไม่ต้องแก้เลย หากมีจุดเลือกชนิดรายงานจำนวนมากอาจพิจารณา factory หรือ dependency injection เพิ่มตามความจำเป็น
#9. ข้อดีและข้อจำกัด
| ด้าน | ผลต่อการออกแบบ |
|---|---|
| ใช้โค้ดร่วมกัน | รวมกฎตรวจข้อมูลและลำดับการสร้างรายงานไว้ที่เดียว |
| ความสม่ำเสมอ | รายงานแต่ละชนิดใช้ขั้นตอนหลักเหมือนกัน |
| การขยาย | เพิ่มชนิดรายงานผ่านคลาสลูกและจุดขยายที่เตรียมไว้ |
| การทดสอบ | ตรวจโครงลำดับร่วมกัน และตรวจผลของแต่ละคลาสลูกแยกได้ |
| การผูกกับคลาสแม่ | การเปลี่ยนสัญญาของ protected methods อาจกระทบคลาสลูกหลายตัว |
| ข้อจำกัด inheritance | Java สืบทอดคลาสได้เพียงหนึ่งคลาส จึงใช้โอกาสในการสืบทอดส่วนนี้ไป [1] |
| จำนวนจุดขยาย | Abstract methods และ hooks ที่มากเกินไปทำให้เข้าใจวงจรงานยาก |
| ความยืดหยุ่น | งานที่ต้องสลับพฤติกรรมหลายส่วนอย่างอิสระอาจเหมาะกับ composition มากกว่า |
#10. Template Method เทียบกับ Strategy
| ประเด็น | Template Method | Strategy |
|---|---|---|
| กลไกหลัก | Inheritance และ method overriding | Composition และ delegation |
| ผู้กำหนดโครงงาน | คลาสแม่กำหนดโครงและเรียกขั้นตอน | Context เรียกออบเจ็กต์ strategy ตามขอบเขตที่ออกแบบ |
| วิธีเปลี่ยนพฤติกรรม | ใช้ออบเจ็กต์ของคลาสลูกอีกชนิด | ส่งหรือเปลี่ยนออบเจ็กต์ strategy |
| การสลับระหว่างทำงาน | โดยทั่วไปใช้รายงานอีกออบเจ็กต์หนึ่ง | รองรับได้หาก API ของ context เปิดให้เปลี่ยน strategy |
| เหมาะกับ | งานที่มีโครงขั้นตอนร่วมกันและแตกต่างบางขั้นตอน | พฤติกรรมที่ต้องประกอบหรือเปลี่ยนได้อย่างอิสระ |
ทั้งสองแพตเทิร์นใช้ร่วมกันได้ เช่น คลาสแม่กำหนดลำดับสร้างรายงานด้วย Template Method และมอบหมายขั้นตอนจัดรูปแบบบางส่วนให้ strategy
#11. ตัวอย่างแนวคิดใน Java Collections
AbstractList ของ Java เป็นตัวอย่างที่ช่วยให้เห็นแนวคิดการเตรียมโครง implementation ไว้ให้คลาสลูก เอกสารระบุว่าสามารถสร้าง list ที่ไม่รองรับการแก้ไขผ่าน API โดย implement get(int) และ size() และใช้พฤติกรรมส่วนอื่นที่คลาสแม่เตรียมไว้ได้ [3]
ตัวอย่างเชิงโครงอัลกอริทึมที่ชัดเจนคือ indexOf(Object) ของ AbstractList ซึ่งมีการค้นหาผ่าน list iterator และเรียกพฤติกรรมพื้นฐานของ list อีกทอดหนึ่ง นี่เป็นการเชื่อมโยงเชิงการออกแบบจากพฤติกรรมที่เอกสารระบุ ไม่ใช่การกล่าวว่าเอกสาร API ตั้งชื่อเมธอดนี้ว่า Template Method โดยตรง
ตัวอย่างนี้ยังแสดงว่า template method ไม่จำเป็นต้องเป็น final เสมอไป เอกสาร AbstractList เปิดให้ override เมธอดที่มี implementation แล้วเพื่อให้เหมาะกับประสิทธิภาพของโครงสร้างข้อมูล [3]
#12. ข้อควรระวังในการออกแบบ
- เปิดให้ override เท่าที่จำเป็น: ขั้นตอนที่เป็นกฎตายตัวอาจใช้
privateส่วนจุดขยายใช้protected - อย่าเรียกเมธอดที่ override ได้จาก constructor: implementation ของคลาสลูกอาจทำงานก่อนสถานะของคลาสลูกพร้อม [2]
- กำหนดสัญญาของขั้นตอน: ระบุข้อมูลนำเข้า ผลลัพธ์ การเปลี่ยนสถานะ และข้อผิดพลาดที่ยอมรับได้
- หลีกเลี่ยง shared mutable state: ตัวอย่างนี้เก็บข้อมูลรายงานในตัวแปรเฉพาะการเรียก ไม่เก็บไว้ใน field ที่เปลี่ยนตลอดเวลา
- จัดการ resource ตามวงจรชีวิตจริง: หากเพิ่มการอ่านไฟล์หรือฐานข้อมูล ใช้
try-with-resourcesหรือfinallyตามความเหมาะสม อย่าคาดหวังว่า hook ท้ายงานจะทำหน้าที่ cleanup อัตโนมัติ - ใช้ inheritance เมื่อสัมพันธ์กันจริง: หากคลาสลูกต้องยกเลิกหรือข้ามขั้นตอนส่วนใหญ่ของคลาสแม่ ควรทบทวนโครงหรือพิจารณา composition
#13. Workshop: เพิ่ม MarkdownReport
เป้าหมาย: เพิ่มรายงาน Markdown โดยใช้ generate() เดิม
#ขั้นตอน
- สร้าง
final class MarkdownReport extends ReportGenerator - ให้
renderHeader()คืน"# Report\n\n" - ให้
renderBody()แสดงรายการเป็น bullet list - ให้
renderFooter()คืนสตริงว่าง - Override
renderNote()ให้แสดงจำนวนรายการ เช่น"\nTotal: 2\n" - เพิ่ม
new MarkdownReport()ในรายการreportsภายในmain()
ใช้ข้อมูลตัวอย่างเดิม ผลลัพธ์เฉพาะส่วน Markdown ควรเป็น:
# Report
- Java
- JUnit & Playwright
Total: 2
สำหรับแบบฝึกหัดนี้ใช้ข้อความธรรมดาตามตัวอย่าง หากนำข้อมูลผู้ใช้ทั่วไปไปสร้าง Markdown ให้กำหนดนโยบายการ escape เครื่องหมายที่มีความหมายใน Markdown เพิ่มเติม
#เกณฑ์ตรวจงาน
- ไม่แก้เมธอด
generate()เพื่อเพิ่มรายงานชนิดใหม่ - รายงานใหม่ใช้กฎตรวจข้อมูลเดิม
- หมายเหตุปรากฏหลังเนื้อหาและก่อนส่วนท้าย
- ทดลองส่งรายการว่างแล้วได้
IllegalArgumentException - ทดลองส่งข้อความว่างแล้วไม่เกิดการ render รายงาน
#คำถามอภิปราย
- เหตุใด
renderNote()จึงเหมาะจะเป็น hook? - หากเพิ่มเงื่อนไขใน
generate()แยกตามชื่อคลาสลูก จะมีผลต่อการขยายระบบอย่างไร? - กรณีใดที่ควรเปลี่ยนไปใช้ Strategy หรือ composition?
- หากงานบางประเภทไม่ต้องตรวจข้อมูลตามกฎเดียวกัน ยังควรอยู่ใน inheritance hierarchy เดียวกันหรือไม่?
#14. การตรวจสอบตัวอย่าง
โค้ดตัวอย่างผ่านการคอมไพล์และรันบน OpenJDK 17 โดยตรวจพฤติกรรมต่อไปนี้:
| กรณี | ผลที่ตรวจสอบ |
|---|---|
| สร้างรายงานข้อความและ HTML | ผลลัพธ์ตรงกับตัวอย่าง |
| ตรวจลำดับของขั้นตอน | Header → Body → Note → Footer |
| รายการว่างหรือข้อความว่าง | ปฏิเสธก่อนเริ่ม render |
รายการหรือสมาชิกเป็น null |
ได้ NullPointerException |
| คลาสลูกพยายามเพิ่มสมาชิกใน snapshot | ได้ UnsupportedOperationException |
| ใช้ hook เริ่มต้น | ไม่มีข้อความหมายเหตุ |
| Override hook | แสดงหมายเหตุในตำแหน่งที่กำหนด |
| HTML text ที่มีอักขระพิเศษ | แปลงอักขระก่อนแทรกลง text node |
| Modifier ของ template method | generate() เป็น final |
#15. แหล่งอ้างอิง
- Oracle — Abstract Methods and Classes: กลไก abstract class, abstract method และข้อจำกัดการสืบทอดคลาสใน Java
- Oracle — Writing Final Classes and Methods: การป้องกัน method overriding ด้วย
finalและข้อควรระวังเรื่อง constructor - Oracle — AbstractList — Java SE 21 API: skeletal implementation และข้อกำหนดของเมธอดที่มี implementation ให้แล้ว
เอกสาร Java Tutorials ในข้อ 1–2 เขียนสำหรับ JDK 8 บทความนี้ใช้อ้างอิงกลไกพื้นฐานเรื่อง inheritance, abstract และ final ส่วนโค้ดตัวอย่างใช้ Java 17 เป็นฐานและไม่พึ่งเฟรมเวิร์ก