- 1. Mediator Pattern คืออะไร
- 2. ปัญหาที่ต้องการแก้
- 3. บทบาทใน Pattern
- 4. ลำดับการทำงาน
- 5. Workshop: ฟอร์มลงทะเบียนด้วย Java
- 6. อ่านโค้ดทีละส่วน
- 7. ตรวจสอบพฤติกรรมของ Workshop
- 8. ข้อดีและข้อจำกัด
- 9. เหมาะกับสถานการณ์ใด
- 10. ต่างจาก Pattern อื่นอย่างไร
- 11. ตัวอย่างแนวคิดจาก Java Swing
- 12. แบบฝึกหัดต่อยอด
- 13. ทบทวนแนวคิด
- เอกสารอ้างอิง
ใช้ตัวกลางประสานงานระหว่างออบเจ็กต์ เพื่อให้แต่ละคอมโพเนนต์ไม่ต้องรู้จักรายละเอียดของคอมโพเนนต์อื่น
ประเภท: Behavioral Design Pattern
ตัวอย่างหลัก: ฟอร์มลงทะเบียน
พื้นฐานที่ควรมี: Java class, interface, inheritance และ enum
#1. Mediator Pattern คืออะไร
Mediator Pattern เป็นรูปแบบการออกแบบที่ย้ายกติกาการทำงานร่วมกันของออบเจ็กต์หลายตัวมาไว้ในออบเจ็กต์ตัวกลางที่เรียกว่า Mediator เมื่อคอมโพเนนต์เกิดเหตุการณ์ เช่น ผู้ใช้กรอกข้อมูลหรือกดปุ่ม คอมโพเนนต์จะแจ้งเหตุการณ์ให้ตัวกลางตัดสินใจว่าจะให้ส่วนใดทำอะไรต่อ
คอมโพเนนต์ยังมีสถานะและพฤติกรรมของตัวเอง แต่ไม่ต้องอ้างอิงคอมโพเนนต์อื่นเพื่อประสานงานกันโดยตรง ส่วน Mediator จะรู้จักคอมโพเนนต์ที่เกี่ยวข้องและควบคุมปฏิสัมพันธ์ภายในขอบเขตที่รับผิดชอบ
ลองนึกถึงหอบังคับการบิน เครื่องบินแต่ละลำรายงานข้อมูลต่อหอบังคับการบิน แล้วรับคำแนะนำกลับมา หอบังคับการบินทำหน้าที่ประสานงานตามสถานการณ์ ภาพเปรียบเทียบนี้ช่วยให้เห็นบทบาทของตัวกลาง ส่วนตัวอย่าง Java ต่อไปนี้จะใช้ฟอร์มที่เรียบง่ายกว่า
#2. ปัญหาที่ต้องการแก้
สมมติว่าฟอร์มลงทะเบียนมี 3 ส่วน ได้แก่
- ช่องกรอกอีเมล
TextField - ช่องยอมรับเงื่อนไข
CheckBox - ปุ่มลงทะเบียน
Button
ปุ่มควรเปิดใช้งานเมื่ออีเมลผ่านกติกาตรวจสอบและผู้ใช้ยอมรับเงื่อนไขแล้ว หากช่องอีเมลต้องอ่านสถานะของ CheckBox และปรับ Button เอง ขณะเดียวกัน CheckBox ก็ต้องอ่านอีเมลและปรับ Button เช่นกัน กติกาเดียวกันจะกระจายอยู่หลายจุด
เมื่อเพิ่มเงื่อนไขใหม่ เช่น ต้องเลือกหลักสูตรก่อนลงทะเบียน การแก้ไขอาจกระทบหลายคลาส และคอมโพเนนต์ที่ควรใช้ซ้ำได้ก็เริ่มผูกติดกับฟอร์มนี้
| ประเด็น | เมื่อคอมโพเนนต์ประสานงานกันเอง | เมื่อใช้ Mediator |
|---|---|---|
| ผู้ตัดสินใจเปิดปุ่ม | กระจายอยู่ในช่องอีเมลและ CheckBox | RegistrationDialog |
| สิ่งที่ TextField ต้องรู้ | คอมโพเนนต์อื่นและกติกาของฟอร์ม | วิธีแจ้งเหตุการณ์ผ่าน Mediator |
| การเปลี่ยนกติกาประสานงาน | อาจแก้หลายคอมโพเนนต์ | แก้ในตัวกลางหรือ policy ที่ตัวกลางเรียกใช้ |
| ภาระที่เพิ่มขึ้น | ความสัมพันธ์ระหว่างคอมโพเนนต์ | ต้องดูแลตัวกลางและสัญญาของเหตุการณ์ |
Mediator ไม่ได้ทำให้ทุก dependency หายไป แต่เปลี่ยนจากการพึ่งพาระหว่างคอมโพเนนต์ไปเป็นการพึ่งพาสัญญาของตัวกลาง
#3. บทบาทใน Pattern
| บทบาท | หน้าที่ | คลาสในตัวอย่าง |
|---|---|---|
| Mediator | กำหนดวิธีรับเหตุการณ์จากคอมโพเนนต์ | Mediator interface |
| Concrete Mediator | รวมกติกาการประสานงานและสั่งคอมโพเนนต์ | RegistrationDialog |
| Colleague / Component | เก็บสถานะและพฤติกรรมพื้นฐาน พร้อมอ้างอิงตัวกลาง | Component |
| Concrete Colleagues | สร้างเหตุการณ์และทำงานเฉพาะส่วน | TextField, CheckBox, Button |
ชื่อคลาสไม่ใช่ตัวตัดสินว่าเป็น Mediator หรือไม่ ให้พิจารณาว่ากติกาการทำงานร่วมกันอยู่ที่ใด และคอมโพเนนต์ยังต้องเรียกหากันโดยตรงหรือไม่
#4. ลำดับการทำงาน
แผนภาพนี้แสดงเหตุการณ์หลัก เมื่อเริ่มจากยังไม่ยอมรับเงื่อนไข แล้วกรอกอีเมลที่ผ่านกติกา จากนั้นยอมรับเงื่อนไขและกดปุ่ม
sequenceDiagram
participant E as TextField
participant M as RegistrationDialog
participant C as CheckBox
participant B as Button
E->>M: onEvent(CHANGED)
Note over M: อีเมลผ่าน แต่ยังไม่ยอมรับเงื่อนไข
M->>B: setEnabled(false)
C->>M: onEvent(CHANGED)
Note over M: อีเมลผ่านและยอมรับเงื่อนไขแล้ว
M->>B: setEnabled(true)
B->>M: onEvent(CLICKED)
Note over M: ตรวจเงื่อนไขอีกครั้งและจำลองการลงทะเบียน
TextField และ CheckBox แจ้งเหตุการณ์ แต่ไม่ได้สั่ง Button เอง การสั่งเปิดหรือปิดปุ่มเป็นความรับผิดชอบของ RegistrationDialog
#5. Workshop: ฟอร์มลงทะเบียนด้วย Java
#เป้าหมาย
สร้างตัวอย่างที่รันจาก Terminal ได้ โดยจำลองคอมโพเนนต์ของฟอร์ม ไม่ต้องเปิดหน้าต่าง GUI เพื่อให้เห็นโครงสร้างของ Pattern ชัดเจน
กติกาคือ เปิดปุ่ม = อีเมลผ่านกติกาตัวอย่าง AND ยอมรับเงื่อนไข
| สถานะอีเมล | ยอมรับเงื่อนไข | ปุ่มลงทะเบียน |
|---|---|---|
| ว่าง | ไม่ยอมรับ | ปิดใช้งาน |
| ไม่ผ่านกติกา | ยอมรับ | ปิดใช้งาน |
| ผ่านกติกา | ไม่ยอมรับ | ปิดใช้งาน |
| ผ่านกติกา | ยอมรับ | เปิดใช้งาน |
การตรวจอีเมลในตัวอย่างใช้ regular expression อย่างง่ายเพื่อประกอบการสอน ไม่ครอบคลุมรูปแบบอีเมลทั้งหมดและไม่ยืนยันว่าผู้ใช้เป็นเจ้าของอีเมลนั้น
#ขั้นตอนที่ 1: สร้างไฟล์
สร้างไฟล์ชื่อ MediatorDemo.java แล้วคัดลอกโค้ดทั้งหมดต่อไปนี้ลงไป คลาสสนับสนุนถูกประกาศเป็น nested classes เพื่อให้ตัวอย่างอยู่ในไฟล์เดียว ในโครงการจริงสามารถแยกไฟล์ตามความเหมาะสม
import java.util.Objects;
public class MediatorDemo {
public static void main(String[] args) {
RegistrationDialog dialog = new RegistrationDialog();
showState("1) Initial", dialog);
dialog.registerButton().click();
dialog.emailField().setText("student");
dialog.termsCheckbox().setChecked(true);
showState("2) Invalid email + accepted terms", dialog);
dialog.registerButton().click();
dialog.emailField().setText("student@npru.ac.th");
showState("3) Valid email + accepted terms", dialog);
dialog.registerButton().click();
dialog.termsCheckbox().setChecked(false);
showState("4) Consent withdrawn", dialog);
dialog.registerButton().click();
}
private static void showState(String label, RegistrationDialog dialog) {
System.out.println(label + " -> enabled="
+ dialog.registerButton().isEnabled());
}
enum UiEvent {
CHANGED, CLICKED
}
interface Mediator {
void onEvent(Component sender, UiEvent event);
}
abstract static class Component {
protected final Mediator mediator;
Component(Mediator mediator) {
this.mediator = Objects.requireNonNull(mediator);
}
}
static final class TextField extends Component {
private String text = "";
TextField(Mediator mediator) {
super(mediator);
}
String getText() {
return text;
}
void setText(String text) {
this.text = Objects.requireNonNull(text).strip();
mediator.onEvent(this, UiEvent.CHANGED);
}
}
static final class CheckBox extends Component {
private boolean checked;
CheckBox(Mediator mediator) {
super(mediator);
}
boolean isChecked() {
return checked;
}
void setChecked(boolean checked) {
this.checked = checked;
mediator.onEvent(this, UiEvent.CHANGED);
}
}
static final class Button extends Component {
private boolean enabled;
Button(Mediator mediator) {
super(mediator);
}
boolean isEnabled() {
return enabled;
}
void setEnabled(boolean enabled) {
this.enabled = enabled;
}
void click() {
if (!enabled) {
System.out.println("Registration disabled.");
return;
}
mediator.onEvent(this, UiEvent.CLICKED);
}
}
static final class RegistrationDialog implements Mediator {
private final TextField email;
private final CheckBox terms;
private final Button register;
RegistrationDialog() {
email = new TextField(this);
terms = new CheckBox(this);
register = new Button(this);
refreshButton();
}
TextField emailField() {
return email;
}
CheckBox termsCheckbox() {
return terms;
}
Button registerButton() {
return register;
}
@Override
public void onEvent(Component sender, UiEvent event) {
if (event == UiEvent.CHANGED
&& (sender == email || sender == terms)) {
refreshButton();
} else if (event == UiEvent.CLICKED && sender == register) {
if (canRegister()) {
System.out.println("Registered: " + email.getText());
}
}
}
private void refreshButton() {
register.setEnabled(canRegister());
}
private boolean canRegister() {
// A small teaching rule, not complete email validation.
boolean emailLooksValid = email.getText()
.matches("[^\\s@]+@[^\\s@]+\\.[^\\s@]+");
return emailLooksValid && terms.isChecked();
}
}
}
#ขั้นตอนที่ 2: รันโปรแกรม
ตรวจสอบว่ามี JDK 17 ขึ้นไป
java -version
javac -version
คอมไพล์และรันจากโฟลเดอร์ที่เก็บไฟล์
javac -encoding UTF-8 MediatorDemo.java
java MediatorDemo
หรือรันไฟล์ source โดยตรงด้วย JDK ที่รองรับ source-file mode
java MediatorDemo.java
#ขั้นตอนที่ 3: ตรวจผลลัพธ์
1) Initial -> enabled=false
Registration disabled.
2) Invalid email + accepted terms -> enabled=false
Registration disabled.
3) Valid email + accepted terms -> enabled=true
Registered: student@npru.ac.th
4) Consent withdrawn -> enabled=false
Registration disabled.
ข้อความ Registered เป็นผลลัพธ์จำลองของ Workshop หากนำไปทำระบบจริงให้เรียก application service สำหรับบันทึกข้อมูลและตรวจสอบกฎฝั่งเซิร์ฟเวอร์
#6. อ่านโค้ดทีละส่วน
#6.1 Mediator interface เป็นสัญญาการสื่อสาร
interface Mediator {
void onEvent(Component sender, UiEvent event);
}
sender ระบุคอมโพเนนต์ที่ส่งเหตุการณ์ ส่วน event ระบุชนิดเหตุการณ์ คอมโพเนนต์รู้จัก interface นี้ โดยไม่ต้องทราบว่าตัวกลางใช้กติกาใดหรือจะสั่งคอมโพเนนต์ตัวไหนต่อ
ตัวอย่างตั้งชื่อเมธอดว่า onEvent เพื่อสื่อว่าเป็น callback ของเหตุการณ์ และใช้ enum เพื่อลดการพิมพ์ชื่อเหตุการณ์ผิดแบบที่อาจเกิดเมื่อใช้ String
#6.2 คอมโพเนนต์เก็บสถานะแล้วแจ้งตัวกลาง
void setChecked(boolean checked) {
this.checked = checked;
mediator.onEvent(this, UiEvent.CHANGED);
}
CheckBox ไม่ได้อ้างอิง TextField หรือ Button เลย จึงไม่ต้องทราบว่าการเปลี่ยนค่าของตัวเองจะมีผลต่อคอมโพเนนต์ใดบ้าง
TextField ใช้ Objects.requireNonNull ป้องกันค่า null และใช้ String.strip() ตัด whitespace ที่หัวและท้ายข้อความ API เหล่านี้มีคำอธิบายในเอกสาร Java ทางการ [2][3]
#6.3 Concrete Mediator ตัดสินใจกติกาการประสานงาน
private void refreshButton() {
register.setEnabled(canRegister());
}
เมื่อช่องอีเมลหรือ CheckBox เปลี่ยนค่า ตัวกลางจะอ่านสถานะปัจจุบันแล้วคำนวณว่าควรเปิดปุ่มหรือไม่ วิธีนี้ทำให้ทั้งสองเหตุการณ์ใช้กติกาเดียวกัน
onEvent ตรวจ sender == email และ sender == terms เพื่อยอมรับเหตุการณ์จากคอมโพเนนต์ของฟอร์มนี้โดยเฉพาะ
#6.4 การอัปเดตจากตัวกลางไม่ควรสร้างวงจรเหตุการณ์โดยไม่ตั้งใจ
Button.setEnabled() เปลี่ยนสถานะอย่างเดียวและไม่แจ้ง CHANGED กลับไป จึงไม่เกิดวงจรที่ตัวกลางสั่งปุ่มแล้วปุ่มแจ้งกลับให้ตัวกลางสั่งซ้ำ
ใน GUI จริง API บางตัวอาจปล่อย event เมื่อเปลี่ยนค่าด้วยโปรแกรม ควรแยกการเปลี่ยนจากผู้ใช้กับการอัปเดตภายใน หรือใช้กลไกป้องกันการเข้าซ้ำตามความเหมาะสม
#6.5 ตรวจเงื่อนไขก่อนดำเนินการ
Button ป้องกันการคลิกเมื่อปิดใช้งาน ส่วนตัวกลางตรวจ canRegister() อีกครั้งก่อนทำงาน วิธีนี้ช่วยให้เงื่อนไขการดำเนินการชัดเจน แต่สำหรับระบบเว็บ การปิดปุ่มฝั่งหน้าจอไม่สามารถทดแทน validation ฝั่งเซิร์ฟเวอร์ได้
#7. ตรวจสอบพฤติกรรมของ Workshop
โค้ดฉบับนี้ผ่านการคอมไพล์และรันด้วย OpenJDK 17.0.20 และตรวจผลลัพธ์ตาม 4 สถานการณ์ใน main() ได้แก่
- เริ่มต้นโดยยังไม่มีข้อมูล ปุ่มต้องปิดใช้งาน
- อีเมล
studentไม่ผ่านกติกา แม้ยอมรับเงื่อนไขแล้วปุ่มยังต้องปิดใช้งาน - เปลี่ยนเป็น
student@npru.ac.thพร้อมยอมรับเงื่อนไข ปุ่มเปิดใช้งานและแสดงผลลงทะเบียนได้ - ถอนการยอมรับเงื่อนไข ปุ่มกลับไปปิดใช้งาน
เมื่อนำตัวอย่างไปต่อยอด ควรเพิ่มการตรวจสอบกรณีล้างอีเมลหลังเปิดปุ่ม การยอมรับเงื่อนไขก่อนกรอกอีเมล และการสลับสถานะซ้ำ เพื่อยืนยันว่าตัวกลางคำนวณจากสถานะปัจจุบันเสมอ
#8. ข้อดีและข้อจำกัด
| ประเด็น | ผลต่อการออกแบบ |
|---|---|
| ลด coupling ระหว่างคอมโพเนนต์ | TextField ไม่ต้องอ้างอิง CheckBox หรือ Button |
| รวมกติกาการประสานงาน | ติดตามสาเหตุของการเปิดหรือปิดปุ่มได้จากตัวกลาง |
| ใช้คอมโพเนนต์ซ้ำ | เปลี่ยนตัวกลางเพื่อใช้กติกาการทำงานร่วมกันแบบอื่นได้ |
| เพิ่มโครงสร้าง | มี interface, event และคลาสตัวกลางเพิ่มขึ้น |
| เสี่ยงต่อ God Object | ตัวกลางอาจโตเกินไปหากรวมทุก use case และทุกกฎธุรกิจไว้ด้วยกัน |
เมื่อระบบเริ่มซับซ้อน ให้แยก Mediator ตามขอบเขตงาน เช่น ฟอร์มลงทะเบียนและฟอร์มแก้ไขโปรไฟล์ ส่วนการตรวจคุณสมบัติหรือบันทึกข้อมูลควรแยกเป็น policy หรือ service ที่ทดสอบได้อิสระ แล้วให้ Mediator เรียกใช้
Mediator ไม่ได้ให้ความสามารถด้าน asynchronous execution, transaction หรือ thread safety โดยอัตโนมัติ ตัวอย่างนี้ทำงานแบบ synchronous บนเธรดเดียว
#9. เหมาะกับสถานการณ์ใด
ใช้เมื่อมีคอมโพเนนต์หลายตัวที่มีปฏิสัมพันธ์ซับซ้อน เช่น ฟอร์มที่หลายช่องควบคุมสถานะกัน หน้าจอตั้งค่าที่เปิดหรือปิดตัวเลือกตามเงื่อนไข หรือกลุ่มออบเจ็กต์ที่ต้องทำงานตามกติกาประสานงานร่วมกัน
หากมีเพียงออบเจ็กต์หนึ่งเรียก service อีกตัวโดยตรง และความสัมพันธ์ยังเข้าใจง่าย การเพิ่ม Mediator อาจเพิ่มโครงสร้างโดยยังไม่เกิดประโยชน์ชัดเจน
#10. ต่างจาก Pattern อื่นอย่างไร
| Pattern | จุดประสงค์หลัก | คำถามที่ช่วยแยกบทบาท |
|---|---|---|
| Mediator | รวมกติกาปฏิสัมพันธ์ระหว่างคอมโพเนนต์ | เมื่อ A เปลี่ยน B และ C ควรทำอะไรต่อ |
| Observer | แจ้งผู้ติดตามเมื่อเกิดเหตุการณ์หรือสถานะเปลี่ยน | ใครสมัครรับการเปลี่ยนแปลงนี้ไว้บ้าง |
| Facade | ทำทางเข้าใช้งาน subsystem ให้เรียบง่าย | ผู้เรียกจะใช้งานระบบย่อยผ่าน API ที่ง่ายขึ้นได้อย่างไร |
| Command | ห่อคำขอเป็นออบเจ็กต์ | จะเก็บ จัดคิว หรือส่งต่อคำสั่งอย่างไร |
Mediator สามารถใช้ Observer เป็นกลไกรับเหตุการณ์จากคอมโพเนนต์ได้ ทั้งสอง Pattern จึงใช้ร่วมกันได้ จุดที่ต้องดูคือใครเป็นผู้ถือกติกาการทำงานร่วมกัน การส่ง event ผ่านตัวกลางอย่างเดียวไม่ได้ทำให้ทุกระบบเป็น Mediator เสมอไป
#11. ตัวอย่างแนวคิดจาก Java Swing
เอกสาร Java อธิบายว่า ButtonGroup ใช้จัดกลุ่มปุ่มที่มีสถานะเลือกได้ เมื่อเลือกปุ่มหนึ่ง ปุ่มอื่นในกลุ่มจะถูกยกเลิกการเลือก [4]
import javax.swing.ButtonGroup;
import javax.swing.JRadioButton;
JRadioButton online = new JRadioButton("Online");
JRadioButton onsite = new JRadioButton("Onsite");
ButtonGroup group = new ButtonGroup();
group.add(online);
group.add(onsite);
ในเชิงการออกแบบ สามารถมอง ButtonGroup เป็นตัวอย่างของการรวมกติกาการเลือกไว้ที่ตัวกลาง ปุ่มแต่ละตัวไม่ต้องไล่ยกเลิกการเลือกปุ่มอื่นด้วยตัวเอง นี่เป็นการตีความเพื่อเชื่อมกับบทเรียน โดยเอกสาร Java ที่อ้างอิงอธิบายพฤติกรรมของ API ไม่ได้ระบุชื่อ GoF Mediator โดยตรง
โค้ด Swing ส่วนนี้เป็นตัวอย่างการจัดกลุ่มปุ่ม ยังต้องเพิ่มปุ่มลงในหน้าต่างหากต้องการแสดง GUI และ ButtonGroup ไม่ได้ทำหน้าที่จัดวางองค์ประกอบบนหน้าจอ
#12. แบบฝึกหัดต่อยอด
#แบบฝึกหัดที่ 1: เพิ่มการเลือกหลักสูตร
เพิ่มคอมโพเนนต์ CourseSelector และกำหนดว่าปุ่มเปิดได้เมื่อเลือกหลักสูตรแล้ว พร้อมกับอีเมลผ่านกติกาและยอมรับเงื่อนไข คอมโพเนนต์เดิมควรยังคงแจ้งเหตุการณ์ผ่าน Mediator โดยไม่อ้างอิงตัวเลือกหลักสูตร
เกณฑ์ตรวจ: การล้างค่าหลักสูตรทำให้ปุ่มปิด และการเลือกหลักสูตรกลับทำให้ปุ่มเปิดเมื่อเงื่อนไขอื่นครบ
#แบบฝึกหัดที่ 2: แยกกฎออกจากตัวกลาง
สร้าง RegistrationPolicy สำหรับตรวจความพร้อมลงทะเบียน แล้วให้ RegistrationDialog รับผิดชอบการประสานงาน ส่วน policy รับผิดชอบการตัดสินตามข้อมูล
เกณฑ์ตรวจ: ทดสอบ policy ได้โดยไม่ต้องสร้าง Button หรือ TextField และพฤติกรรมของฟอร์มยังคงเดิม
#แบบฝึกหัดที่ 3: ใช้กับ GUI
เปลี่ยนจากคอมโพเนนต์จำลองเป็น Swing หรือ JavaFX โดยให้ callback ของ UI ส่งเหตุการณ์ไปยังตัวกลาง และระวัง event ที่เกิดจากการอัปเดตค่าด้วยโปรแกรม
เกณฑ์ตรวจ: การเปลี่ยนอีเมลและเงื่อนไขทำให้สถานะปุ่มเปลี่ยนตามกติกา และการอัปเดตจากตัวกลางไม่ทำให้เกิดวงจร event
#13. ทบทวนแนวคิด
หัวใจของ Mediator Pattern คือการมีเจ้าของกติกาการทำงานร่วมกันที่ชัดเจน คอมโพเนนต์ดูแลหน้าที่ของตนเองและแจ้งเหตุการณ์ ส่วนตัวกลางเป็นผู้เชื่อมสถานะกับการกระทำของคอมโพเนนต์อื่น
ในตัวอย่างนี้ TextField ดูแลข้อความ CheckBox ดูแลสถานะยอมรับเงื่อนไข Button ดูแลสถานะเปิดใช้งาน ส่วน RegistrationDialog ดูแลความสัมพันธ์ระหว่างทั้งสามส่วน
#เอกสารอ้างอิง
- Gamma, E., Helm, R., Johnson, R., & Vlissides, J. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995. ข้อมูลหนังสือจากสำนักพิมพ์
- Oracle. Objects — Java SE 21 API — การใช้งาน
requireNonNull. - Oracle. String — Java SE 21 API — การใช้งาน
stripและmatches. - Oracle. ButtonGroup — Java SE 21 API — กติกาการเลือกปุ่มภายในกลุ่ม.
โค้ด Workshop เป็นตัวอย่างที่จัดทำขึ้นสำหรับบทความนี้ โดยใช้แนวคิดจาก Mediator Pattern ไม่ได้คัดลอกโค้ดตัวอย่างจากหนังสือ