Màn hình thanh toán của một cửa hàng online thường có năm thành phần: ô nhập mã giảm giá, chọn hình thức giao hàng, chọn phương thức thanh toán, nhãn tổng tiền và nút "Thanh toán". Các thành phần này ràng buộc lẫn nhau: đổi sang giao nhanh thì tổng tiền đổi, đơn trên 20 triệu thì không được chọn COD, mã FREESHIP chỉ áp dụng cho giao tiêu chuẩn. Nếu mỗi thành phần giữ tham chiếu tới các thành phần còn lại, bạn có một mạng nhện phụ thuộc chéo. Mediator pattern giải quyết đúng chỗ này bằng cách đưa mọi luật phối hợp vào một đối tượng trung gian. Bài này cài đặt nó từ đầu, sau đó ghép với Memento, mẫu cho phép lưu và khôi phục trạng thái mà không phá encapsulation, để làm tính năng hoàn tác (undo) cho một đơn nháp.
Mediator pattern và Memento đều thuộc nhóm behavioral trong 23 mẫu của GoF. Nếu bạn đã đọc Iterator pattern trong Java hay State pattern và máy trạng thái cho đơn hàng, bài này tiếp tục cùng mạch: tách trách nhiệm về giao tiếp và về trạng thái ra khỏi các lớp nghiệp vụ. Toàn bộ code chạy với JDK 21 trên Apple M4, 16 GB RAM, không cần thư viện ngoài, và output trong bài là output thật của lần chạy.
Vấn đề mà Mediator pattern giải quyết: thành phần gọi chéo nhau
Giả sử bạn chưa dùng Mediator pattern mà viết trực tiếp. Khi người dùng đổi phương thức thanh toán, PaymentSelect phải gọi PayButton để khoá nút nếu đơn quá lớn, gọi TotalLabel để tính lại tiền. Khi đổi giao hàng, ShippingSelect phải gọi lại TotalLabel và PayButton, đồng thời hỏi CouponField xem có FREESHIP không. Với n thành phần, số liên kết có thể lên tới n(n-1), tức 20 liên kết cho năm thành phần. Hệ quả:
- Không thể tái sử dụng
ShippingSelectở màn hình khác (ví dụ trang giỏ hàng) vì nó kéo theoPayButtonvàCouponField. - Thêm một thành phần mới (ví dụ ô nhập điểm thưởng) buộc sửa nhiều lớp cũ.
- Luật nghiệp vụ như "COD không áp dụng cho đơn từ 20 triệu" bị rải ở nhiều nơi, rất khó tìm và dễ lệch nhau.
Cấu trúc Mediator pattern
Theo mô tả của refactoring.guru về Mediator pattern, Mediator pattern là một mẫu hành vi (behavioral) "lets you reduce chaotic dependencies between objects", tạm dịch là giúp giảm các phụ thuộc hỗn loạn giữa các đối tượng bằng cách để chúng chỉ giao tiếp qua một đối tượng trung gian. Cấu trúc gồm:
| Vai trò | Trong ví dụ thanh toán | Trách nhiệm |
|---|---|---|
| Mediator (interface) | CheckoutMediator | Khai báo phương thức notify(sender, event) |
| ConcreteMediator | CheckoutDialog | Biết toàn bộ thành phần, chứa luật phối hợp |
| Colleague (thành phần) | CouponField, PaymentSelect, ShippingSelect, PayButton, TotalLabel | Chỉ giữ tham chiếu tới interface Mediator và báo sự kiện cho nó |
Điểm mấu chốt của Mediator pattern: mối quan hệ nhiều-nhiều biến thành một-nhiều. Mỗi thành phần chỉ có một phụ thuộc duy nhất là interface Mediator. Mọi luật "cái này đổi thì cái kia phải đổi theo" nằm trong CheckoutDialog.
Cài đặt Mediator pattern từng bước
Đầu tiên là interface và lớp cơ sở cho thành phần. Thành phần không biết ai đang nghe, chỉ biết gọi mediator.notify:
interface CheckoutMediator {
void notify(Component sender, String event);
}
abstract static class Component {
protected CheckoutMediator mediator;
final String name;
Component(String name) { this.name = name; }
void setMediator(CheckoutMediator m) { this.mediator = m; }
}
static class PaymentSelect extends Component {
private String method = "COD";
PaymentSelect() { super("payment"); }
void choose(String m) { method = m; mediator.notify(this, "changed"); }
String method() { return method; }
}
static class PayButton extends Component {
private boolean enabled = true;
PayButton() { super("payButton"); }
void setEnabled(boolean e) { enabled = e; }
void click() {
if (!enabled) return;
mediator.notify(this, "clicked");
}
}Các lớp CouponField, ShippingSelect và TotalLabel viết tương tự. Sau đó là ConcreteMediator của Mediator pattern, nơi ba luật nghiệp vụ được gom lại:
static class CheckoutDialog implements CheckoutMediator {
final CouponField coupon = new CouponField();
final PaymentSelect payment = new PaymentSelect();
final ShippingSelect shipping = new ShippingSelect();
final PayButton pay = new PayButton();
final TotalLabel total = new TotalLabel();
private final long subtotal;
CheckoutDialog(long subtotal) {
this.subtotal = subtotal;
for (Component c : List.of(coupon, payment, shipping, pay, total)) c.setMediator(this);
refresh();
}
@Override
public void notify(Component sender, String event) {
if (sender == pay) {
log("Thanh toán " + total() + " đ bằng " + payment.method());
return;
}
refresh(); // bất kỳ ô nào đổi: tính lại tổng và trạng thái nút
}
long total() {
long ship = shipping.type().equals("EXPRESS") ? 45_000 : 25_000;
if (coupon.code().equals("FREESHIP") && shipping.type().equals("STANDARD")) ship = 0;
return subtotal + ship;
}
private void refresh() {
total.show(total());
boolean codTooBig = payment.method().equals("COD") && total() >= 20_000_000;
pay.setEnabled(!codTooBig);
}
}Chạy thử hai kịch bản: đơn 1,2 triệu và đơn 22 triệu. Với đơn thứ hai, COD phải bị chặn, đổi sang chuyển khoản thì mới thanh toán được (cd code/s6-19 && javac -d out Mediator.java && java -cp out Mediator):
== Đơn 1,2 triệu ==
TotalLabel = 1225000 đ
PayButton enabled=true
TotalLabel = 1200000 đ
PayButton enabled=true
TotalLabel = 1245000 đ
PayButton enabled=true
TotalLabel = 1200000 đ
PayButton enabled=true
Thanh toán 1200000 đ bằng COD
== Đơn 22 triệu, COD ==
TotalLabel = 22025000 đ
PayButton enabled=false
PayButton: bấm khi đang bị khoá, bỏ qua
TotalLabel = 22025000 đ
PayButton enabled=true
Thanh toán 22025000 đ bằng BANK_TRANSFER
Trace: [payment:changed, payButton:clicked]Đọc output theo thứ tự: dòng đầu là lần refresh() trong constructor (1.200.000 + 25.000 phí giao). Nhập FREESHIP làm phí giao về 0 (1.200.000), đổi sang EXPRESS thì mã không còn tác dụng (1.245.000), đổi lại STANDARD thì FREESHIP có hiệu lực trở lại. Với đơn 22 triệu, nút bị khoá ngay khi khởi tạo vì COD vượt ngưỡng, bấm bị bỏ qua, đổi sang chuyển khoản thì nút mở. Nhờ Mediator pattern, không thành phần nào gọi trực tiếp thành phần khác, nên có thể thêm LoyaltyPoints mà chỉ sửa CheckoutDialog.
Mediator pattern trong thực tế: JDK, Spring và các thư viện
Bạn gặp Mediator pattern hoặc các biến thể gần giống nó nhiều hơn mình tưởng, dù không phải lúc nào cũng mang tên đó:
- Spring
ApplicationEventPublisher: bean phát sự kiện, các@EventListenernhận. Bên phát không biết bên nghe là ai,ApplicationContextđóng vai trung gian. Đây là biến thể publish/subscribe của Mediator pattern, xem phần Standard and Custom Events của tài liệu Spring Framework. - Spring MVC
DispatcherServlet: tài liệu Spring mô tả nó là front controller, chọn handler, gọi view resolver; các thành phần đó không gọi nhau mà đi qua nó. Đây là điều phối theo kiểu Mediator, dù tài liệu không gọi bằng tên này (DispatcherServlet).
Các thư viện mediator như MediatR của .NET áp dụng cùng ý tưởng cho lệnh và truy vấn trong ứng dụng. Cũng nên tránh gán nhãn Mediator cho mọi thứ có tính điều phối: ExecutorService chỉ là bộ thực thi tác vụ, còn domain event kiểu publish/subscribe gần với Observer hơn.
Cần phân biệt với Observer: Observer là quan hệ một-chiều, chủ thể phát sự kiện cho người đăng ký và không quan tâm họ làm gì. Mediator pattern có luật điều phối, có thể quyết định gọi ai, theo thứ tự nào, kể cả chặn sự kiện. Nhiều cài đặt Mediator pattern thực tế dùng Observer ở bên dưới để chuyển sự kiện.
Khi nào không nên dùng Mediator pattern
Nhược điểm lớn nhất của Mediator pattern, như refactoring.guru cảnh báo ("Over time a mediator can evolve into a God Object"), là nó có thể phình thành God Object: biết quá nhiều, làm quá nhiều. CheckoutDialog ở trên mới 40 dòng, nhưng nếu thêm 30 luật khuyến mãi, nó thành một lớp khổng lồ khó test. Vài nguyên tắc để tránh:
- Chỉ dùng khi có nhiều thành phần phụ thuộc chéo. Hai lớp gọi nhau thì cứ để chúng gọi nhau.
- Tách đối tượng điều phối theo ngữ cảnh (một cho form thanh toán, một cho form địa chỉ), không dùng một Mediator toàn ứng dụng.
- Đẩy luật phức tạp ra các lớp chính sách riêng (ví dụ
ShippingPolicy,CodLimitPolicy), Mediator pattern chỉ điều phối. - Nếu chỉ cần "ai quan tâm thì nghe", dùng Observer hoặc event bus là đủ, đừng viết Mediator pattern.
Memento pattern: lưu trạng thái mà không phá encapsulation
Bây giờ sang vấn đề thứ hai. Nhân viên bán hàng soạn một đơn nháp cho khách doanh nghiệp: thêm laptop, thêm chuột, viết ghi chú giao hàng. Họ muốn hoàn tác (undo) và làm lại (redo). Cách ngây thơ là cho lớp ngoài đọc mọi trường của đơn rồi tự lưu lại. Cách đó phá encapsulation: bên ngoài biết cấu trúc nội bộ, và đổi cấu trúc là hỏng cả phần undo.
Memento định nghĩa ba vai trò: Originator là đối tượng có trạng thái, tự tạo snapshot và tự khôi phục từ snapshot; Memento là snapshot bất biến; Caretaker giữ danh sách snapshot nhưng không đọc được nội dung. Trong Java, cách chặt chẽ nhất là lồng lớp Memento vào trong Originator với constructor và trường private, vì khi đó Snapshot nằm trong phạm vi của Originator và các lớp ở file hoặc package khác không đọc được trường của nó.
Có một chú ý về phạm vi private của Java: nó được kiểm tra ở mức lớp top-level chứa khai báo, không phải ở mức từng lớp lồng. Trong file demo dưới đây, DraftOrder, Snapshot và History đều là lớp lồng của cùng lớp Memento, nên về mặt ngôn ngữ History vẫn đọc được s.customer (tôi đã thử riêng một ví dụ ba lớp lồng như vậy và nó biên dịch, chạy, in ra giá trị). Demo chỉ không dùng quyền đó, và History chỉ gọi save() rồi restore(). Để che giấu thật sự, hãy tách DraftOrder (kèm Snapshot lồng bên trong) ra file riêng, hoặc cho Snapshot là một interface rỗng được triển khai bởi lớp private nằm trong Originator.
static class DraftOrder {
private String customer;
private final List<String> items = new ArrayList<>();
private String note = "";
static final class Snapshot {
private final String customer;
private final List<String> items;
private final String note;
private Snapshot(String customer, List<String> items, String note) {
this.customer = customer;
this.items = List.copyOf(items); // sao chép phòng thủ
this.note = note;
}
}
Snapshot save() { return new Snapshot(customer, items, note); }
void restore(Snapshot s) {
this.customer = s.customer;
this.items.clear();
this.items.addAll(s.items);
this.note = s.note;
}
}Caretaker là một cặp ngăn xếp undo và redo, có giới hạn số snapshot để không tốn bộ nhớ vô hạn:
static class History {
private final Deque<DraftOrder.Snapshot> undo = new ArrayDeque<>();
private final Deque<DraftOrder.Snapshot> redo = new ArrayDeque<>();
private final int limit;
private final DraftOrder order;
void checkpoint() {
undo.push(order.save());
if (undo.size() > limit) undo.removeLast(); // bỏ snapshot cũ nhất
redo.clear(); // thao tác mới xoá nhánh redo
}
boolean undo() {
if (undo.isEmpty()) return false;
redo.push(order.save());
order.restore(undo.pop());
return true;
}
boolean redo() {
if (redo.isEmpty()) return false;
undo.push(order.save());
order.restore(redo.pop());
return true;
}
}Quy ước dùng: gọi checkpoint() trước mỗi thao tác sửa. Biên dịch và chạy bằng javac -d out Memento.java && java -cp out Memento. Output:
Hiện tại : Công ty An Phát [LAP-001, MOUSE-07] ghi chú='Giao giờ hành chính'
Sau undo : Công ty An Phát [LAP-001, MOUSE-07] ghi chú=''
Undo tiếp: Công ty An Phát [LAP-001] ghi chú=''
Sau redo : Công ty An Phát [LAP-001, MOUSE-07] ghi chú=''
Thao tác mới xoá redo, redo() = false
-- Giới hạn lịch sử 3 --
Số snapshot giữ lại: 3
Undo hết cỡ: Khách lẻ [SKU-1, SKU-2, SKU-3] ghi chú=''Hai kết quả cuối đáng chú ý. Thêm 6 món liên tiếp nhưng lịch sử giới hạn 3 snapshot, nên undo hết cỡ chỉ lùi được về trạng thái có 3 món chứ không về rỗng. Đó là đánh đổi có chủ đích giữa bộ nhớ và độ sâu undo. Ngoài ra, sau một thao tác mới, redo bị xoá, đúng hành vi của mọi trình soạn thảo.
Memento trong JDK: StateEditable và UndoManager
JDK có sẵn một cài đặt Memento trong module java.desktop: interface StateEditable với hai phương thức storeState(Hashtable) và restoreState(Hashtable), dùng cùng StateEdit và UndoManager. Theo Javadoc, UndoManager giữ danh sách có thứ tự các UndoableEdit, mặc định giới hạn 100 mục, và undo()/redo() ném CannotUndoException/CannotRedoException khi không có gì để làm. Bạn không cần giao diện đồ hoạ để dùng nó, nên đoạn dưới chạy được ở chế độ headless: javac -d out SwingUndo.java && java -Djava.awt.headless=true -cp out SwingUndo.
static class Cart implements StateEditable {
int quantity = 1;
String coupon = "";
public void storeState(Hashtable<Object, Object> state) {
state.put("quantity", quantity);
state.put("coupon", coupon);
}
public void restoreState(Hashtable<?, ?> state) {
Object q = state.get("quantity");
if (q != null) quantity = (Integer) q;
Object c = state.get("coupon");
if (c != null) coupon = (String) c;
}
}
StateEdit e1 = new StateEdit(cart, "tăng số lượng");
cart.quantity = 3;
e1.end();
manager.addEdit(e1);Hiện tại : quantity=3, coupon='SALE10'
undo 1 : quantity=3, coupon='' | canRedo=true
undo 2 : quantity=1, coupon='' | canUndo=false
redo 1 : quantity=3, coupon='' | Redo nhập couponMột bẫy mà mình gặp khi chạy thật: ban đầu restoreState viết thẳng (Integer) state.get("quantity") và ném NullPointerException ở lần undo đầu tiên. Nguyên nhân là StateEdit lưu trạng thái trước và sau thao tác, rồi loại bỏ các khoá không đổi khỏi Hashtable để tiết kiệm bộ nhớ. Thao tác "nhập coupon" không đổi quantity, nên khoá này vắng mặt khi restore. Vì vậy phải kiểm tra null trước khi gán. Đây là ví dụ cho thấy ngay trong API chuẩn, Memento dạng "chỉ lưu phần khác biệt" có hợp đồng ngầm mà tài liệu mô tả khá ngắn gọn; hãy viết test cho mọi cặp save/restore.
Ghép Mediator pattern và Memento
Mediator pattern và Memento bổ sung nhau tự nhiên. Trong form thanh toán, mỗi lần người dùng đổi một ô, Mediator pattern xử lý hệ quả lan truyền; cũng chính đối tượng trung gian là nơi hợp lý để đặt Caretaker, vì nó thấy mọi thay đổi. Có một điểm dễ sai về thứ tự: nếu thành phần đã gán giá trị mới rồi mới báo changed, thì lúc Mediator chụp snapshot, trạng thái đã là trạng thái sau khi đổi và undo sẽ không lùi được. Khi ghép Mediator pattern với Memento, quy ước ở phần Memento ("gọi checkpoint() trước mỗi thao tác sửa") vẫn đúng ở đây, nên thành phần phải phát hai sự kiện: beforeChange trước khi gán để Mediator chụp snapshot, rồi changed sau khi gán để Mediator tính lại phần dẫn xuất.
Khi undo, Mediator pattern khôi phục các giá trị gốc rồi gọi refresh() một lần, và dùng cờ restoring để các lần gán trong lúc khôi phục không sinh thêm snapshot. Nguyên tắc: chỉ lưu trạng thái gốc, tính lại trạng thái dẫn xuất, vì lưu cả tổng tiền và nút rồi khôi phục chúng có thể lệch với giá trị gốc.
Đoạn code sau (file MediatorMemento.java) cài đặt cả hai cách để so sánh, với đơn 22 triệu, người dùng đổi sang chuyển khoản rồi đổi sang giao nhanh, sau đó undo hai lần. Dưới đây là phần cốt lõi, toàn bộ file nằm trong thư mục code. Chạy bằng javac -d out MediatorMemento.java && java -cp out MediatorMemento:
void set(String v) {
mediator.notify(this, "beforeChange"); // Mediator chụp trạng thái cũ ở đây
value = v;
mediator.notify(this, "changed"); // Mediator tính lại phần dẫn xuất ở đây
}
// ...
@Override public void notify(Object sender, String event) {
if (restoring) return;
if (event.equals("beforeChange") && snapshotBefore) undo.push(save());
if (event.equals("changed")) {
if (!snapshotBefore) undo.push(save()); // SAI: đã là trạng thái SAU khi đổi
refresh();
}
}
boolean undo() {
if (undo.isEmpty()) return false;
Snapshot s = undo.pop();
restoring = true;
coupon.set(s.coupon()); payment.set(s.payment()); shipping.set(s.shipping());
restoring = false;
refresh(); // một lần, để phần dẫn xuất nhất quán
return true;
}
// ... refresh(): tính lại tổng tiền và trạng thái nút từ trạng thái gốc== Chụp snapshot TRƯỚC khi đổi (beforeChange) ==
Ban đầu : coupon='', payment=COD, shipping=STANDARD | tổng=22025000 đ, nút Thanh toán=khoá
Sau 2 thao tác: coupon='', payment=BANK_TRANSFER, shipping=EXPRESS | tổng=22045000 đ, nút Thanh toán=mở
Undo 1 : coupon='', payment=BANK_TRANSFER, shipping=STANDARD | tổng=22025000 đ, nút Thanh toán=mở
Undo 2 : coupon='', payment=COD, shipping=STANDARD | tổng=22025000 đ, nút Thanh toán=khoá
== Chụp snapshot SAU khi đổi (changed), cách sai ==
Ban đầu : coupon='', payment=COD, shipping=STANDARD | tổng=22025000 đ, nút Thanh toán=khoá
Sau 2 thao tác: coupon='', payment=BANK_TRANSFER, shipping=EXPRESS | tổng=22045000 đ, nút Thanh toán=mở
Undo 1 : coupon='', payment=BANK_TRANSFER, shipping=EXPRESS | tổng=22045000 đ, nút Thanh toán=mở
Undo 2 : coupon='', payment=BANK_TRANSFER, shipping=STANDARD | tổng=22025000 đ, nút Thanh toán=mởVới Mediator pattern đóng vai người chụp snapshot, cách đúng (chụp ở beforeChange) lùi về từng bước: undo lần một trả giao hàng về STANDARD, undo lần hai trả thanh toán về COD và nút Thanh toán bị khoá trở lại như lúc đầu. Cách sai (chụp ở changed) lưu trạng thái sau thao tác, nên undo lần một không thay đổi gì ("Undo 1" vẫn là BANK_TRANSFER và EXPRESS), và hai lần undo vẫn không về được COD ban đầu. Lỗi này của Mediator pattern khi ghép Memento khó thấy vì mọi thứ vẫn chạy mà không có ngoại lệ.
Lỗi thường gặp
1. Snapshot dùng chung tham chiếu (shallow copy). Đây là lỗi kinh điển nhất. Đoạn sau cố tình viết sai để minh hoạ, snapshot giữ cùng danh sách với đối tượng gốc:
record Shallow(List<String> items) {}
Shallow save() { return new Shallow(items); } // không sao chép
void restore(Shallow s) { items.clear(); items.addAll(s.items()); }-- Lỗi shallow copy --
Snapshot lưu lúc có 1 món, nhưng giờ chứa: [A, B]
Sau restore: [] (kỳ vọng [A])Snapshot lưu khi chỉ có món A, nhưng thêm B vào đối tượng gốc cũng làm snapshot có B. Tệ hơn, restore gọi items.clear() trên cùng một danh sách với s.items(), nên cả hai cùng rỗng. Cách sửa: sao chép phòng thủ như List.copyOf trong constructor của Snapshot, hoặc dùng cấu trúc bất biến ngay từ đầu. Với đối tượng lồng nhau nhiều tầng, cần sao chép sâu, xem lại Prototype pattern: clone, copy constructor và sao chép sâu.
2. Lịch sử không giới hạn. Mỗi snapshot của một đơn hàng 1.000 dòng tốn bộ nhớ đáng kể; hãy đặt giới hạn như limit ở trên, hoặc lưu delta thay vì toàn bộ trạng thái như StateEdit làm.
3. Mediator pattern thành God Object. Dấu hiệu: notify có chuỗi if/else hàng trăm dòng theo tên thành phần. Hãy chuyển thành bảng đăng ký luật, hoặc tách Mediator con theo vùng chức năng.
4. Vòng lặp sự kiện. Mediator pattern dễ gặp lỗi này: trung gian gọi setEnabled, mà thành phần lại báo sự kiện ngược về Mediator, dẫn tới đệ quy vô hạn. Trong ví dụ trên setEnabled không phát sự kiện; nếu thành phần của bạn có phát, cần cờ chống đệ quy hoặc so sánh giá trị cũ trước khi thông báo.
5. Snapshot chứa tài nguyên không khôi phục được. Kết nối DB, luồng đang chạy, file đang mở không thể đưa vào Memento. Memento phù hợp với dữ liệu giá trị, không phù hợp với tài nguyên bên ngoài.
Đánh đổi và checklist của Mediator pattern và Memento
| Mẫu | Được | Mất |
|---|---|---|
| Mediator pattern | Giảm phụ thuộc chéo, thành phần tái sử dụng được, luật tập trung một chỗ | Dễ thành God Object; thêm một lớp gián tiếp khiến luồng khó theo dõi bằng debugger |
| Memento | Undo/redo, rollback mà không lộ cấu trúc nội bộ; Originator được giải phóng khỏi việc quản lý lịch sử | Tốn bộ nhớ nếu snapshot lớn hoặc nhiều; phải kiểm soát vòng đời của snapshot |
- Có từ ba thành phần trở lên phụ thuộc chéo? Cân nhắc Mediator pattern. Chỉ hai lớp? Gọi trực tiếp.
- Trong Mediator pattern, thành phần chỉ biết interface trung gian, không giữ tham chiếu tới thành phần khác.
- Luật nghiệp vụ phức tạp được tách ra lớp chính sách, Mediator chỉ điều phối.
- Memento lồng trong Originator, trường private, sao chép phòng thủ mọi collection.
- Chỉ lưu trạng thái gốc; trạng thái dẫn xuất tính lại sau khi restore.
- Khi ghép Mediator pattern với Memento, thành phần phát
beforeChangeđể Mediator pattern chụp snapshot trước khi giá trị đổi. - Với Mediator pattern, dùng cờ chống đệ quy khi khôi phục để lần gán lúc undo không tạo thêm snapshot.
- Giới hạn độ sâu lịch sử và có test cho vòng save, thay đổi, restore.
Về phía nguồn tham khảo, nếu muốn đọc định nghĩa gốc hãy xem chương Behavioral Patterns của sách Design Patterns (GoF, 1994), và mô tả Mediator pattern, Memento cùng sơ đồ UML tại refactoring.guru đã trích ở trên. Toàn bộ code trong bài nằm trong code/s6-19/ gồm Mediator.java (Mediator pattern), Memento.java, SwingUndo.java, MediatorMemento.java (ghép Mediator pattern với Memento) và file run_output.txt.
Bài tiếp theo
Hai mẫu hôm nay giải quyết cách các đối tượng nói chuyện và nhớ trạng thái. Bài kế tiếp đi vào thứ mà mọi framework Java đều dựa vào: Dependency Injection: tự viết IoC container để hiểu Spring, nơi chúng ta tự viết container tạo và nối các đối tượng để thấy Spring làm gì bên dưới @Autowired.



