- 1. เป้าหมายการเรียนรู้
- 2. ปัญหาก่อนใช้ Bridge Pattern
- 3. แยกสองแกนที่เปลี่ยนแปลงได้
- 4. องค์ประกอบของ Bridge Pattern
- 5. โครงสร้างคลาส
- 6. Workshop: เขียนโปรแกรมในไฟล์เดียว
- 7. คอมไพล์และรัน
- 8. การทำงานของโปรแกรม
- 9. ขยายระบบ: เพิ่ม Markdown
- 10. ขยายอีกฝั่ง: เพิ่มรายงานรายการแรก
- 11. เปรียบเทียบ Bridge, Adapter และ Strategy
- 12. ควรใช้เมื่อใด
- 13. ประโยชน์และสิ่งที่ต้องแลก
- 14. ข้อผิดพลาดที่พบบ่อย
- 15. กิจกรรมทดลองและเกณฑ์ตรวจสอบ
- 16. ข้อคิดสำหรับการนำไปใช้
- เอกสารอ้างอิง
Bridge Pattern เป็นรูปแบบการออกแบบเชิงโครงสร้าง (Structural Design Pattern) ที่แยก Abstraction และ Implementation ออกจากกัน เพื่อให้ทั้งสองฝั่งเปลี่ยนแปลงและขยายได้อย่างอิสระ โดยใช้การประกอบออบเจ็กต์ (Composition) เป็นสะพานเชื่อมระหว่างสองฝั่ง

ในบทความนี้จะทดลองสร้างระบบรายงานที่เลือกได้ทั้ง ชนิดรายงาน และ รูปแบบการแสดงผล พร้อมโค้ด Java ที่คัดลอกไปรันได้ในไฟล์เดียว
แนวคิดสำคัญ: รายงานเป็นผู้ตัดสินใจว่า “จะแสดงข้อมูลอะไร” ส่วนตัวจัดรูปแบบเป็นผู้ตัดสินใจว่า “จะแสดงข้อมูลนั้นอย่างไร”
#1. เป้าหมายการเรียนรู้
เมื่อจบบทความ ผู้อ่านจะสามารถ
- ระบุปัญหาที่เกิดจากการสร้างคลาสตามทุกคู่ของคุณสมบัติ
- อธิบายบทบาท Abstraction, Refined Abstraction, Implementor และ Concrete Implementor
- ใช้ interface และ constructor injection เพื่อประกอบสองฝั่งเข้าด้วยกัน
- เพิ่มชนิดรายงานหรือรูปแบบการแสดงผล โดยไม่ต้องสร้างคลาสสำหรับทุกคู่
- แยกความตั้งใจของ Bridge ออกจาก Adapter และ Strategy
พื้นฐานที่ควรมี: Java class, interface, inheritance, polymorphism และ List
เครื่องมือ: JDK 17 ขึ้นไป ไม่ต้องติดตั้งไลบรารีเพิ่มเติม ตัวอย่างนี้ใช้เฉพาะ Java Standard Library
#2. ปัญหาก่อนใช้ Bridge Pattern
สมมติว่าระบบมีชนิดรายงานสองแบบ
- รายงานสรุป: แสดงจำนวนรายการทั้งหมด
- รายงานละเอียด: แสดงข้อมูลแต่ละรายการ
และรองรับรูปแบบการแสดงผลสองแบบ
- ข้อความธรรมดา (Plain Text)
- HTML
หากสร้างคลาสเฉพาะสำหรับทุกคู่ อาจได้คลาสดังนี้
| ชนิดรายงาน | Plain Text | HTML |
|---|---|---|
| รายงานสรุป | SummaryTextReport |
SummaryHtmlReport |
| รายงานละเอียด | DetailedTextReport |
DetailedHtmlReport |
เมื่อเพิ่ม Markdown อาจต้องเพิ่ม SummaryMarkdownReport และ DetailedMarkdownReport อีกสองคลาส และเมื่อเพิ่มรายงานชนิดใหม่ ก็ต้องทำรูปแบบการแสดงผลทุกแบบให้รายงานนั้นด้วย
ถ้ามีชนิดรายงาน m แบบ และรูปแบบการแสดงผล n แบบ แนวทางสร้างคลาสสำหรับทุกคู่อาจต้องมีคลาสคู่ผสมถึง m × n คลาส
Bridge แยกความเปลี่ยนแปลงทั้งสองด้านออกเป็นคนละกลุ่ม จึงใช้ตัวเลือกฝั่งรายงาน m คลาส และตัวเลือกฝั่งจัดรูปแบบ n คลาส รวมเป็น m + n คลาสของตัวเลือก ตัวเลขนี้ไม่นับคลาสฐาน interface และคลาสสนับสนุน และเป็นการเปรียบเทียบกับแนวทางสร้างคลาสเฉพาะทุกคู่ ไม่ใช่สูตรนับคลาสทั้งหมดของทุกระบบ
#3. แยกสองแกนที่เปลี่ยนแปลงได้
| แกน | หน้าที่ | ตัวอย่าง |
|---|---|---|
| ชนิดรายงาน | เลือกเนื้อหาและระดับรายละเอียด | SummaryReport, DetailedReport |
| รูปแบบการแสดงผล | แปลงเนื้อหาให้อยู่ในรูปแบบปลายทาง | PlainTextFormatter, HtmlFormatter |
รายงานแต่ละชนิดถือ reference ของ ReportFormatter และเรียกใช้งานผ่านสัญญากลาง โดยไม่ต้องเลือกเองว่าตัวจัดรูปแบบเป็นคลาสใด
ความสัมพันธ์นี้เป็นแบบ has-a: Report มี ReportFormatter อยู่ภายใน ส่วนการสืบทอดยังคงใช้ภายในแต่ละฝั่งได้ เช่น SummaryReport extends Report
#4. องค์ประกอบของ Bridge Pattern
| บทบาท | ในตัวอย่าง | ความรับผิดชอบ |
|---|---|---|
| Abstraction | Report |
กำหนด API ระดับรายงาน และเก็บ reference ของตัวจัดรูปแบบ |
| Refined Abstraction | SummaryReport, DetailedReport |
กำหนดว่ารายงานชนิดนั้นจะมีข้อมูลอะไร |
| Implementor | ReportFormatter |
กำหนดสัญญาที่ฝั่งรายงานใช้เรียกงานจัดรูปแบบ |
| Concrete Implementor | PlainTextFormatter, HtmlFormatter |
สร้างผลลัพธ์ข้อความธรรมดาหรือ HTML |
| Client | BridgeDemo |
เลือกและประกอบชนิดรายงานเข้ากับรูปแบบที่ต้องการ |
คำว่า Abstraction ในชื่อบทบาทของแพตเทิร์นไม่ได้หมายความว่าต้องใช้คำสั่ง abstract ของ Java เสมอไป ตัวอย่างนี้ใช้ abstract class เพราะต้องการแบ่งปัน constructor และข้อมูลพื้นฐานระหว่างรายงานสองชนิด
ส่วน Implementor เป็นบทบาทของสัญญาฝั่ง implementation จึงใช้ Java interface ได้ ไม่จำเป็นต้องเป็นคลาสที่มีโค้ดทำงานจริงทั้งหมด
#5. โครงสร้างคลาส
classDiagram
direction TB
class Report {
<<abstract>>
#ReportFormatter formatter
#List~String~ entries
+generate() String
}
class SummaryReport {
+generate() String
}
class DetailedReport {
+generate() String
}
class ReportFormatter {
<<interface>>
+format(String title, List~String~ lines) String
}
class PlainTextFormatter {
+format(String title, List~String~ lines) String
}
class HtmlFormatter {
+format(String title, List~String~ lines) String
}
Report <|-- SummaryReport
Report <|-- DetailedReport
Report --> ReportFormatter : delegates formatting
ReportFormatter <|.. PlainTextFormatter
ReportFormatter <|.. HtmlFormatter
เส้นเชื่อมจาก Report ไปยัง ReportFormatter คือจุดเชื่อมของสองฝั่ง ใน UML ใช้ association เพราะตัวจัดรูปแบบถูกส่งเข้ามาจากภายนอกและอาจใช้ร่วมกันได้ ไม่ได้ระบุการเป็นเจ้าของอายุการใช้งานแบบ UML composition แม้ในบริบทการออกแบบโค้ดจะเรียกแนวทางนี้ว่า object composition
#6. Workshop: เขียนโปรแกรมในไฟล์เดียว
สร้างโฟลเดอร์สำหรับทดลอง และบันทึกโค้ดต่อไปนี้เป็นไฟล์ BridgeDemo.java
import java.util.List;
import java.util.Objects;
public class BridgeDemo {
public static void main(String[] args) {
List<String> entries = List.of("Build passed", "Tests passed");
ReportFormatter text = new PlainTextFormatter();
ReportFormatter html = new HtmlFormatter();
show("Summary / Text", new SummaryReport(text, entries));
show("Summary / HTML", new SummaryReport(html, entries));
show("Detailed / Text", new DetailedReport(text, entries));
show("Detailed / HTML", new DetailedReport(html, entries));
}
private static void show(String label, Report report) {
System.out.println("== " + label + " ==");
System.out.println(report.generate());
System.out.println();
}
}
// Implementor: the contract for formatting report content.
interface ReportFormatter {
String format(String title, List<String> lines);
}
// Concrete Implementor 1
final class PlainTextFormatter implements ReportFormatter {
@Override
public String format(String title, List<String> lines) {
StringBuilder output = new StringBuilder(title);
for (String line : lines) {
output.append("\n- ").append(line);
}
return output.toString();
}
}
// Concrete Implementor 2
final class HtmlFormatter implements ReportFormatter {
@Override
public String format(String title, List<String> lines) {
StringBuilder output = new StringBuilder();
output.append("<h1>").append(escape(title)).append("</h1>\n<ul>");
for (String line : lines) {
output.append("\n <li>").append(escape(line)).append("</li>");
}
return output.append("\n</ul>").toString();
}
private static String escape(String value) {
return value.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
}
// Abstraction: stores the bridge to the formatting implementation.
abstract class Report {
protected final ReportFormatter formatter;
protected final List<String> entries;
protected Report(ReportFormatter formatter, List<String> entries) {
this.formatter = Objects.requireNonNull(formatter, "formatter");
this.entries = List.copyOf(entries);
}
public abstract String generate();
}
// Refined Abstraction 1: decides what the summary contains.
final class SummaryReport extends Report {
SummaryReport(ReportFormatter formatter, List<String> entries) {
super(formatter, entries);
}
@Override
public String generate() {
return formatter.format(
"Summary report",
List.of("Total items: " + entries.size()));
}
}
// Refined Abstraction 2: decides what the detailed report contains.
final class DetailedReport extends Report {
DetailedReport(ReportFormatter formatter, List<String> entries) {
super(formatter, entries);
}
@Override
public String generate() {
return formatter.format("Detailed report", entries);
}
}
ตัวอย่างเก็บหลายคลาสไว้ในไฟล์เดียวเพื่อให้ทดลองได้สะดวก โดยมี BridgeDemo เป็น public class เพียงคลาสเดียว ในโปรเจกต์จริงสามารถแยกแต่ละคลาสออกเป็นไฟล์ของตนเอง และกำหนด package กับ visibility ให้เหมาะสม
#อธิบายจุดสำคัญของโค้ด
สัญญาของฝั่ง implementation
interface ReportFormatter {
String format(String title, List<String> lines);
}
ReportFormatter รับข้อมูลกลาง ได้แก่ชื่อรายงานและรายการข้อความ ตัวจัดรูปแบบไม่ต้องรู้ว่าข้อมูลมาจากรายงานสรุปหรือรายงานละเอียด Java interface ช่วยกำหนดสัญญาให้คลาสหลายตัวนำไป implement และให้ client เรียกผ่านชนิด interface ได้ [2]
จุดเชื่อมระหว่างสองฝั่ง
protected final ReportFormatter formatter;
ฝั่ง Report อ้างถึง interface และรับตัวจัดรูปแบบผ่าน constructor การส่ง dependency เข้ามาลักษณะนี้เรียกว่า constructor injection และไม่จำเป็นต้องใช้ framework
การแยกเนื้อหาออกจากรูปแบบ
return formatter.format(
"Summary report",
List.of("Total items: " + entries.size()));
SummaryReport คำนวณเนื้อหาที่จะรายงาน แล้วมอบหมายการจัดรูปแบบให้ formatter หากเปลี่ยนจาก PlainTextFormatter เป็น HtmlFormatter ก็ไม่ต้องเขียนการนับจำนวนรายการใหม่
การเก็บข้อมูลรายการ
List.copyOf(entries) สร้าง snapshot ของรายการอินพุตที่แก้ไขผ่าน list นั้นไม่ได้ และไม่ยอมรับ null list หรือ null element วิธีนี้เพียงพอกับข้อมูล String ในตัวอย่าง ไม่ใช่การ deep copy ออบเจ็กต์ทั่วไปที่อยู่ภายใน list [4]
#7. คอมไพล์และรัน
ตรวจสอบว่ามี JDK และตั้งค่า PATH แล้ว
java -version
javac -version
เข้าไปที่โฟลเดอร์ที่มี BridgeDemo.java แล้วรัน
javac -encoding UTF-8 BridgeDemo.java
java BridgeDemo
ผลลัพธ์ที่ได้
== Summary / Text ==
Summary report
- Total items: 2
== Summary / HTML ==
<h1>Summary report</h1>
<ul>
<li>Total items: 2</li>
</ul>
== Detailed / Text ==
Detailed report
- Build passed
- Tests passed
== Detailed / HTML ==
<h1>Detailed report</h1>
<ul>
<li>Build passed</li>
<li>Tests passed</li>
</ul>
ผลลัพธ์ HTML ในตัวอย่างเป็น HTML fragment ที่พิมพ์ออกทาง console ยังไม่ได้เปิดเว็บเบราว์เซอร์หรือเขียนไฟล์ HTML
HtmlFormatter แปลงอักขระพิเศษก่อนใส่ลงในเนื้อหาของ <h1> และ <li> เช่น < เป็น < และ & เป็น & ฟังก์ชันนี้ออกแบบสำหรับบริบทข้อความ HTML ของตัวอย่าง การนำข้อมูลไปใส่ใน URL, JavaScript หรือบริบทอื่นต้องใช้การจัดการที่เหมาะกับบริบทนั้น
#8. การทำงานของโปรแกรม
- Client สร้าง
PlainTextFormatterและHtmlFormatter - Client เลือกชนิดรายงาน และส่ง formatter ที่ต้องการผ่าน constructor
- Client เรียก
generate()ผ่านตัวแปรชนิดReport - รายงานสร้างเนื้อหาตามหน้าที่ของตนเอง
- รายงานเรียก
formatter.format(...)เพื่อสร้างผลลัพธ์ปลายทาง
ทั้งสี่คู่ทำงานได้จากคลาสรายงานสองชนิดและตัวจัดรูปแบบสองชนิด โดยไม่มี SummaryHtmlReport หรือ DetailedTextReport
#9. ขยายระบบ: เพิ่ม Markdown
เพิ่มคลาสต่อไปนี้ที่ท้ายไฟล์ BridgeDemo.java โดยอยู่นอกคลาสอื่น
final class MarkdownFormatter implements ReportFormatter {
@Override
public String format(String title, List<String> lines) {
StringBuilder output = new StringBuilder("# ").append(title);
for (String line : lines) {
output.append("\n- ").append(line);
}
return output.toString();
}
}
จากนั้นเพิ่มโค้ดต่อไปนี้ภายใน main()
Report markdownReport = new DetailedReport(
new MarkdownFormatter(), entries);
System.out.println(markdownReport.generate());
จะได้ผลลัพธ์
# Detailed report
- Build passed
- Tests passed
MarkdownFormatter ใช้กับ SummaryReport ได้เช่นกัน โดยไม่ต้องแก้คลาสรายงานทั้งสอง ตัว formatter แบบง่ายนี้ถือว่าข้อความตัวอย่างเป็นข้อมูลที่เชื่อถือได้และไม่มี Markdown พิเศษ หากต้องการให้ข้อมูลภายนอกแสดงเป็นข้อความล้วน ต้องจัดการ syntax และ HTML ตามข้อกำหนดของตัว renderer ที่ใช้งาน
จุดที่ยังเปลี่ยนคือ client หรือจุดประกอบออบเจ็กต์ เพราะระบบต้องระบุว่าจะใช้ formatter ตัวใหม่ คลาสเดิมไม่จำเป็นต้องรู้จัก implementation ใหม่ แต่การเพิ่มตัวเลือกไม่ได้ทำให้โค้ดทุกส่วนของโปรเจกต์หยุดเปลี่ยนแปลง
#10. ขยายอีกฝั่ง: เพิ่มรายงานรายการแรก
เพิ่มคลาสต่อไปนี้ที่ท้ายไฟล์
final class FirstItemReport extends Report {
FirstItemReport(ReportFormatter formatter, List<String> entries) {
super(formatter, entries);
}
@Override
public String generate() {
List<String> lines = entries.isEmpty()
? List.of("No items")
: List.of(entries.get(0));
return formatter.format("First item report", lines);
}
}
เรียกใช้งานจาก main()
Report firstItem = new FirstItemReport(new HtmlFormatter(), entries);
System.out.println(firstItem.generate());
รายงานชนิดใหม่นี้ใช้ formatter เดิมได้ทุกตัว เพราะยังส่งข้อมูลตามสัญญา format(String, List<String>) ที่ตกลงกันไว้
ขอบเขตของความอิสระ: หากเพิ่มความต้องการใหม่ เช่น ตารางหลายคอลัมน์ รูปภาพ หรือแผนภูมิ สัญญาปัจจุบันอาจไม่เพียงพอและอาจต้องเปลี่ยนทั้งสองฝั่ง Bridge จึงต้องอาศัยสัญญากลางที่เหมาะสม ไม่ได้รับประกันว่าทุกการเปลี่ยนแปลงจะเป็นอิสระตลอดไป
#11. เปรียบเทียบ Bridge, Adapter และ Strategy
| ประเด็น | Bridge | Adapter | Strategy |
|---|---|---|---|
| ความตั้งใจหลัก | แยก abstraction และ implementation ให้ขยายได้อิสระ | ทำให้อินเทอร์เฟซที่มีอยู่ใช้งานร่วมกันได้ | แยกอัลกอริทึมหรือวิธีทำงานที่ทดแทนกันได้ |
| ปัญหาที่เน้น | สองแกนของความเปลี่ยนแปลงและคลาสคู่ผสม | ความเข้ากันไม่ได้ของสัญญาเดิม | การเลือกวิธีทำงานหนึ่งจากหลายวิธี |
| ตัวอย่าง | ชนิดรายงาน × รูปแบบการแสดงผล | ห่อไลบรารีส่งอีเมลเดิมให้ตรงกับ interface ใหม่ | เลือกวิธีเรียงข้อมูลหรือคำนวณส่วนลด |
| ลักษณะที่อาจพบในโค้ด | ออบเจ็กต์ระดับสูงถือ reference ของ implementation | ออบเจ็กต์ห่อระบบเดิมและแปลงการเรียก | Context ถือ reference ของ strategy |
Bridge และ Strategy อาจมีโครงสร้างโค้ดใกล้เคียงกัน การเห็น constructor รับ interface เพียงอย่างเดียวจึงยังไม่พอจะตัดสินชื่อแพตเทิร์น ในตัวอย่างนี้มองเป็น Bridge เพราะออกแบบให้ ตระกูลรายงาน และ ตระกูลตัวจัดรูปแบบ ขยายแยกจากกัน ส่วนเมื่อมองเฉพาะการเลือก formatter ก็อาจอธิบายในมุม Strategy ได้เช่นกัน ความตั้งใจในการออกแบบเป็นสิ่งที่ช่วยแยกแพตเทิร์น [1]
#12. ควรใช้เมื่อใด
- มีความเปลี่ยนแปลงอย่างน้อยสองด้านที่แยกจากกันได้ และต้องนำมาจับคู่กันหลายแบบ
- เริ่มมีชื่อคลาสที่เกิดจากการผสมคุณสมบัติ เช่น
SummaryHtmlReportและDetailedHtmlReport - ต้องการเพิ่ม implementation โดยลดผลกระทบต่อโค้ดระดับสูง
- ต้องการทดสอบ logic ระดับรายงานแยกจากการจัดรูปแบบปลายทาง
หากระบบมีรายงานเพียงชนิดเดียว รูปแบบเดียว และยังไม่มีเหตุผลรองรับการขยาย การแยก hierarchy สองฝั่งอาจเพิ่มภาระการอ่านโค้ด ควรเริ่มจากโครงสร้างที่พอดีกับความต้องการจริง
#13. ประโยชน์และสิ่งที่ต้องแลก
| ประโยชน์ | สิ่งที่ต้องพิจารณา |
|---|---|
| ลดการสร้างคลาสเฉพาะทุกคู่ | มี interface และชั้นการมอบหมายงานเพิ่มขึ้น |
| แยกเหตุผลที่แต่ละส่วนต้องเปลี่ยน | ต้องออกแบบข้อมูลกลางและสัญญาให้เหมาะสม |
| ทดสอบเนื้อหารายงานแยกจากการจัดรูปแบบได้ | ยังต้องทดสอบคู่สำคัญเมื่อนำสองฝั่งมาทำงานร่วมกัน |
| เพิ่ม implementation ใหม่ได้โดยไม่แก้รายงานเดิม | Client หรือ configuration ยังต้องเลือก implementation |
#14. ข้อผิดพลาดที่พบบ่อย
ใช้ instanceof เลือกรูปแบบในรายงาน — ถ้า SummaryReport ต้องตรวจว่า formatter เป็น HtmlFormatter หรือไม่ แสดงว่า logic ระดับรายงานกลับไปผูกกับ implementation เฉพาะอีกครั้ง
สร้าง interface แล้วคิดว่าเป็น Bridge ทันที — interface เป็นกลไกของภาษา ส่วน Bridge เป็นการจัดวางความรับผิดชอบเพื่อรองรับสองแกนที่ขยายได้อิสระ
กล่าวว่า Bridge ห้าม inheritance — ตัวอย่างยังใช้ inheritance ภายในตระกูลรายงาน แต่ใช้การประกอบออบเจ็กต์เพื่อเชื่อมข้ามสองตระกูล
เข้าใจว่าต้องเปลี่ยน implementation บนออบเจ็กต์เดิมขณะรันได้เสมอ — ตัวอย่างใช้ final field และเลือก formatter ตอนสร้างรายงาน หากต้องการเปลี่ยนรูปแบบ ให้สร้างรายงานใหม่ด้วย formatter ตัวอื่น Bridge ไม่ได้บังคับให้มี setter
ให้ formatter ตัดสินใจเนื้อหาธุรกิจ — การนับจำนวนรายการควรอยู่ฝั่งรายงาน ส่วน formatter ควรรับเนื้อหาที่พร้อมแสดงแล้ว
#15. กิจกรรมทดลองและเกณฑ์ตรวจสอบ
- รันตัวอย่างเดิม และตรวจว่ามีผลลัพธ์ครบทั้งสี่คู่
- เพิ่ม
MarkdownFormatterและใช้กับรายงานทั้งสองชนิด โดยไม่แก้โค้ดภายในรายงานเดิม - เพิ่ม
FirstItemReportและใช้กับ formatter เดิมทั้งสามตัว - เปลี่ยนข้อมูลเป็น
List.of()แล้วตรวจว่ารายงานสรุปแสดงTotal items: 0และรายงานรายการแรกแสดงNo items - เปลี่ยนข้อมูลเป็น
List.of("R&D <core>")แล้วตรวจว่า HTML ของรายงานละเอียดมีR&D <core> - อธิบายว่าหากเพิ่มเนื้อหาเป็นตารางหลายคอลัมน์ จะต้องประเมินสัญญา
ReportFormatterใหม่อย่างไร
เกณฑ์สำเร็จ: เพิ่มตัวเลือกทั้งสองฝั่งได้โดยไม่สร้างคลาสใหม่สำหรับทุกคู่ และชี้ได้ชัดเจนว่าการเลือกเนื้อหาอยู่ที่รายงาน ส่วนการจัดรูปแบบอยู่ที่ formatter
#16. ข้อคิดสำหรับการนำไปใช้
จุดที่ควรมองหาในระบบจริงคือ “อะไรสองอย่างที่ต้องเปลี่ยนแยกกัน แต่ถูกเขียนรวมไว้ในคลาสเดียว” หากแบ่งหน้าที่และกำหนดสัญญากลางได้ดี Bridge จะช่วยให้แต่ละฝั่งเติบโตได้โดยไม่เพิ่มคลาสตามทุกคู่ของคุณสมบัติ
ในตัวอย่างนี้ ความสัมพันธ์หลักคือ Report มี ReportFormatter และมอบหมายการแสดงผลผ่านสัญญาเดียวกัน ทำให้ SummaryReport และ DetailedReport เลือกใช้งาน Plain Text, HTML หรือ Markdown ได้จากการประกอบออบเจ็กต์
#เอกสารอ้างอิง
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. หัวข้อ Bridge, Adapter และ Strategy — แหล่งอ้างอิงหลักด้านความตั้งใจและโครงสร้างของแพตเทิร์น
- Dev.java — Interfaces — การกำหนดและใช้งาน Java interface
- Dev.java — Inheritance — พื้นฐาน inheritance และ polymorphism
- Java SE 17 API — List.copyOf — คุณสมบัติของ list ที่สร้างจาก
copyOf