🧩 Design Pattern — Mẫu thiết kế phần mềm
Khóa học tổng hợp 23 mẫu kinh điển của GoF và mẫu Object Pool. Mỗi bài có lý thuyết đầy đủ, sơ đồ UML minh hoạ, và code ví dụ chạy được bằng C++ (ngôn ngữ chính, định hướng lập trình nhúng), Python và Java.
?Design Pattern là gì?
Design Pattern (mẫu thiết kế) là những giải pháp đã được kiểm chứng cho các vấn đề lặp đi lặp lại trong thiết kế phần mềm hướng đối tượng. Nó không phải một đoạn code để chép, mà là một “bản thiết kế” tổng quát — bạn hiểu ý tưởng rồi áp dụng vào bài toán của mình, bằng ngôn ngữ của mình.
Năm 1994, bốn tác giả Erich Gamma, Richard Helm, Ralph Johnson và John Vlissides — thường gọi là “Gang of Four” (GoF) — xuất bản cuốn Design Patterns: Elements of Reusable Object-Oriented Software, mô tả 23 mẫu, chia thành 3 nhóm.
Mỗi mẫu thường được mô tả bằng 4 yếu tố: tên · vấn đề (khi nào dùng) · giải pháp (các thành phần và quan hệ) · hệ quả (ưu nhược điểm). Các bài trong khóa đều theo đúng cấu trúc này.
🗣️ Ngôn ngữ chung
Nói “dùng Observer cho cảm biến” là cả nhóm hiểu ngay thiết kế, không cần giải thích dài.
🧱 Code dễ mở rộng
Thêm cảm biến, thêm chế độ, đổi phần cứng mà không phải sửa lại code cũ đang chạy ổn.
🎓 Đọc hiểu thư viện
Arduino, ESP-IDF, Qt, STL, Python stdlib… đều dùng các mẫu này. Biết mẫu là đọc code nhanh hơn nhiều.
▦Ba nhóm mẫu thiết kế
| Nhóm | Trả lời câu hỏi | Ví dụ trong nhúng |
|---|---|---|
| Creational | Tạo đối tượng như thế nào cho linh hoạt, che giấu lớp cụ thể? | Một driver UART duy nhất (Singleton), pool buffer tĩnh (Object Pool) |
| Structural | Ghép các lớp/đối tượng thành cấu trúc lớn hơn ra sao? | Bọc thư viện cảm biến (Adapter), tách logic và driver màn hình (Bridge) |
| Behavioral | Các đối tượng giao tiếp và phân chia trách nhiệm thế nào? | Máy trạng thái robot (State), sự kiện cảm biến (Observer), hàng đợi lệnh (Command) |
⇢Cách đọc sơ đồ lớp UML
Mỗi bài đều có sơ đồ lớp UML. Một ô chữ nhật là một lớp: phần trên là tên (kèm «interface» hoặc «abstract» nếu có), phần giữa là thuộc tính, phần dưới là phương thức. Các đường nối thể hiện quan hệ:
SNăm nguyên lý SOLID — nền tảng của mọi mẫu
| Nguyên lý | Ý nghĩa ngắn gọn | Ví dụ nhúng | |
|---|---|---|---|
| S | Single Responsibility | Mỗi lớp chỉ có một lý do để thay đổi. | Lớp đọc cảm biến không lo việc gửi MQTT. |
| O | Open/Closed | Mở để mở rộng, đóng để sửa đổi. | Thêm loại cảm biến mới mà không sửa vòng loop(). |
| L | Liskov Substitution | Lớp con thay được lớp cha mà chương trình vẫn đúng. | Mọi ISensor đều trả về đơn vị °C như hợp đồng. |
| I | Interface Segregation | Nhiều interface nhỏ tốt hơn một interface “to đùng”. | Tách IReadable và ICalibratable. |
| D | Dependency Inversion | Phụ thuộc vào trừu tượng, không phụ thuộc vào cụ thể. | Logic robot phụ thuộc IMotor, không phụ thuộc L298N. |
CVì sao học bằng C++ — và lưu ý khi dùng trong nhúng
Firmware cho Arduino, ESP32, STM32 phần lớn được viết bằng C/C++. C++ cho phép dùng các mẫu hướng đối tượng mà vẫn kiểm soát chặt bộ nhớ và hiệu năng. Python (và MicroPython) giúp thử ý tưởng nhanh, viết công cụ trên PC; Java giúp đối chiếu với phần lớn tài liệu Design Pattern trên mạng.
- Hạn chế cấp phát động lúc chạy (
new,malloc,std::vectortăng kích thước): heap nhỏ dễ phân mảnh. Ưu tiên đối tượng tĩnh,std::array, Object Pool. - Hàm ảo tốn thêm một con trỏ vtable mỗi đối tượng và một lần gọi gián tiếp — thường chấp nhận được; khi cần tối ưu tuyệt đối, dùng template để chọn lúc biên dịch (xem bài Strategy, Visitor).
- Nhiều trình biên dịch nhúng tắt exception và RTTI (
-fno-exceptions -fno-rtti) → trả lỗi bằngbool/std::optional, tránhdynamic_cast. - Arduino AVR (Uno, Nano) không có STL đầy đủ; ESP32 và STM32 (GCC) hỗ trợ C++17 tốt. Các ví dụ dùng
std::coutđể chạy trên PC — trên board hãy thay bằngSerial.println(). - Luôn khai báo
virtual ~Base() = default;cho lớp cơ sở có hàm ảo; ưu tiênstd::unique_ptrthay cho con trỏ thô sở hữu.
⚙Chuẩn bị môi trường thực hành
| Ngôn ngữ | Cài đặt | Chạy một ví dụ |
|---|---|---|
| C++17 | Windows: MinGW-w64 / MSYS2 (g++) hoặc Visual Studio; macOS/Linux: g++ hoặc clang++ | g++ -std=c++17 01-singleton.cpp -o app && ./app |
| Python 3.10+ | python.org (tick “Add to PATH”) | python 01-singleton.py |
| Java 17+ | Eclipse Temurin / Oracle JDK | java 01-singleton.java (chạy thẳng file nguồn) |
Không muốn cài gì? Dán code vào godbolt.org (C++), online-python.com hoặc jdoodle.com. Trên Windows, nếu chữ tiếng Việt bị lỗi trong cửa sổ dòng lệnh, chạy chcp 65001 trước.
⌚Lộ trình gợi ý — 12 buổi × 2 giờ
Design Pattern là gì · UML · SOLID · ôn OOP trong C++ (virtual, con trỏ thông minh)
Dự án: firmware trạm thời tiết ESP32 kết hợp Singleton + Observer + State + Strategy + Command
≡Danh sách 24 mẫu
| # | Mẫu | Tên tiếng Việt | Nhóm | Độ phổ biến |
|---|---|---|---|---|
| 1 | 1️⃣ Singleton | Đơn thể | Creational | ★★★★★ |
| 2 | 🏭 Factory Method | Phương thức nhà máy | Creational | ★★★★★ |
| 3 | 🏗️ Abstract Factory | Nhà máy trừu tượng | Creational | ★★★★★ |
| 4 | 🧱 Builder | Người xây dựng | Creational | ★★★★★ |
| 5 | 🧬 Prototype | Nguyên mẫu | Creational | ★★★★★ |
| 6 | 🏊 Object Pool | Bể đối tượng | Creational | ★★★★★ |
| 7 | 🔌 Adapter | Bộ chuyển đổi | Structural | ★★★★★ |
| 8 | 🌉 Bridge | Cầu nối | Structural | ★★★★★ |
| 9 | 🌳 Composite | Hỗn hợp / Cây đối tượng | Structural | ★★★★★ |
| 10 | 🎁 Decorator | Người trang trí | Structural | ★★★★★ |
| 11 | 🏠 Facade | Mặt tiền | Structural | ★★★★★ |
| 12 | 🪶 Flyweight | Hạng ruồi / Dùng chung | Structural | ★★★★★ |
| 13 | 🛡️ Proxy | Người đại diện | Structural | ★★★★★ |
| 14 | ⛓️ Chain of Responsibility | Chuỗi trách nhiệm | Behavioral | ★★★★★ |
| 15 | 🎮 Command | Mệnh lệnh | Behavioral | ★★★★★ |
| 16 | 🗣️ Interpreter | Thông dịch | Behavioral | ★★★★★ |
| 17 | 🔁 Iterator | Bộ duyệt | Behavioral | ★★★★★ |
| 18 | 🗼 Mediator | Người trung gian | Behavioral | ★★★★★ |
| 19 | 💾 Memento | Vật lưu niệm | Behavioral | ★★★★★ |
| 20 | 👀 Observer | Người quan sát | Behavioral | ★★★★★ |
| 21 | 🚦 State | Trạng thái | Behavioral | ★★★★★ |
| 22 | ♟️ Strategy | Chiến lược | Behavioral | ★★★★★ |
| 23 | 📐 Template Method | Phương thức khuôn mẫu | Behavioral | ★★★★★ |
| 24 | 🧳 Visitor | Người viếng thăm | Behavioral | ★★★★★ |
1️⃣ Singleton — Đơn thể
Mục đích: Đảm bảo một lớp chỉ có duy nhất một thể hiện (instance) và cung cấp một điểm truy cập toàn cục tới thể hiện đó.
1Vấn đề cần giải quyết
Có những đối tượng mà cả chương trình chỉ nên có một: bộ cấu hình ứng dụng, đối tượng ghi log, bộ quản lý kết nối phần cứng (cổng Serial tới Arduino), bộ đệm (cache)… Nếu mỗi nơi tự new một bản:
- Cấu hình bị lệch nhau — module A sửa
wifi.ssidnhưng module B vẫn đọc giá trị cũ. - Lãng phí tài nguyên — mở nhiều kết nối tới cùng một thiết bị, có khi còn gây xung đột.
- Biến toàn cục (
public static) thì giải quyết được “truy cập ở mọi nơi”, nhưng không chặn được người khác tạo thêm đối tượng và có thể bị ghi đè bất cứ lúc nào.
2Giải pháp
Singleton giải quyết bằng 3 bước:
- Đặt constructor là
private→ bên ngoài không thể gọinew. - Giữ một biến
statictrỏ tới thể hiện duy nhất. - Cung cấp phương thức
static getInstance(): lần đầu thì tạo, các lần sau trả lại đúng đối tượng đã tạo.
Cách cài đặt được khuyên dùng ở mỗi ngôn ngữ:
| Ngôn ngữ | Cách viết | Ghi chú |
|---|---|---|
| C++11 trở lên | Meyers Singleton: static T& get() { static T inst; return inst; } | Lazy, thread-safe theo chuẩn, không dùng heap. Nhớ = delete copy constructor và phép gán. |
| C++ trên vi điều khiển | Đối tượng toàn cục / static (như Serial, WiFi của Arduino) | Đơn giản nhất, nhưng cẩn thận thứ tự khởi tạo giữa các file .cpp — dùng hàm begin() gọi trong setup(). |
| Python | Một module, hoặc ghi đè __new__ / metaclass | Module chỉ được nạp một lần nên đã là singleton tự nhiên. |
| Java | enum hoặc static holder (Bill Pugh) | Double-checked locking phải có volatile. |
3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Singleton | Khai báo constructor private, giữ thể hiện duy nhất trong biến static và trả về qua getInstance(). |
| Client | Không bao giờ new Singleton, chỉ gọi Singleton.getInstance(). |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Singleton – C++17. Biên dịch: g++ -std=c++17 01-singleton.cpp
// Trên Arduino/ESP32: thay std::cout bằng Serial.println.
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <thread>
#include <mutex>
#include <vector>
// Cách 1 – "Meyers Singleton": biến static cục bộ trong hàm.
// Từ C++11, chuẩn đảm bảo việc khởi tạo biến static cục bộ là thread-safe
// và chỉ diễn ra MỘT lần, ở lần gọi đầu tiên (lazy).
class AppConfig {
public:
static AppConfig& getInstance() {
static AppConfig instance; // nằm ở vùng nhớ tĩnh, không dùng heap
return instance;
}
// Cấm sao chép và gán -> không thể tạo bản thứ hai
AppConfig(const AppConfig&) = delete;
AppConfig& operator=(const AppConfig&) = delete;
void set(const std::string& k, const std::string& v) { props_[k] = v; }
std::string get(const std::string& k) const { return props_.at(k); }
private:
AppConfig() { std::cout << ">> AppConfig được khởi tạo (chỉ 1 lần)\n"; }
std::map<std::string, std::string> props_;
};
// Cách 2 – Singleton quản lý một ngoại vi phần cứng (UART giả lập).
// Trên vi điều khiển chỉ có MỘT bộ UART0 thật, nên đối tượng điều khiển nó
// cũng chỉ nên có một. std::call_once đảm bảo init phần cứng chạy đúng 1 lần.
class Uart0 {
public:
static Uart0& getInstance() {
static Uart0 instance;
std::call_once(initFlag_, [] { instance.initHardware(115200); });
return instance;
}
Uart0(const Uart0&) = delete;
Uart0& operator=(const Uart0&) = delete;
static inline int initCount = 0;
private:
Uart0() = default;
void initHardware(int baud) { ++initCount; baud_ = baud; /* cấu hình thanh ghi... */ }
int baud_ = 0;
static inline std::once_flag initFlag_;
};
int main() {
AppConfig& c1 = AppConfig::getInstance();
AppConfig& c2 = AppConfig::getInstance();
c1.set("wifi.ssid", "BKSTAR-Lab");
std::cout << "&c1 == &c2 ? " << std::boolalpha << (&c1 == &c2) << "\n";
std::cout << "Đọc qua c2: wifi.ssid = " << c2.get("wifi.ssid") << "\n";
// 50 luồng cùng gọi getInstance()
std::set<const void*> addrs;
std::mutex m;
std::vector<std::thread> threads;
for (int i = 0; i < 50; ++i)
threads.emplace_back([&] {
const void* p = &Uart0::getInstance();
std::lock_guard<std::mutex> lock(m);
addrs.insert(p);
});
for (auto& t : threads) t.join();
std::cout << "Số địa chỉ Uart0 khác nhau từ 50 luồng: " << addrs.size() << "\n";
std::cout << "Số lần initHardware() chạy: " << Uart0::initCount << "\n";
// AppConfig x; // LỖI biên dịch: constructor là private
// AppConfig y = c1; // LỖI biên dịch: copy constructor đã bị delete
}
g++ -std=c++17 01-singleton.cpp -o app && ./app>> AppConfig được khởi tạo (chỉ 1 lần) &c1 == &c2 ? true Đọc qua c2: wifi.ssid = BKSTAR-Lab Số địa chỉ Uart0 khác nhau từ 50 luồng: 1 Số lần initHardware() chạy: 1
Giải thích code C++17
AppConfig::getInstance()dùng Meyers Singleton: biếnstaticcục bộ được chuẩn C++11 đảm bảo khởi tạo đúng 1 lần và thread-safe.= deletecopy constructor và phép gán → dòngAppConfig y = c1;sẽ lỗi ngay lúc biên dịch, không đợi tới lúc chạy.Uart0mô phỏng driver ngoại vi:std::call_onceđảm bảoinitHardware()chạy đúng 1 lần dù 50 luồng gọi đồng thời.- Đối tượng nằm ở vùng nhớ tĩnh (
.bss/.data) chứ không phải heap — quan trọng với MCU có RAM nhỏ.
"""Singleton – Python 3.
Trong Python, MODULE chính là singleton tự nhiên: import nhiều lần vẫn chỉ nạp một lần.
Ở đây minh hoạ thêm 2 cách viết bằng lớp. (MicroPython trên ESP32 dùng được cách 1 và module.)"""
import threading
# Cách 1 – ghi đè __new__: mọi lần gọi AppConfig() đều trả về cùng một đối tượng
class AppConfig:
_instance = None
_lock = threading.Lock()
def __new__(cls):
if cls._instance is None: # kiểm tra nhanh, không khoá
with cls._lock:
if cls._instance is None: # double-checked locking
print(">> AppConfig được khởi tạo (chỉ 1 lần)")
cls._instance = super().__new__(cls)
cls._instance.props = {}
return cls._instance
# Cách 2 – metaclass: tái sử dụng cho nhiều lớp
class SingletonMeta(type):
_instances = {}
_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
with cls._lock:
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class Uart0(metaclass=SingletonMeta):
init_count = 0
def __init__(self, baud=115200):
Uart0.init_count += 1 # chỉ chạy 1 lần nhờ metaclass
self.baud = baud
if __name__ == "__main__":
c1 = AppConfig()
c2 = AppConfig()
c1.props["wifi.ssid"] = "BKSTAR-Lab"
print("c1 is c2 ?", c1 is c2)
print("Đọc qua c2: wifi.ssid =", c2.props["wifi.ssid"])
ids = set()
threads = [threading.Thread(target=lambda: ids.add(id(Uart0()))) for _ in range(50)]
for t in threads: t.start()
for t in threads: t.join()
print("Số đối tượng Uart0 khác nhau từ 50 luồng:", len(ids))
print("Số lần __init__ của Uart0 chạy:", Uart0.init_count)
python 01-singleton.py>> AppConfig được khởi tạo (chỉ 1 lần) c1 is c2 ? True Đọc qua c2: wifi.ssid = BKSTAR-Lab Số đối tượng Uart0 khác nhau từ 50 luồng: 1 Số lần __init__ của Uart0 chạy: 1
Giải thích code Python 3
- Cách 1 ghi đè
__new__+ khoáthreading.Lock(double-checked locking). - Cách 2 dùng metaclass
SingletonMeta— tái sử dụng cho nhiều lớp;__init__chỉ chạy một lần. - Cách “Pythonic” nhất: đặt trạng thái trong một module (
config.py) — module chỉ được import một lần.
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class SingletonDemo {
public static void main(String[] args) throws InterruptedException {
// 1. Lấy instance nhiều lần -> luôn là cùng một đối tượng
AppConfig c1 = AppConfig.getInstance();
AppConfig c2 = AppConfig.getInstance();
c1.set("wifi.ssid", "BKSTAR-Lab");
System.out.println("c1 == c2 ? " + (c1 == c2));
System.out.println("Đọc qua c2: wifi.ssid = " + c2.get("wifi.ssid"));
// 2. Kiểm tra an toàn đa luồng: 50 luồng cùng gọi getInstance()
Set<Integer> ids = ConcurrentHashMap.newKeySet();
Thread[] threads = new Thread[50];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> ids.add(System.identityHashCode(LazyLogger.getInstance())));
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println("Số instance LazyLogger tạo ra bởi 50 luồng: " + ids.size());
System.out.println("Số lần constructor LazyLogger chạy: " + LazyLogger.constructed);
// 3. Enum Singleton – cách ngắn gọn và an toàn nhất trong Java
Counter.INSTANCE.increase();
Counter.INSTANCE.increase();
System.out.println("Counter.INSTANCE.value = " + Counter.INSTANCE.value());
}
}
/** Cách 1 – Bill Pugh (static holder): lazy + thread-safe, không cần synchronized. */
final class AppConfig {
private final java.util.Map<String, String> props = new java.util.HashMap<>();
private AppConfig() { // constructor private: bên ngoài không thể new
System.out.println(">> AppConfig được khởi tạo (chỉ 1 lần)");
}
private static class Holder { // lớp này chỉ được nạp khi gọi getInstance()
private static final AppConfig INSTANCE = new AppConfig();
}
public static AppConfig getInstance() {
return Holder.INSTANCE;
}
public void set(String key, String value) { props.put(key, value); }
public String get(String key) { return props.get(key); }
}
/** Cách 2 – Double-checked locking: lazy, thread-safe, chỉ khóa ở lần tạo đầu tiên. */
final class LazyLogger {
static volatile int constructed = 0;
private static volatile LazyLogger instance; // volatile để tránh lỗi reorder lệnh
private LazyLogger() { constructed++; }
public static LazyLogger getInstance() {
if (instance == null) { // kiểm tra lần 1 – không khóa (nhanh)
synchronized (LazyLogger.class) {
if (instance == null) { // kiểm tra lần 2 – trong vùng khóa
instance = new LazyLogger();
}
}
}
return instance;
}
}
/** Cách 3 – Enum: JVM đảm bảo duy nhất, chống cả reflection và serialization. */
enum Counter {
INSTANCE;
private int value;
public void increase() { value++; }
public int value() { return value; }
}
java 01-singleton.java>> AppConfig được khởi tạo (chỉ 1 lần) c1 == c2 ? true Đọc qua c2: wifi.ssid = BKSTAR-Lab Số instance LazyLogger tạo ra bởi 50 luồng: 1 Số lần constructor LazyLogger chạy: 1 Counter.INSTANCE.value = 2
Giải thích code Java 17+
- AppConfig dùng kỹ thuật static holder: lớp lồng
Holderchỉ được JVM nạp khi lần đầu gọigetInstance(), và việc nạp lớp vốn đã thread-safe. - LazyLogger minh hoạ double-checked locking. Từ khoá
volatilelà bắt buộc — thiếu nó, luồng khác có thể thấy một đối tượng “chưa khởi tạo xong”. - Thử 50 luồng gọi đồng thời: chỉ 1 thể hiện được tạo, constructor chạy đúng 1 lần.
- Counter là cách viết bằng
enum: gọn nhất và an toàn nhất.
6Áp dụng trong lập trình nhúng
- Mỗi ngoại vi phần cứng là duy nhất (UART0, bus I2C, SPI, bộ ADC) → lớp driver của nó nên là Singleton để tránh hai nơi cùng cấu hình thanh ghi.
- Arduino làm sẵn điều này:
Serial,Wire,SPI,WiFilà các đối tượng toàn cục duy nhất. - Tránh khởi tạo phần cứng trong constructor của đối tượng toàn cục (chạy trước
main()/setup(), khi clock chưa cấu hình xong) — dùng hàmbegin(). - Trên FreeRTOS: nếu nhiều task cùng dùng Singleton, bảo vệ dữ liệu bên trong bằng mutex.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Đảm bảo chỉ có một thể hiện.
- Truy cập toàn cục, khởi tạo trễ (lazy) khi cần.
- Kiểm soát chặt việc truy cập tài nguyên dùng chung.
✘ Nhược điểm
- Vi phạm nguyên lý Single Responsibility (vừa lo nghiệp vụ vừa lo vòng đời).
- Che giấu phụ thuộc → khó viết unit test, khó mock.
- Cần cẩn thận trong môi trường đa luồng.
- Dễ bị lạm dụng như một “biến toàn cục trá hình”.
8Khi nào nên dùng
- Cần đúng một đối tượng dùng chung: cấu hình, logger, cache, connection manager, driver phần cứng.
- Muốn kiểm soát chặt hơn biến toàn cục.
9Gặp ở đâu trong thực tế
C++ / nhúng
- Arduino:
Serial,Wire,WiFi,EEPROM - ESP-IDF:
esp_event_loopmặc định std::cout— đối tượng luồng xuất duy nhất
Python
- Module Python (mọi lần
importtrả về cùng đối tượng module) logging.getLogger(name)None,True,False
Java
java.lang.Runtime.getRuntime()java.awt.Desktop.getDesktop()- Bean mặc định (scope singleton) trong Spring Framework
10Liên hệ với các mẫu khác
Facade, Abstract Factory và Builder thường được cài đặt dưới dạng Singleton. Khác với Flyweight: Flyweight có nhiều thể hiện bất biến được chia sẻ, Singleton chỉ có một thể hiện (có thể thay đổi).
11Bài tập thực hành
🏭 Factory Method — Phương thức nhà máy
Mục đích: Định nghĩa một giao diện để tạo đối tượng, nhưng để lớp con quyết định lớp cụ thể nào sẽ được khởi tạo.
1Vấn đề cần giải quyết
Ứng dụng của trường ban đầu chỉ gửi thông báo qua Email, nên code viết thẳng new EmailNotification() khắp nơi. Giờ nhà trường muốn thêm SMS và Zalo:
- Phải tìm và sửa mọi chỗ có
new EmailNotification(), thêm hàng loạtif/elsehoặcswitch. - Mỗi lần thêm kênh mới lại sửa code cũ → vi phạm nguyên lý Open/Closed (mở để mở rộng, đóng để sửa đổi).
- Logic nghiệp vụ (gửi cho ai, khi nào) bị trộn lẫn với logic khởi tạo đối tượng.
2Giải pháp
Thay vì gọi new trực tiếp, ta gọi một phương thức đặc biệt — factory method — createNotification(). Lớp cha NotificationService chứa thuật toán chung và gọi factory method, còn lớp con ghi đè phương thức này để trả về loại thông báo cụ thể.
Mọi sản phẩm đều cài đặt chung interface Notification, nên lớp cha làm việc với sản phẩm mà không cần biết lớp cụ thể của nó.
switch trả về đối tượng (không phải mẫu GoF). Factory Method dựa vào kế thừa và đa hình — muốn thêm loại mới chỉ cần thêm lớp con, không sửa code cũ.3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Product (Notification) | Interface chung cho mọi đối tượng mà factory method tạo ra. |
| ConcreteProduct (EmailNotification…) | Các cài đặt cụ thể của Product. |
| Creator (NotificationService) | Khai báo factory method trả về Product; chứa logic nghiệp vụ dùng Product. |
| ConcreteCreator (EmailService…) | Ghi đè factory method để trả về ConcreteProduct tương ứng. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Factory Method – C++17
#include <iostream>
#include <memory>
#include <string>
#include <typeinfo>
// ===== Product =====
class Notification {
public:
virtual ~Notification() = default; // destructor ảo: bắt buộc khi xoá qua con trỏ lớp cha
virtual void send(const std::string& message) = 0;
};
// ===== Concrete Products =====
class EmailNotification : public Notification {
public:
void send(const std::string& m) override { std::cout << "[EMAIL] " << m << "\n"; }
};
class SmsNotification : public Notification {
public:
void send(const std::string& m) override {
// SMS giới hạn độ dài (tính theo byte để đơn giản)
std::cout << "[SMS] " << (m.size() > 30 ? m.substr(0, 30) + "..." : m) << "\n";
}
};
class ZaloNotification : public Notification {
public:
void send(const std::string& m) override { std::cout << "[ZALO] " << m << " 👍\n"; }
};
// ===== Creator =====
class NotificationService {
public:
virtual ~NotificationService() = default;
// Logic nghiệp vụ dùng sản phẩm mà KHÔNG biết lớp cụ thể của nó
void notifyUser(const std::string& message) {
std::unique_ptr<Notification> n = createNotification();
std::cout << name() << " -> ";
n->send(message);
}
protected:
// FACTORY METHOD – lớp con quyết định tạo đối tượng nào
virtual std::unique_ptr<Notification> createNotification() = 0;
virtual const char* name() const = 0;
};
// ===== Concrete Creators =====
class EmailService : public NotificationService {
protected:
std::unique_ptr<Notification> createNotification() override { return std::make_unique<EmailNotification>(); }
const char* name() const override { return "EmailService"; }
};
class SmsService : public NotificationService {
protected:
std::unique_ptr<Notification> createNotification() override { return std::make_unique<SmsNotification>(); }
const char* name() const override { return "SmsService"; }
};
class ZaloService : public NotificationService {
protected:
std::unique_ptr<Notification> createNotification() override { return std::make_unique<ZaloNotification>(); }
const char* name() const override { return "ZaloService"; }
};
int main() {
EmailService email;
SmsService sms;
ZaloService zalo;
NotificationService* services[] = { &email, &sms, &zalo }; // mảng tĩnh, không cấp phát
for (NotificationService* s : services)
s->notifyUser("Lop Design Pattern bat dau luc 19h toi nay!");
}
g++ -std=c++17 02-factory-method.cpp -o app && ./appEmailService -> [EMAIL] Lop Design Pattern bat dau luc 19h toi nay! SmsService -> [SMS] Lop Design Pattern bat dau luc... ZaloService -> [ZALO] Lop Design Pattern bat dau luc 19h toi nay! 👍
Giải thích code C++17
createNotification()là hàm thuần ảo (= 0) trả vềstd::unique_ptr<Notification>— quyền sở hữu rõ ràng, không rò rỉ bộ nhớ.- Lớp gốc có
virtual ~Notification() = default;— bắt buộc khi xoá đối tượng con qua con trỏ lớp cha. - Mảng
NotificationService* services[]cấp phát tĩnh, chỉ đối tượng sản phẩm mới tạo bằngmake_unique. - Lưu ý:
substr(0, 30)cắt theo byte, có thể cắt đôi một ký tự UTF-8 tiếng Việt — vì vậy thông điệp mẫu viết không dấu.
"""Factory Method – Python 3. Dùng abc.ABC để khai báo lớp/phương thức trừu tượng."""
from abc import ABC, abstractmethod
# ===== Product =====
class Notification(ABC):
@abstractmethod
def send(self, message: str) -> None: ...
class EmailNotification(Notification):
def send(self, message): print(f"[EMAIL] {message}")
class SmsNotification(Notification):
def send(self, message):
text = message[:30] + "..." if len(message) > 30 else message
print(f"[SMS] {text}")
class ZaloNotification(Notification):
def send(self, message): print(f"[ZALO] {message} 👍")
# ===== Creator =====
class NotificationService(ABC):
@abstractmethod
def create_notification(self) -> Notification:
"""Factory Method – lớp con quyết định tạo đối tượng nào."""
def notify_user(self, message: str) -> None:
n = self.create_notification()
print(f"{type(self).__name__} -> ", end="")
n.send(message)
# ===== Concrete Creators =====
class EmailService(NotificationService):
def create_notification(self): return EmailNotification()
class SmsService(NotificationService):
def create_notification(self): return SmsNotification()
class ZaloService(NotificationService):
def create_notification(self): return ZaloNotification()
if __name__ == "__main__":
for service in (EmailService(), SmsService(), ZaloService()):
service.notify_user("Lớp Design Pattern bắt đầu lúc 19h tối nay!")
# Python: lớp là "first-class object" nên Simple Factory thường chỉ là một dict
registry = {"email": EmailNotification, "sms": SmsNotification}
registry["sms"]().send("Tạo đối tượng từ tên lớp lưu trong dict")
python 02-factory-method.pyEmailService -> [EMAIL] Lớp Design Pattern bắt đầu lúc 19h tối nay! SmsService -> [SMS] Lớp Design Pattern bắt đầu lúc... ZaloService -> [ZALO] Lớp Design Pattern bắt đầu lúc 19h tối nay! 👍 [SMS] Tạo đối tượng từ tên lớp lưu t...
Giải thích code Python 3
abc.ABC+@abstractmethodkhiến lớp con quên cài đặt factory method sẽ báo lỗi ngay khi tạo đối tượng.- Vì lớp trong Python là đối tượng, một
dictánh xạ tên → lớp đã là “simple factory” rất gọn.
public class FactoryMethodDemo {
public static void main(String[] args) {
// Client chỉ làm việc với lớp trừu tượng NotificationService.
// Muốn đổi kênh gửi -> đổi lớp con, không sửa logic gửi.
NotificationService[] services = {
new EmailService(),
new SmsService(),
new ZaloService()
};
for (NotificationService s : services) {
s.notifyUser("Lớp Design Pattern bắt đầu lúc 19h tối nay!");
}
}
}
// ===== Product: giao diện chung cho mọi loại thông báo =====
interface Notification {
void send(String message);
}
// ===== Concrete Products =====
class EmailNotification implements Notification {
public void send(String message) { System.out.println("[EMAIL] " + message); }
}
class SmsNotification implements Notification {
public void send(String message) {
// SMS giới hạn độ dài -> xử lý riêng ở đây
String text = message.length() > 30 ? message.substring(0, 30) + "..." : message;
System.out.println("[SMS] " + text);
}
}
class ZaloNotification implements Notification {
public void send(String message) { System.out.println("[ZALO] " + message + " 👍"); }
}
// ===== Creator: chứa thuật toán chung + "factory method" trừu tượng =====
abstract class NotificationService {
/** Factory Method – lớp con quyết định tạo đối tượng cụ thể nào. */
protected abstract Notification createNotification();
/** Logic nghiệp vụ dùng sản phẩm mà KHÔNG biết lớp cụ thể của nó. */
public void notifyUser(String message) {
Notification n = createNotification();
System.out.print(getClass().getSimpleName() + " -> ");
n.send(message);
}
}
// ===== Concrete Creators =====
class EmailService extends NotificationService {
protected Notification createNotification() { return new EmailNotification(); }
}
class SmsService extends NotificationService {
protected Notification createNotification() { return new SmsNotification(); }
}
class ZaloService extends NotificationService {
protected Notification createNotification() { return new ZaloNotification(); }
}
java 02-factory-method.javaEmailService -> [EMAIL] Lớp Design Pattern bắt đầu lúc 19h tối nay! SmsService -> [SMS] Lớp Design Pattern bắt đầu lúc... ZaloService -> [ZALO] Lớp Design Pattern bắt đầu lúc 19h tối nay! 👍
Giải thích code Java 17+
notifyUser()ở lớp cha là logic chung — nó không hề biết đang gửi qua Email hay SMS.- Mỗi lớp con chỉ cần ghi đè một dòng:
createNotification(). SmsNotificationcó xử lý riêng (cắt chuỗi 30 ký tự) — chi tiết này được đóng gói trong sản phẩm, client không cần biết.- Thêm kênh Telegram? Tạo
TelegramNotification+TelegramService. Không sửa dòng code cũ nào.
6Áp dụng trong lập trình nhúng
- Firmware hỗ trợ nhiều phiên bản phần cứng:
createSensor()trả vềDht22SensorhaySht31Sensortuỳ chân cấu hình hoặc giá trị trong EEPROM. - Trên MCU nên tránh
newlúc chạy: factory có thể trả về tham chiếu tới đối tượng tĩnh đã tạo sẵn, hoặc dùng placement new vào buffer tĩnh. - Có thể thay đa hình lúc chạy bằng
#if/template để chọn lúc biên dịch khi phần cứng cố định.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Tách rời code tạo đối tượng khỏi code sử dụng (loose coupling).
- Tuân thủ Open/Closed: thêm sản phẩm mới không sửa code cũ.
- Tuân thủ Single Responsibility: việc khởi tạo tập trung một chỗ.
✘ Nhược điểm
- Số lượng lớp tăng lên (mỗi sản phẩm cần thêm một Creator con).
- Có thể “dùng dao mổ trâu giết gà” nếu chỉ có 1–2 loại sản phẩm không bao giờ thay đổi.
8Khi nào nên dùng
- Không biết trước chính xác loại đối tượng cần tạo.
- Muốn cho phép người dùng thư viện/framework mở rộng các thành phần bên trong.
- Muốn tái sử dụng đối tượng có sẵn thay vì luôn tạo mới (factory method có thể trả về đối tượng từ cache).
9Gặp ở đâu trong thực tế
C++ / nhúng
std::make_unique,std::make_shared- Qt:
QStyleFactory::create() - Arduino:
WiFiClient/WiFiClientSecuretạo theo cấu hình
Python
logging.getLogger()open()trả vềTextIOWrapperhayBufferedReadertuỳ chế độcollections.namedtuple()tạo lớp mới
Java
java.util.Calendar.getInstance()java.text.NumberFormat.getInstance()Collection.iterator()— mỗi collection tạo iterator riêngjavax.xml.parsers.DocumentBuilderFactory.newInstance()
10Liên hệ với các mẫu khác
Factory Method thường là điểm khởi đầu, sau đó phát triển thành Abstract Factory, Prototype hoặc Builder khi cần linh hoạt hơn. Template Method thường gọi Factory Method bên trong.
11Bài tập thực hành
🏗️ Abstract Factory — Nhà máy trừu tượng
Mục đích: Cung cấp một interface để tạo ra các họ (family) đối tượng liên quan với nhau mà không cần chỉ rõ lớp cụ thể của chúng.
1Vấn đề cần giải quyết
Ứng dụng có giao diện Sáng (Light) và Tối (Dark). Mỗi giao diện gồm nhiều thành phần: nút bấm, checkbox, thanh cuộn…
- Nếu tạo từng thành phần bằng
if (dark) new DarkButton() else new LightButton()ở khắp nơi → code rối, dễ trộn lẫn nút tối với checkbox sáng. - Thêm giao diện thứ ba (High Contrast) phải sửa tất cả các chỗ đó.
2Giải pháp
Khai báo interface riêng cho từng loại sản phẩm (Button, Checkbox). Tạo interface Abstract Factory (UIFactory) với các phương thức tạo cho mọi loại sản phẩm. Mỗi “họ” sản phẩm có một Concrete Factory riêng (LightThemeFactory, DarkThemeFactory).
Client nhận một factory duy nhất lúc khởi động và chỉ làm việc qua các interface → đảm bảo mọi thành phần cùng một họ.
3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| AbstractFactory (UIFactory) | Khai báo các phương thức tạo cho từng loại sản phẩm trừu tượng. |
| ConcreteFactory (LightThemeFactory…) | Cài đặt các phương thức tạo, trả về sản phẩm cùng một họ. |
| AbstractProduct (Button, Checkbox) | Interface cho một loại sản phẩm. |
| ConcreteProduct (DarkButton…) | Sản phẩm cụ thể thuộc một họ. |
| Client | Chỉ dùng AbstractFactory và AbstractProduct. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Abstract Factory – C++17
// (Trong firmware, ý tưởng tương tự: một factory cho màn hình OLED, một factory cho LCD.)
// Ở đây: mỗi "họ" giao diện Light/Dark gồm Button + Checkbox cùng phong cách.
#include <iostream>
#include <memory>
// ===== Abstract Products =====
class Button { public: virtual ~Button() = default; virtual void paint() const = 0; };
class Checkbox { public: virtual ~Checkbox() = default; virtual void paint() const = 0; };
// ===== Họ Light =====
class LightButton : public Button {
public: void paint() const override { std::cout << "[ Nút nền trắng, chữ đen ]\n"; }
};
class LightCheckbox : public Checkbox {
public: void paint() const override { std::cout << "[x] Checkbox viền xám nhạt\n"; }
};
// ===== Họ Dark =====
class DarkButton : public Button {
public: void paint() const override { std::cout << "[ Nút nền đen, chữ trắng ]\n"; }
};
class DarkCheckbox : public Checkbox {
public: void paint() const override { std::cout << "[x] Checkbox viền trắng phát sáng\n"; }
};
// ===== Abstract Factory =====
class UIFactory {
public:
virtual ~UIFactory() = default;
virtual std::unique_ptr<Button> createButton() const = 0;
virtual std::unique_ptr<Checkbox> createCheckbox() const = 0;
};
// ===== Concrete Factories =====
class LightThemeFactory : public UIFactory {
public:
std::unique_ptr<Button> createButton() const override { return std::make_unique<LightButton>(); }
std::unique_ptr<Checkbox> createCheckbox() const override { return std::make_unique<LightCheckbox>(); }
};
class DarkThemeFactory : public UIFactory {
public:
std::unique_ptr<Button> createButton() const override { return std::make_unique<DarkButton>(); }
std::unique_ptr<Checkbox> createCheckbox() const override { return std::make_unique<DarkCheckbox>(); }
};
// Client – chỉ biết interface
void render(const UIFactory& factory) {
auto button = factory.createButton();
auto checkbox = factory.createCheckbox();
button->paint();
checkbox->paint();
}
int main() {
// Thực tế: chọn factory theo chân cấu hình phần cứng hoặc macro lúc biên dịch
LightThemeFactory light;
DarkThemeFactory dark;
render(light);
std::cout << "-----\n";
render(dark);
}
g++ -std=c++17 03-abstract-factory.cpp -o app && ./app[ Nút nền trắng, chữ đen ] [x] Checkbox viền xám nhạt ----- [ Nút nền đen, chữ trắng ] [x] Checkbox viền trắng phát sáng
Giải thích code C++17
- Các hàm tạo đều
constvà trả vềstd::unique_ptrcủa interface tương ứng. render(const UIFactory&)nhận tham chiếu — không cần biết factory cụ thể, không cấp phát cho factory.- Đổi họ sản phẩm chỉ bằng việc truyền đối tượng factory khác.
"""Abstract Factory – Python 3."""
from abc import ABC, abstractmethod
# ===== Abstract Products =====
class Button(ABC):
@abstractmethod
def paint(self): ...
class Checkbox(ABC):
@abstractmethod
def paint(self): ...
# ===== Họ Light =====
class LightButton(Button):
def paint(self): print("[ Nút nền trắng, chữ đen ]")
class LightCheckbox(Checkbox):
def paint(self): print("[x] Checkbox viền xám nhạt")
# ===== Họ Dark =====
class DarkButton(Button):
def paint(self): print("[ Nút nền đen, chữ trắng ]")
class DarkCheckbox(Checkbox):
def paint(self): print("[x] Checkbox viền trắng phát sáng")
# ===== Abstract Factory =====
class UIFactory(ABC):
@abstractmethod
def create_button(self) -> Button: ...
@abstractmethod
def create_checkbox(self) -> Checkbox: ...
class LightThemeFactory(UIFactory):
def create_button(self): return LightButton()
def create_checkbox(self): return LightCheckbox()
class DarkThemeFactory(UIFactory):
def create_button(self): return DarkButton()
def create_checkbox(self): return DarkCheckbox()
def render(factory: UIFactory) -> None:
"""Client – chỉ biết interface."""
factory.create_button().paint()
factory.create_checkbox().paint()
if __name__ == "__main__":
theme = {"light": LightThemeFactory, "dark": DarkThemeFactory}
render(theme["light"]())
print("-----")
render(theme["dark"]())
python 03-abstract-factory.py[ Nút nền trắng, chữ đen ] [x] Checkbox viền xám nhạt ----- [ Nút nền đen, chữ trắng ] [x] Checkbox viền trắng phát sáng
Giải thích code Python 3
- Factory được chọn qua
dicttheo tên chủ đề — dễ đọc từ file cấu hình JSON. - Python dùng duck typing nên không bắt buộc ABC, nhưng ABC giúp phát hiện sớm lớp thiếu phương thức.
public class AbstractFactoryDemo {
public static void main(String[] args) {
// Chọn "họ" sản phẩm một lần duy nhất ở đây (có thể đọc từ cấu hình)
render(new LightThemeFactory());
System.out.println("-----");
render(new DarkThemeFactory());
}
/** Client: chỉ biết các interface, không biết lớp cụ thể nào. */
static void render(UIFactory factory) {
Button button = factory.createButton();
Checkbox checkbox = factory.createCheckbox();
button.paint();
checkbox.paint();
}
}
// ===== Abstract Products =====
interface Button { void paint(); }
interface Checkbox { void paint(); }
// ===== Concrete Products – họ Light =====
class LightButton implements Button {
public void paint() { System.out.println("[ Nút nền trắng, chữ đen ]"); }
}
class LightCheckbox implements Checkbox {
public void paint() { System.out.println("[x] Checkbox viền xám nhạt"); }
}
// ===== Concrete Products – họ Dark =====
class DarkButton implements Button {
public void paint() { System.out.println("[ Nút nền đen, chữ trắng ]"); }
}
class DarkCheckbox implements Checkbox {
public void paint() { System.out.println("[x] Checkbox viền trắng phát sáng"); }
}
// ===== Abstract Factory: tạo ra cả một HỌ sản phẩm liên quan =====
interface UIFactory {
Button createButton();
Checkbox createCheckbox();
}
// ===== Concrete Factories: mỗi factory đảm bảo các sản phẩm "hợp tông" =====
class LightThemeFactory implements UIFactory {
public Button createButton() { return new LightButton(); }
public Checkbox createCheckbox() { return new LightCheckbox(); }
}
class DarkThemeFactory implements UIFactory {
public Button createButton() { return new DarkButton(); }
public Checkbox createCheckbox() { return new DarkCheckbox(); }
}
java 03-abstract-factory.java[ Nút nền trắng, chữ đen ] [x] Checkbox viền xám nhạt ----- [ Nút nền đen, chữ trắng ] [x] Checkbox viền trắng phát sáng
Giải thích code Java 17+
- Hàm
render(UIFactory)không có một chữ “Light” hay “Dark” nào — nó hoàn toàn độc lập với giao diện cụ thể. - Việc chọn họ sản phẩm chỉ diễn ra một lần trong
main(thực tế: đọc từ file cấu hình hoặc cài đặt hệ điều hành). - Muốn thêm giao diện High Contrast: thêm 1 factory + 2 sản phẩm, không sửa client.
6Áp dụng trong lập trình nhúng
- Một firmware chạy trên nhiều board:
Esp32FactorytạoEsp32Gpio+Esp32Adc+Esp32Pwm;Stm32Factorytạo bộ tương ứng — các thành phần luôn cùng một nền tảng. - Đây chính là ý tưởng của tầng HAL (Hardware Abstraction Layer).
- Nếu board được chọn lúc biên dịch, có thể dùng
using Factory = Esp32Factory;+ template để không tốn hàm ảo.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Đảm bảo các sản phẩm từ cùng một factory luôn tương thích với nhau.
- Client tách biệt hoàn toàn khỏi lớp cụ thể.
- Đổi cả họ sản phẩm chỉ bằng việc đổi một đối tượng factory.
✘ Nhược điểm
- Thêm một loại sản phẩm mới (ví dụ Slider) phải sửa interface factory và mọi concrete factory.
- Nhiều interface và lớp mới → code phức tạp hơn.
8Khi nào nên dùng
- Hệ thống cần làm việc với nhiều họ sản phẩm liên quan và cần đảm bảo tính nhất quán giữa chúng.
- Muốn cung cấp thư viện sản phẩm mà chỉ lộ ra interface, không lộ cài đặt.
- Hỗ trợ đa nền tảng: Windows/macOS, MySQL/PostgreSQL…
9Gặp ở đâu trong thực tế
C++ / nhúng
- STM32 HAL / CMSIS-Driver (mỗi dòng chip một bộ driver cùng interface)
- Qt: các plugin nền tảng (QPA)
- Thư viện đồ hoạ LVGL với nhiều driver màn hình
Python
sqlite3/psycopg2theo chuẩn DB-API:connect()→cursor()cùng driver- Tkinter
ttkthemes
Java
javax.xml.parsers.DocumentBuilderFactoryjavax.xml.transform.TransformerFactory- JDBC:
Connectiontạo raStatement,PreparedStatementcùng một driver
10Liên hệ với các mẫu khác
Concrete Factory thường là Singleton. Có thể cài đặt bằng Factory Method hoặc Prototype. Abstract Factory có thể thay Facade khi chỉ cần che giấu cách tạo đối tượng của hệ thống con.
11Bài tập thực hành
🧱 Builder — Người xây dựng
Mục đích: Tách quá trình xây dựng một đối tượng phức tạp ra khỏi biểu diễn của nó, để cùng một quá trình có thể tạo ra nhiều biểu diễn khác nhau — xây từng bước một.
1Vấn đề cần giải quyết
Lớp Robot có 2 thông số bắt buộc (tên, bo mạch) và nhiều thông số tuỳ chọn (số bánh, pin, bluetooth, danh sách cảm biến…). Các cách làm thông thường đều có vấn đề:
- Telescoping constructor:
new Robot("BK", "ESP32", 4, 3000, true, null, null, false)— không ai nhớ tham số thứ 5 là gì, dễ truyền nhầm thứ tự. - Constructor rỗng + nhiều setter: đối tượng ở trạng thái “dở dang” giữa các lần gọi setter, và không thể làm đối tượng bất biến (immutable).
2Giải pháp
Đưa việc xây dựng ra một lớp riêng — Builder. Builder có các phương thức đặt từng thuộc tính, mỗi phương thức return this để gọi nối tiếp (fluent interface). Cuối cùng gọi build() để kiểm tra dữ liệu và tạo đối tượng hoàn chỉnh.
Tuỳ chọn: lớp Director đóng gói các “công thức” xây dựng thường dùng (robot dò line, robot sumo…) để tái sử dụng.
Robot::Builder, các hàm trả về Builder& để gọi nối tiếp. Firmware hay tắt exception (-fno-exceptions) nên build() trả về std::optional thay vì throw.Python: tham số đặt tên + giá trị mặc định đã giải quyết phần lớn vấn đề; Builder vẫn hữu ích khi cần kiểm tra ràng buộc và xây từng bước. Java: static nested class hoặc Lombok
@Builder.3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Builder (Robot.Builder) | Cung cấp các bước xây dựng và phương thức build(). |
| Product (Robot) | Đối tượng phức tạp được tạo ra, thường immutable. |
| Director (RobotDirector) | Biết trình tự gọi các bước để tạo cấu hình chuẩn (không bắt buộc). |
| Client | Tạo Builder, gọi các bước, nhận Product. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Builder – C++17
#include <iostream>
#include <optional>
#include <string>
#include <vector>
class Robot {
public:
class Builder; // khai báo trước lớp lồng
void print() const {
std::cout << "Robot " << name_ << " | mạch=" << board_ << " | bánh=" << wheels_
<< " | pin=" << batteryMah_ << "mAh | BT=" << (bluetooth_ ? "có" : "không") << " | cảm biến=[";
for (size_t i = 0; i < sensors_.size(); ++i) std::cout << (i ? ", " : "") << sensors_[i];
std::cout << "]\n";
}
private:
Robot() = default; // chỉ Builder mới tạo được Robot
std::string name_, board_;
int wheels_ = 2, batteryMah_ = 1000;
bool bluetooth_ = false;
std::vector<std::string> sensors_;
};
class Robot::Builder {
public:
Builder(std::string name, std::string board) { // tham số bắt buộc
r_.name_ = std::move(name);
r_.board_ = std::move(board);
}
Builder& wheels(int n) { r_.wheels_ = n; return *this; }
Builder& battery(int mAh) { r_.batteryMah_ = mAh; return *this; }
Builder& bluetooth(bool on) { r_.bluetooth_ = on; return *this; }
Builder& addSensor(std::string s) { r_.sensors_.push_back(std::move(s)); return *this; }
// Firmware nhúng thường TẮT exceptions (-fno-exceptions) nên trả về std::optional
// thay cho throw: rỗng nghĩa là cấu hình không hợp lệ.
std::optional<Robot> build() const {
if (r_.wheels_ != 2 && r_.wheels_ != 4) return std::nullopt;
return r_;
}
private:
Robot r_;
};
// Director – đóng gói các "công thức" lắp ráp
class RobotDirector {
public:
std::optional<Robot> makeLineFollower() const {
return Robot::Builder("LINE", "Arduino Nano").addSensor("Dò line x5").battery(1200).build();
}
std::optional<Robot> makeSumoRobot() const {
return Robot::Builder("SUMO", "STM32").wheels(4).addSensor("Hồng ngoại x4").battery(2200).build();
}
};
int main() {
auto custom = Robot::Builder("BK-01", "ESP32")
.wheels(4)
.addSensor("Siêu âm HC-SR04")
.addSensor("Dò line TCRT5000")
.battery(3000)
.bluetooth(true)
.build();
if (custom) custom->print();
RobotDirector director;
director.makeLineFollower()->print();
director.makeSumoRobot()->print();
auto bad = Robot::Builder("LOI", "Arduino Uno").wheels(3).build();
std::cout << "Robot 3 bánh hợp lệ? " << (bad ? "có" : "không – build() trả về rỗng") << "\n";
}
g++ -std=c++17 04-builder.cpp -o app && ./appRobot BK-01 | mạch=ESP32 | bánh=4 | pin=3000mAh | BT=có | cảm biến=[Siêu âm HC-SR04, Dò line TCRT5000] Robot LINE | mạch=Arduino Nano | bánh=2 | pin=1200mAh | BT=không | cảm biến=[Dò line x5] Robot SUMO | mạch=STM32 | bánh=4 | pin=2200mAh | BT=không | cảm biến=[Hồng ngoại x4] Robot 3 bánh hợp lệ? không – build() trả về rỗng
Giải thích code C++17
- Constructor của
Robotlàprivate; lớp lồngRobot::Builderđược truy cập vì lớp lồng có quyền với thành viên private của lớp bao. - Mỗi hàm trả về
Builder&→ gọi nối tiếp.wheels(4).addSensor(...). build()trả vềstd::optional<Robot>: rỗng khi cấu hình sai — phù hợp firmware biên dịch với-fno-exceptions.std::movetránh sao chép chuỗi thừa.
"""Builder – Python 3.
Python có tham số đặt tên + giá trị mặc định nên "telescoping constructor" ít xảy ra.
Builder vẫn hữu ích khi cần xây từng bước, kiểm tra ràng buộc, hoặc có Director dùng lại công thức."""
from dataclasses import dataclass, field
@dataclass(frozen=True) # frozen=True -> đối tượng bất biến
class Robot:
name: str
board: str
wheels: int = 2
battery_mah: int = 1000
bluetooth: bool = False
sensors: tuple = field(default_factory=tuple)
def __str__(self):
return (f"Robot {self.name:<6}| mạch={self.board:<12}| bánh={self.wheels} | "
f"pin={self.battery_mah}mAh | BT={str(self.bluetooth):<5}| cảm biến={list(self.sensors)}")
class RobotBuilder:
def __init__(self, name: str, board: str):
self._name, self._board = name, board
self._wheels, self._battery, self._bt = 2, 1000, False
self._sensors = []
def wheels(self, n): self._wheels = n; return self # return self -> gọi nối tiếp
def battery(self, mah): self._battery = mah; return self
def bluetooth(self, on): self._bt = on; return self
def add_sensor(self, s): self._sensors.append(s); return self
def build(self) -> Robot:
if self._wheels not in (2, 4):
raise ValueError(f"Robot chỉ hỗ trợ 2 hoặc 4 bánh, nhận được {self._wheels}")
return Robot(self._name, self._board, self._wheels, self._battery, self._bt, tuple(self._sensors))
class RobotDirector:
def make_line_follower(self):
return RobotBuilder("LINE", "Arduino Nano").add_sensor("Dò line x5").battery(1200).build()
def make_sumo_robot(self):
return RobotBuilder("SUMO", "STM32").wheels(4).add_sensor("Hồng ngoại x4").battery(2200).build()
if __name__ == "__main__":
custom = (RobotBuilder("BK-01", "ESP32")
.wheels(4)
.add_sensor("Siêu âm HC-SR04")
.add_sensor("Dò line TCRT5000")
.battery(3000)
.bluetooth(True)
.build())
print(custom)
director = RobotDirector()
print(director.make_line_follower())
print(director.make_sumo_robot())
try:
RobotBuilder("LOI", "Arduino Uno").wheels(3).build()
except ValueError as e:
print("Lỗi:", e)
python 04-builder.pyRobot BK-01 | mạch=ESP32 | bánh=4 | pin=3000mAh | BT=True | cảm biến=['Siêu âm HC-SR04', 'Dò line TCRT5000'] Robot LINE | mạch=Arduino Nano| bánh=2 | pin=1200mAh | BT=False| cảm biến=['Dò line x5'] Robot SUMO | mạch=STM32 | bánh=4 | pin=2200mAh | BT=False| cảm biến=['Hồng ngoại x4'] Lỗi: Robot chỉ hỗ trợ 2 hoặc 4 bánh, nhận được 3
Giải thích code Python 3
@dataclass(frozen=True)tạo lớp sản phẩm bất biến chỉ với vài dòng.- Mỗi phương thức
return selfđể gọi nối tiếp, đặt cả biểu thức trong ngoặc để xuống dòng. build()némValueErrorkhi cấu hình sai.
import java.util.ArrayList;
import java.util.List;
public class BuilderDemo {
public static void main(String[] args) {
// 1. Dùng Builder trực tiếp với "fluent API" – dễ đọc, không cần nhớ thứ tự tham số
Robot custom = new Robot.Builder("BK-01", "ESP32")
.wheels(4)
.addSensor("Siêu âm HC-SR04")
.addSensor("Dò line TCRT5000")
.battery(3000)
.bluetooth(true)
.build();
System.out.println(custom);
// 2. Dùng Director để đóng gói các "công thức" lắp ráp hay dùng
RobotDirector director = new RobotDirector();
System.out.println(director.makeLineFollower());
System.out.println(director.makeSumoRobot());
// 3. Builder kiểm tra dữ liệu trước khi tạo đối tượng
try {
new Robot.Builder("LOI", "Arduino Uno").wheels(3).build();
} catch (IllegalStateException e) {
System.out.println("Lỗi: " + e.getMessage());
}
}
}
/** Product – bất biến (immutable): mọi field là final, không có setter. */
final class Robot {
private final String name; // bắt buộc
private final String board; // bắt buộc
private final int wheels; // tuỳ chọn
private final int batteryMah; // tuỳ chọn
private final boolean bluetooth; // tuỳ chọn
private final List<String> sensors; // tuỳ chọn
private Robot(Builder b) { // chỉ Builder mới gọi được
this.name = b.name;
this.board = b.board;
this.wheels = b.wheels;
this.batteryMah = b.batteryMah;
this.bluetooth = b.bluetooth;
this.sensors = List.copyOf(b.sensors);
}
@Override
public String toString() {
return String.format("Robot %-6s| mạch=%-12s| bánh=%d | pin=%dmAh | BT=%-5s| cảm biến=%s",
name, board, wheels, batteryMah, bluetooth, sensors);
}
/** Builder – static nested class. */
public static class Builder {
private final String name;
private final String board;
private int wheels = 2;
private int batteryMah = 1000;
private boolean bluetooth = false;
private final List<String> sensors = new ArrayList<>();
public Builder(String name, String board) { // tham số bắt buộc đưa vào constructor
this.name = name;
this.board = board;
}
public Builder wheels(int n) { this.wheels = n; return this; }
public Builder battery(int mAh) { this.batteryMah = mAh; return this; }
public Builder bluetooth(boolean on) { this.bluetooth = on; return this; }
public Builder addSensor(String s) { this.sensors.add(s); return this; }
public Robot build() {
if (wheels != 2 && wheels != 4)
throw new IllegalStateException("Robot chỉ hỗ trợ 2 hoặc 4 bánh, nhận được " + wheels);
return new Robot(this);
}
}
}
/** Director – biết TRÌNH TỰ các bước để tạo ra những cấu hình chuẩn. */
class RobotDirector {
Robot makeLineFollower() {
return new Robot.Builder("LINE", "Arduino Nano")
.addSensor("Dò line x5")
.battery(1200)
.build();
}
Robot makeSumoRobot() {
return new Robot.Builder("SUMO", "STM32")
.wheels(4)
.addSensor("Hồng ngoại x4")
.battery(2200)
.build();
}
}
java 04-builder.javaRobot BK-01 | mạch=ESP32 | bánh=4 | pin=3000mAh | BT=true | cảm biến=[Siêu âm HC-SR04, Dò line TCRT5000] Robot LINE | mạch=Arduino Nano| bánh=2 | pin=1200mAh | BT=false| cảm biến=[Dò line x5] Robot SUMO | mạch=STM32 | bánh=4 | pin=2200mAh | BT=false| cảm biến=[Hồng ngoại x4] Lỗi: Robot chỉ hỗ trợ 2 hoặc 4 bánh, nhận được 3
Giải thích code Java 17+
- Constructor của
Robotlàprivatevà nhận vàoBuilder→ chỉ có thể tạo Robot thông qua Builder. - Mọi field của Robot là
final, danh sách cảm biến được sao chép bằngList.copyOf→ Robot là immutable, an toàn đa luồng. - Tham số bắt buộc đưa vào constructor của Builder; tham số tuỳ chọn có giá trị mặc định.
build()là nơi lý tưởng để kiểm tra ràng buộc (ví dụ chỉ 2 hoặc 4 bánh) trước khi đối tượng ra đời.
6Áp dụng trong lập trình nhúng
- Cấu hình ngoại vi có rất nhiều tham số: UART (baud, parity, stop bit, chân TX/RX), SPI, timer PWM… Builder giúp code cấu hình dễ đọc và kiểm tra hợp lệ trước khi ghi vào thanh ghi.
- Xây gói tin giao thức (Modbus, MQTT, CAN) từng trường một rồi
build()tính checksum. - ESP-IDF dùng struct cấu hình + designated initializer (
.baud_rate = 115200) — một dạng “builder” kiểu C.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Code tạo đối tượng dễ đọc, tự mô tả.
- Tạo được đối tượng immutable với nhiều tham số tuỳ chọn.
- Kiểm tra dữ liệu tập trung trước khi tạo.
- Tái sử dụng quy trình xây dựng cho nhiều biểu diễn (qua Director).
✘ Nhược điểm
- Phải viết thêm một lớp Builder (trùng lặp các field).
- Không đáng dùng với lớp chỉ có 2–3 thuộc tính.
8Khi nào nên dùng
- Constructor có nhiều tham số (thường từ 4 trở lên), nhất là tham số tuỳ chọn.
- Cần tạo đối tượng immutable.
- Cần tạo các biểu diễn khác nhau của cùng một sản phẩm theo cùng quy trình (ví dụ xuất tài liệu ra HTML/PDF/Markdown).
9Gặp ở đâu trong thực tế
C++ / nhúng
std::stringstream- ESP-IDF:
uart_config_t,wifi_config_t - Thư viện MQTT
PubSubClientdựng gói CONNECT/PUBLISH
Python
argparse.ArgumentParserthêm từng đối số- SQLAlchemy
select().where().order_by() email.message.EmailMessage
Java
StringBuilder/StringBufferjava.net.http.HttpRequest.newBuilder()Stream.builder(),Locale.Builder- Lombok
@Builder, OkHttpRequest.Builder
10Liên hệ với các mẫu khác
Builder tập trung vào xây dựng từng bước; Abstract Factory tập trung vào họ sản phẩm và trả về ngay. Builder thường dùng để tạo cây Composite phức tạp.
11Bài tập thực hành
🧬 Prototype — Nguyên mẫu
Mục đích: Tạo đối tượng mới bằng cách sao chép (clone) một đối tượng có sẵn — bản mẫu — thay vì khởi tạo từ đầu.
1Vấn đề cần giải quyết
Bạn muốn tạo một bản sao y hệt một đối tượng. Cách “ngây thơ” là tạo đối tượng mới rồi chép từng field sang. Nhưng:
- Nhiều field là
private— từ bên ngoài không đọc được. - Code sao chép phụ thuộc vào lớp cụ thể của đối tượng, trong khi đôi khi ta chỉ biết nó qua interface.
- Việc khởi tạo có thể rất tốn kém (đọc file, truy vấn CSDL, tính toán nặng) — làm lại nhiều lần là lãng phí.
2Giải pháp
Giao trách nhiệm sao chép cho chính đối tượng đó: khai báo interface chung có phương thức clone(). Đối tượng tự biết cách sao chép mọi field của nó (kể cả private).
Có thể kết hợp Prototype Registry: một kho lưu sẵn các bản mẫu đã cấu hình, cần loại nào thì lấy ra và clone.
• Shallow: chỉ chép tham chiếu → bản sao và bản gốc dùng chung các đối tượng con (như List). Sửa list của bản sao sẽ làm hỏng bản gốc!
• Deep: tạo bản sao mới cho cả các đối tượng con. Ví dụ dùng
new ArrayList<>(src.questions) để deep copy.| Ngôn ngữ | Công cụ có sẵn | Lưu ý |
|---|---|---|
| C++ | Copy constructor; hàm ảo clone() trả về std::unique_ptr<Base> | Copy mặc định sao chép sâu với std::vector, std::string… nhưng nông với con trỏ thô → dễ double-free (quy tắc 3/5). |
| Python | copy.copy() (nông), copy.deepcopy() (sâu) | Có thể tuỳ biến bằng __copy__/__deepcopy__. |
| Java | Cloneable + Object.clone() | Nhiều cạm bẫy; nên dùng copy constructor. |
3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Prototype | Interface khai báo phương thức clone(). |
| ConcretePrototype (ExamPaper, Circle) | Cài đặt clone() — tự sao chép chính mình. |
| Registry (ShapeRegistry) | Lưu các bản mẫu dựng sẵn, trả về bản sao theo khoá (không bắt buộc). |
| Client | Gọi clone() thay vì new. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Prototype – C++17
#include <iostream>
#include <map>
#include <memory>
#include <string>
#include <vector>
// ===== Ví dụ 1: ExamPaper – copy constructor của C++ đã là deep copy cho std::vector =====
class ExamPaper {
public:
explicit ExamPaper(std::string title) : title_(std::move(title)) {}
ExamPaper clone() const { return *this; } // gọi copy constructor mặc định
void setCode(std::string c) { code_ = std::move(c); }
void addQuestion(std::string q){ questions_.push_back(std::move(q)); }
const std::vector<std::string>* questionsPtr() const { return &questions_; }
void print() const {
std::cout << code_ << " (" << title_ << "): " << questions_.size() << " câu\n";
}
private:
std::string title_, code_ = "Đề gốc";
std::vector<std::string> questions_; // vector có copy constructor sao chép từng phần tử
};
// ===== Ví dụ 2: Shape – clone qua con trỏ lớp cha ("virtual constructor") =====
class Shape {
public:
virtual ~Shape() = default;
virtual std::unique_ptr<Shape> clone() const = 0; // C++ không có virtual constructor -> dùng clone()
void move(int dx, int dy) { x_ += dx; y_ += dy; }
virtual void print() const = 0;
protected:
int x_ = 0, y_ = 0;
std::string color_;
};
class Circle : public Shape {
public:
Circle(int r, std::string color) : radius_(r) { color_ = std::move(color); }
std::unique_ptr<Shape> clone() const override { return std::make_unique<Circle>(*this); }
void print() const override {
std::cout << "Circle(r=" << radius_ << ", " << color_ << ", @" << x_ << "," << y_ << ")";
}
private:
int radius_;
};
class ShapeRegistry {
public:
ShapeRegistry() {
prototypes_["red-circle"] = std::make_unique<Circle>(10, "đỏ");
prototypes_["big-blue-circle"] = std::make_unique<Circle>(40, "xanh");
}
std::unique_ptr<Shape> get(const std::string& key) const { return prototypes_.at(key)->clone(); }
private:
std::map<std::string, std::unique_ptr<Shape>> prototypes_;
};
// ===== Cạm bẫy: con trỏ thô (raw pointer) -> copy mặc định là SHALLOW =====
struct SensorBuffer {
int* data; // trỏ tới mảng cấp phát động
explicit SensorBuffer(int n) : data(new int[n]{}) {}
// Không tự viết copy constructor => bản sao dùng CHUNG mảng data (nguy hiểm, double-free!)
};
int main() {
ExamPaper tpl("Đề kiểm tra C++ – 45 phút");
tpl.addQuestion("Singleton là gì?");
tpl.addQuestion("Phân biệt Factory Method và Abstract Factory.");
ExamPaper deA = tpl.clone();
deA.setCode("Mã đề 101");
deA.addQuestion("Viết Builder cho lớp Student.");
tpl.print();
deA.print();
std::cout << "deA dùng chung vector với đề gốc? " << std::boolalpha
<< (deA.questionsPtr() == tpl.questionsPtr()) << "\n";
ShapeRegistry registry;
auto c1 = registry.get("red-circle");
auto c2 = registry.get("red-circle");
c2->move(50, 50);
c1->print(); std::cout << " | "; c2->print();
std::cout << " | cùng đối tượng? " << (c1.get() == c2.get()) << "\n";
SensorBuffer a(4);
SensorBuffer b = a; // shallow copy
b.data[0] = 99;
std::cout << "Shallow copy: sửa b.data[0] làm a.data[0] = " << a.data[0] << "\n";
delete[] a.data; // chỉ xoá 1 lần vì dùng chung
}
g++ -std=c++17 05-prototype.cpp -o app && ./appĐề gốc (Đề kiểm tra C++ – 45 phút): 2 câu Mã đề 101 (Đề kiểm tra C++ – 45 phút): 3 câu deA dùng chung vector với đề gốc? false Circle(r=10, đỏ, @0,0) | Circle(r=10, đỏ, @50,50) | cùng đối tượng? false Shallow copy: sửa b.data[0] làm a.data[0] = 99
Giải thích code C++17
ExamPaper::clone()chỉ cầnreturn *this;— copy constructor mặc định đã sao chép sâustd::vectorvàstd::string.- Với cây kế thừa, C++ không có “virtual constructor” nên dùng hàm ảo
clone()trả vềstd::unique_ptr<Shape>— đây chính là Prototype. SensorBufferminh hoạ cạm bẫy: với con trỏ thô, copy mặc định chỉ chép địa chỉ → hai đối tượng dùng chung mảng (sửablàm hỏnga, và nguy cơ double-free).
"""Prototype – Python 3. Module copy có sẵn: copy.copy (shallow) và copy.deepcopy (deep)."""
import copy
class ExamPaper:
def __init__(self, title):
self.title = title
self.code = "Đề gốc"
self.questions = []
def clone(self):
return copy.deepcopy(self) # sao chép sâu: list questions cũng được nhân bản
def __str__(self):
return f"{self.code} ({self.title}): {len(self.questions)} câu"
class Circle:
def __init__(self, radius, color):
self.radius, self.color, self.x, self.y = radius, color, 0, 0
def clone(self):
return copy.copy(self) # chỉ có field kiểu bất biến -> shallow là đủ
def move(self, dx, dy):
self.x += dx; self.y += dy
def __str__(self):
return f"Circle(r={self.radius}, {self.color}, @{self.x},{self.y})"
class ShapeRegistry:
def __init__(self):
self._prototypes = {"red-circle": Circle(10, "đỏ"), "big-blue-circle": Circle(40, "xanh")}
def get(self, key):
return self._prototypes[key].clone()
if __name__ == "__main__":
template = ExamPaper("Đề kiểm tra Python – 45 phút")
template.questions += ["Singleton là gì?", "Phân biệt Factory Method và Abstract Factory."]
de_a = template.clone()
de_a.code = "Mã đề 101"
de_a.questions.append("Viết Builder cho lớp Student.")
print(template)
print(de_a)
print("Dùng chung list câu hỏi?", de_a.questions is template.questions)
# Cạm bẫy shallow copy
de_b = copy.copy(template)
de_b.questions.append("Câu hỏi thêm vào bản shallow copy")
print("Sau khi sửa bản shallow, đề gốc có:", len(template.questions), "câu <- bị ảnh hưởng!")
reg = ShapeRegistry()
c1, c2 = reg.get("red-circle"), reg.get("red-circle")
c2.move(50, 50)
print(c1, " | ", c2, " | cùng đối tượng?", c1 is c2)
python 05-prototype.pyĐề gốc (Đề kiểm tra Python – 45 phút): 2 câu Mã đề 101 (Đề kiểm tra Python – 45 phút): 3 câu Dùng chung list câu hỏi? False Sau khi sửa bản shallow, đề gốc có: 3 câu <- bị ảnh hưởng! Circle(r=10, đỏ, @0,0) | Circle(r=10, đỏ, @50,50) | cùng đối tượng? False
Giải thích code Python 3
copy.deepcopysao chép sâu toàn bộ đồ thị đối tượng;copy.copychỉ sao chép nông.- Ví dụ cho thấy rõ: bản shallow copy thêm câu hỏi làm đề gốc cũng tăng lên 3 câu.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class PrototypeDemo {
public static void main(String[] args) {
// 1. Tạo "bản mẫu" một lần (giả sử việc tạo rất tốn kém: đọc file, query DB...)
ExamPaper template = new ExamPaper("Đề kiểm tra Java – 45 phút");
template.addQuestion("Singleton là gì?");
template.addQuestion("Phân biệt Factory Method và Abstract Factory.");
// 2. Clone ra các đề con rồi chỉnh riêng từng đề
ExamPaper deA = template.clone();
deA.setCode("Mã đề 101");
deA.addQuestion("Viết Builder cho lớp Student.");
ExamPaper deB = template.clone();
deB.setCode("Mã đề 102");
deB.addQuestion("Vẽ UML của Prototype.");
System.out.println(template);
System.out.println(deA);
System.out.println(deB);
System.out.println("deA.questions == template.questions ? " + deA.sameQuestionsAs(template));
// 3. Prototype Registry: lưu sẵn các bản mẫu, cần thì clone theo tên
ShapeRegistry registry = new ShapeRegistry();
Shape c1 = registry.get("red-circle");
Shape c2 = registry.get("red-circle");
c2.move(50, 50);
System.out.println(c1 + " | " + c2 + " | cùng đối tượng? " + (c1 == c2));
}
}
/** Prototype interface của Java chính là Cloneable + clone(). Ở đây tự định nghĩa cho rõ. */
interface Prototype<T> { T clone(); }
class ExamPaper implements Prototype<ExamPaper> {
private final String title;
private String code = "Đề gốc";
private List<String> questions = new ArrayList<>();
ExamPaper(String title) { this.title = title; }
/** Copy constructor dùng cho clone – sao chép SÂU (deep copy) danh sách câu hỏi. */
private ExamPaper(ExamPaper src) {
this.title = src.title;
this.code = src.code;
this.questions = new ArrayList<>(src.questions); // nếu gán thẳng src.questions => shallow copy, các đề sẽ dùng chung list!
}
@Override public ExamPaper clone() { return new ExamPaper(this); }
void setCode(String code) { this.code = code; }
void addQuestion(String q) { questions.add(q); }
boolean sameQuestionsAs(ExamPaper o) { return this.questions == o.questions; }
@Override public String toString() {
return code + " (" + title + "): " + questions.size() + " câu " + questions;
}
}
abstract class Shape implements Prototype<Shape> {
protected int x, y;
protected String color;
void move(int dx, int dy) { x += dx; y += dy; }
@Override public abstract Shape clone(); // ghi đè Object.clone() (protected) thành public
}
class Circle extends Shape {
int radius;
Circle(int radius, String color) { this.radius = radius; this.color = color; }
private Circle(Circle s) { this.x = s.x; this.y = s.y; this.color = s.color; this.radius = s.radius; }
@Override public Circle clone() { return new Circle(this); }
@Override public String toString() { return "Circle(r=" + radius + ", " + color + ", @" + x + "," + y + ")"; }
}
class ShapeRegistry {
private final Map<String, Shape> prototypes = new HashMap<>();
ShapeRegistry() {
prototypes.put("red-circle", new Circle(10, "đỏ"));
prototypes.put("big-blue-circle", new Circle(40, "xanh"));
}
Shape get(String key) { return prototypes.get(key).clone(); } // luôn trả về BẢN SAO
}
java 05-prototype.javaĐề gốc (Đề kiểm tra Java – 45 phút): 2 câu [Singleton là gì?, Phân biệt Factory Method và Abstract Factory.] Mã đề 101 (Đề kiểm tra Java – 45 phút): 3 câu [Singleton là gì?, Phân biệt Factory Method và Abstract Factory., Viết Builder cho lớp Student.] Mã đề 102 (Đề kiểm tra Java – 45 phút): 3 câu [Singleton là gì?, Phân biệt Factory Method và Abstract Factory., Vẽ UML của Prototype.] deA.questions == template.questions ? false Circle(r=10, đỏ, @0,0) | Circle(r=10, đỏ, @50,50) | cùng đối tượng? false
Giải thích code Java 17+
ExamPaperdùng copy constructor private để clone — truy cập được mọi field private.- Dòng
new ArrayList<>(src.questions)đảm bảo deep copy: thêm câu hỏi vào đề 101 không ảnh hưởng đề gốc (kết quảfalse). ShapeRegistry.get()luôn trả về bản sao mới — di chuyểnc2không làmc1thay đổi.- Trong
Shapephải khai báo lạipublic abstract Shape clone()vìObject.clone()làprotected.
6Áp dụng trong lập trình nhúng
- Lưu sẵn “cấu hình mặc định” của thiết bị (ngưỡng, hệ số hiệu chuẩn) rồi clone ra để chỉnh cho từng kênh cảm biến.
- Clone gói tin mẫu (header cố định) rồi chỉ điền payload — nhanh hơn dựng lại từ đầu.
- Trên MCU: tránh clone ra heap; sao chép vào biến/stack hoặc vào slot của Object Pool.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Sao chép đối tượng mà không phụ thuộc vào lớp cụ thể.
- Tránh lặp lại quá trình khởi tạo tốn kém.
- Tạo đối tượng phức tạp tiện lợi hơn từ các bản mẫu dựng sẵn.
- Có thể thay thế việc tạo nhiều lớp con chỉ khác nhau về cấu hình.
✘ Nhược điểm
- Clone đối tượng có tham chiếu vòng (A → B → A) rất phức tạp.
- Dễ nhầm lẫn shallow/deep copy gây lỗi khó phát hiện.
8Khi nào nên dùng
- Chi phí tạo mới cao hơn nhiều so với sao chép.
- Cần nhiều đối tượng chỉ khác nhau một chút về trạng thái.
- Code không nên phụ thuộc vào lớp cụ thể của đối tượng cần sao chép.
9Gặp ở đâu trong thực tế
C++ / nhúng
- Copy constructor của mọi container STL
- Qt:
QObjectcố ý KHÔNG cho copy — ví dụ khi Prototype không phù hợp
Python
- Module
copy dict.copy(),list.copy()(nông)
Java
java.lang.Object#clone()và interfaceCloneableArrayList.clone(),HashMap.clone()(shallow copy)- Spring bean với
scope="prototype"
10Liên hệ với các mẫu khác
Prototype có thể thay thế Factory Method khi không muốn tạo nhiều lớp Creator con. Memento đôi khi dùng Prototype để chụp trạng thái. Composite và Decorator hưởng lợi từ Prototype khi cần sao chép cấu trúc phức tạp.
11Bài tập thực hành
🏊 Object Pool — Bể đối tượng
Mục đích: Quản lý một tập các đối tượng có thể tái sử dụng. Thay vì tạo mới và huỷ liên tục, client mượn đối tượng từ bể và trả lại khi dùng xong.
1Vấn đề cần giải quyết
Một số đối tượng rất “đắt” để tạo: kết nối cơ sở dữ liệu (bắt tay TCP, xác thực), thread, socket, bộ đệm lớn, đối tượng đồ hoạ trong game (đạn, hiệu ứng nổ)…
- Tạo mới cho mỗi yêu cầu → chậm (mỗi kết nối DB có thể mất hàng trăm mili-giây).
- Không giới hạn số lượng → cạn tài nguyên (CSDL chỉ cho phép N kết nối đồng thời).
- Tạo/huỷ liên tục gây áp lực cho bộ thu gom rác (GC), làm game bị giật.
2Giải pháp
Tạo lớp Pool giữ hai danh sách: đối tượng rảnh và đối tượng đang dùng.
acquire(): nếu có đối tượng rảnh → lấy ra; nếu chưa đạt giới hạn → tạo mới; nếu đã đầy → chờ, trảnullhoặc ném ngoại lệ.release(obj): làm sạch (reset) đối tượng rồi đưa về danh sách rảnh.
synchronized (hoặc dùng BlockingQueue) vì nhiều luồng cùng mượn/trả.3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Reusable (DbConnection) | Đối tượng tốn kém, có thể reset để tái sử dụng. |
| ObjectPool (ConnectionPool) | Quản lý danh sách rảnh/đang dùng, giới hạn số lượng; thường là Singleton. |
| Client | Mượn bằng acquire(), bắt buộc trả bằng release(). |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Object Pool – C++17. Rất phổ biến trong firmware: KHÔNG malloc/new lúc chạy,
// mọi đối tượng nằm sẵn trong một mảng tĩnh có kích thước biết trước khi biên dịch.
#include <array>
#include <cstdio>
#include <iostream>
class DbConnection { // trên MCU có thể là: gói tin, buffer DMA, timer phần mềm...
public:
static inline int created = 0;
void open() { id_ = ++created; std::cout << " (khởi tạo kết nối #" << id_ << " – tốn ~200ms)\n"; }
void query(const char* sql) { ++useCount_; std::cout << " #" << id_ << " chạy: " << sql << "\n"; }
void reset() { /* xoá trạng thái dở dang */ }
void print() const { std::cout << "Conn#" << id_ << "(đã dùng " << useCount_ << " lần)"; }
private:
int id_ = 0, useCount_ = 0;
};
template <typename T, size_t N>
class ObjectPool {
public:
// Trả về nullptr khi pool đầy – firmware không ném exception
T* acquire() {
for (size_t i = 0; i < N; ++i) {
if (!inUse_[i]) {
if (!initialized_[i]) { items_[i].open(); initialized_[i] = true; } // khởi tạo lười, 1 lần
inUse_[i] = true;
return &items_[i];
}
}
return nullptr;
}
void release(T* obj) {
size_t i = obj - items_.data(); // tính chỉ số từ địa chỉ
if (i < N && inUse_[i]) { obj->reset(); inUse_[i] = false; }
}
void printStatus() const {
size_t used = 0;
for (bool b : inUse_) used += b;
std::cout << " [Pool] đang dùng=" << used << ", rảnh=" << N - used << ", tối đa=" << N << "\n";
}
private:
std::array<T, N> items_{}; // bộ nhớ cấp phát TĨNH, biết trước lúc biên dịch
std::array<bool, N> inUse_{};
std::array<bool, N> initialized_{};
};
int main() {
static ObjectPool<DbConnection, 2> pool; // static: nằm trong .bss, không ở stack/heap
DbConnection* a = pool.acquire();
DbConnection* b = pool.acquire();
a->query("SELECT * FROM students");
b->query("SELECT * FROM courses");
pool.printStatus();
DbConnection* c = pool.acquire();
std::cout << "Lấy thêm khi pool đầy -> " << (c ? "có" : "nullptr") << "\n";
pool.release(a);
pool.printStatus();
DbConnection* d = pool.acquire();
d->query("UPDATE scores SET point = 10");
std::cout << "d có phải là a được tái sử dụng? " << std::boolalpha << (d == a) << "\n";
pool.printStatus();
std::cout << "Tổng số kết nối thực sự được tạo: " << DbConnection::created << "\n";
std::cout << "Kích thước pool trong RAM: " << sizeof(pool) << " byte (cố định)\n";
}
g++ -std=c++17 06-object-pool.cpp -o app && ./app(khởi tạo kết nối #1 – tốn ~200ms) (khởi tạo kết nối #2 – tốn ~200ms) #1 chạy: SELECT * FROM students #2 chạy: SELECT * FROM courses [Pool] đang dùng=2, rảnh=0, tối đa=2 Lấy thêm khi pool đầy -> nullptr [Pool] đang dùng=1, rảnh=1, tối đa=2 #1 chạy: UPDATE scores SET point = 10 d có phải là a được tái sử dụng? true [Pool] đang dùng=2, rảnh=0, tối đa=2 Tổng số kết nối thực sự được tạo: 2 Kích thước pool trong RAM: 20 byte (cố định)
Giải thích code C++17
ObjectPool<T, N>là template: kích thước N biết lúc biên dịch, mọi đối tượng nằm trongstd::array— không có một lần cấp phát động nào.static ObjectPool pool;đặt pool vào vùng nhớ tĩnh; kết quảsizeof(pool)cho biết chính xác RAM tiêu tốn.acquire()trả vềnullptrkhi đầy — firmware xử lý bằng cách bỏ qua hoặc thử lại ở vòngloop()sau.release()tính chỉ số bằng phép trừ con trỏobj - items_.data().
"""Object Pool – Python 3. Dùng contextmanager để "mượn – tự động trả" bằng câu lệnh with."""
from contextlib import contextmanager
class DbConnection:
created = 0
def __init__(self):
DbConnection.created += 1
self.id = DbConnection.created
self.use_count = 0
print(f" (tạo mới kết nối #{self.id} – tốn ~200ms)")
def query(self, sql):
self.use_count += 1
print(f" #{self.id} chạy: {sql}")
def reset(self):
pass # xoá transaction dở dang...
class ConnectionPool:
def __init__(self, max_size):
self.max_size = max_size
self._available = []
self._in_use = set()
def acquire(self):
if self._available:
conn = self._available.pop()
elif len(self._in_use) < self.max_size:
conn = DbConnection()
else:
return None
self._in_use.add(conn)
return conn
def release(self, conn):
if conn in self._in_use:
self._in_use.remove(conn)
conn.reset()
self._available.append(conn)
@contextmanager
def connection(self):
conn = self.acquire()
try:
yield conn
finally:
if conn: self.release(conn) # luôn trả lại, kể cả khi có lỗi
def status(self):
print(f" [Pool] đang dùng={len(self._in_use)}, rảnh={len(self._available)}, tối đa={self.max_size}")
if __name__ == "__main__":
pool = ConnectionPool(2)
a, b = pool.acquire(), pool.acquire()
a.query("SELECT * FROM students")
b.query("SELECT * FROM courses")
pool.status()
print("Lấy thêm khi pool đầy ->", pool.acquire())
pool.release(a)
pool.status()
with pool.connection() as d: # mượn bằng with, tự trả khi ra khỏi khối
d.query("UPDATE scores SET point = 10")
print("d có phải là a được tái sử dụng?", d is a)
pool.status()
pool.status()
print("Tổng số kết nối thực sự được tạo:", DbConnection.created)
python 06-object-pool.py(tạo mới kết nối #1 – tốn ~200ms) (tạo mới kết nối #2 – tốn ~200ms) #1 chạy: SELECT * FROM students #2 chạy: SELECT * FROM courses [Pool] đang dùng=2, rảnh=0, tối đa=2 Lấy thêm khi pool đầy -> None [Pool] đang dùng=1, rảnh=1, tối đa=2 #1 chạy: UPDATE scores SET point = 10 d có phải là a được tái sử dụng? True [Pool] đang dùng=2, rảnh=0, tối đa=2 [Pool] đang dùng=1, rảnh=1, tối đa=2 Tổng số kết nối thực sự được tạo: 2
Giải thích code Python 3
@contextmanagercho phép viếtwith pool.connection() as d:— đối tượng luôn được trả lại kể cả khi có ngoại lệ.- Trạng thái pool in ra trước và sau khối
withcho thấy việc trả tự động.
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Set;
public class ObjectPoolDemo {
public static void main(String[] args) {
ConnectionPool pool = new ConnectionPool(2); // tối đa 2 kết nối
DbConnection a = pool.acquire();
DbConnection b = pool.acquire();
a.query("SELECT * FROM students");
b.query("SELECT * FROM courses");
pool.printStatus();
DbConnection c = pool.acquire(); // pool đã cạn
System.out.println("Lấy thêm khi pool đầy -> " + c);
pool.release(a); // trả lại -> có thể tái sử dụng
pool.printStatus();
DbConnection d = pool.acquire();
d.query("UPDATE scores SET point = 10");
System.out.println("d có phải là kết nối a được tái sử dụng? " + (d == a));
pool.printStatus();
System.out.println("Tổng số kết nối thực sự được tạo: " + DbConnection.created);
}
}
/** Đối tượng "đắt" – tạo mới tốn thời gian (mở socket, bắt tay, xác thực...). */
class DbConnection {
static int created = 0;
private final int id;
private int useCount = 0;
DbConnection() {
id = ++created;
System.out.println(" (tạo mới kết nối #" + id + " – tốn ~200ms)");
}
void query(String sql) { useCount++; System.out.println(" #" + id + " chạy: " + sql); }
void reset() { /* xoá transaction dở dang, đặt lại trạng thái */ }
@Override public String toString() { return "Conn#" + id + "(đã dùng " + useCount + " lần)"; }
}
/** Object Pool – quản lý tập đối tượng có thể tái sử dụng. */
class ConnectionPool {
private final int maxSize;
private final Deque<DbConnection> available = new ArrayDeque<>();
private final Set<DbConnection> inUse = new HashSet<>();
ConnectionPool(int maxSize) { this.maxSize = maxSize; }
public synchronized DbConnection acquire() {
DbConnection conn;
if (!available.isEmpty()) {
conn = available.pop(); // ưu tiên tái sử dụng
} else if (inUse.size() < maxSize) {
conn = new DbConnection(); // còn hạn mức -> tạo mới
} else {
return null; // hết -> trả null (thực tế: chờ hoặc ném lỗi)
}
inUse.add(conn);
return conn;
}
public synchronized void release(DbConnection conn) {
if (inUse.remove(conn)) {
conn.reset(); // làm sạch trước khi cho mượn lại
available.push(conn);
}
}
void printStatus() {
System.out.println(" [Pool] đang dùng=" + inUse.size() + ", rảnh=" + available.size() + ", tối đa=" + maxSize);
}
}
java 06-object-pool.java(tạo mới kết nối #1 – tốn ~200ms) (tạo mới kết nối #2 – tốn ~200ms) #1 chạy: SELECT * FROM students #2 chạy: SELECT * FROM courses [Pool] đang dùng=2, rảnh=0, tối đa=2 Lấy thêm khi pool đầy -> null [Pool] đang dùng=1, rảnh=1, tối đa=2 #1 chạy: UPDATE scores SET point = 10 d có phải là kết nối a được tái sử dụng? true [Pool] đang dùng=2, rảnh=0, tối đa=2 Tổng số kết nối thực sự được tạo: 2
Giải thích code Java 17+
- Pool giới hạn 2 kết nối: lần mượn thứ 3 trả về
nullvì đã đầy. - Sau khi
release(a), lầnacquire()tiếp theo nhận lại chính đối tượnga(d == alàtrue). - Dù gọi acquire 4 lần, chỉ 2 kết nối thực sự được tạo.
reset()trước khi cho mượn lại là bước rất quan trọng — tránh rò rỉ trạng thái của người dùng trước.
6Áp dụng trong lập trình nhúng
- Đây là mẫu cực kỳ phổ biến trong firmware: nhiều chuẩn an toàn (MISRA C++, AUTOSAR) cấm cấp phát động sau khi khởi động vì phân mảnh heap và thời gian không xác định.
- Ví dụ: pool buffer cho gói tin UART/CAN, pool khối nhớ cho hàng đợi FreeRTOS, pool sự kiện trong hệ thống event-driven.
- FreeRTOS cung cấp
xQueueCreateStatic,xTaskCreateStatic— tạo đối tượng trên bộ nhớ tĩnh do bạn cấp.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Tăng hiệu năng rõ rệt khi đối tượng tốn kém để khởi tạo.
- Giới hạn và kiểm soát tài nguyên hệ thống.
- Giảm áp lực cho garbage collector.
✘ Nhược điểm
- Client quên trả đối tượng → pool cạn dần (leak).
- Đối tượng trả về chưa được reset sạch → lỗi khó tìm.
- Thêm độ phức tạp đồng bộ đa luồng.
- Với đối tượng nhẹ, pool còn chậm hơn tạo mới (JVM hiện đại cấp phát rất nhanh).
8Khi nào nên dùng
- Đối tượng tốn kém để tạo: kết nối DB, thread, socket, buffer lớn.
- Số lượng đối tượng đồng thời cần bị giới hạn.
- Game: đạn, hạt (particle), kẻ địch xuất hiện và biến mất liên tục.
9Gặp ở đâu trong thực tế
C++ / nhúng
- FreeRTOS:
heap_1…heap_5, các API...CreateStatic - Zephyr RTOS:
k_mem_slab - lwIP:
memppools cho pbuf/TCP PCB
Python
concurrent.futures.ThreadPoolExecutormultiprocessing.Pool- SQLAlchemy
QueuePool
Java
- Connection pool: HikariCP, Apache DBCP, C3P0
- Thread pool:
java.util.concurrent.ExecutorService/Executors.newFixedThreadPool() Integer.valueOf()cache các số từ -128 đến 127
10Liên hệ với các mẫu khác
Pool thường được cài đặt như Singleton. Khác với Flyweight: đối tượng trong Flyweight được dùng chung đồng thời và bất biến; đối tượng trong Pool chỉ được một client dùng tại một thời điểm.
11Bài tập thực hành
🔌 Adapter — Bộ chuyển đổi
Mục đích: Cho phép các lớp có interface không tương thích làm việc được với nhau, bằng cách bọc một lớp bằng một interface mà client mong đợi.
1Vấn đề cần giải quyết
Trạm thời tiết của lớp đang hiển thị nhiệt độ từ các cảm biến cài đặt interface TemperatureSensor (trả về độ C, có hàm readCelsius()).
Nhà trường mua thêm cảm biến nhập khẩu kèm thư viện UsFahrenheitSensor — trả về độ F, tên hàm là getTempF(). Ta không thể sửa thư viện này (không có mã nguồn, hoặc sửa sẽ mất khi cập nhật), cũng không muốn sửa Dashboard đang chạy tốt.
2Giải pháp
Tạo lớp Adapter cài đặt interface mà client cần (TemperatureSensor), bên trong giữ đối tượng cần chuyển đổi (adaptee). Mỗi lời gọi từ client được adapter dịch sang lời gọi tương ứng của adaptee — kèm chuyển đổi dữ liệu nếu cần (°F → °C).
| Object Adapter | Class Adapter | |
|---|---|---|
| Cơ chế | Composition — adapter chứa adaptee | Kế thừa — adapter extends adaptee |
| Bọc được lớp con của adaptee? | ✓ | ✗ |
| Ghi đè hành vi adaptee? | Khó | Dễ |
| Khuyên dùng | ✓ (linh hoạt hơn) | Khi cần ghi đè |
3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Target (TemperatureSensor) | Interface mà client mong đợi. |
| Adaptee (UsFahrenheitSensor) | Lớp có sẵn với interface không tương thích. |
| Adapter (FahrenheitSensorAdapter) | Cài đặt Target, chuyển lời gọi sang Adaptee. |
| Client (Dashboard) | Chỉ làm việc với Target. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Adapter – C++17. Tình huống nhúng điển hình: driver cảm biến từ thư viện khác
// có API không khớp với lớp trừu tượng (HAL) mà firmware của ta đang dùng.
#include <cstdio>
#include <string>
// ===== Target – interface mà firmware mong đợi =====
class TemperatureSensor {
public:
virtual ~TemperatureSensor() = default;
virtual std::string name() const = 0;
virtual float readCelsius() = 0;
};
// Client
class Dashboard {
public:
void show(TemperatureSensor& s) {
float c = s.readCelsius();
std::printf("%5.1f°C %s <- %s\n", c, c > 30 ? "🔥 Nóng" : "🙂 Ổn ", s.name().c_str());
}
};
// Lớp đã tương thích sẵn
class Dht11Sensor : public TemperatureSensor {
public:
std::string name() const override { return "DHT11 (nội địa)"; }
float readCelsius() override { return 28.5f; }
};
// ===== Adaptee – thư viện bên thứ ba, KHÔNG được sửa =====
class UsFahrenheitSensor {
public:
explicit UsFahrenheitSensor(const char* serial) : serial_(serial) {}
const char* getSerialNumber() const { return serial_; }
float getTempF() const { return 95.0f; } // 95°F = 35°C
private:
const char* serial_;
};
// ===== Cách 1 – Object Adapter (composition): giữ THAM CHIẾU tới adaptee =====
class FahrenheitSensorAdapter : public TemperatureSensor {
public:
explicit FahrenheitSensorAdapter(UsFahrenheitSensor& adaptee) : adaptee_(adaptee) {}
std::string name() const override { return std::string("Adapter[") + adaptee_.getSerialNumber() + "]"; }
float readCelsius() override { return (adaptee_.getTempF() - 32.0f) * 5.0f / 9.0f; }
private:
UsFahrenheitSensor& adaptee_;
};
// ===== Cách 2 – Class Adapter: đa kế thừa (C++ hỗ trợ) =====
// public kế thừa Target, private kế thừa Adaptee (chỉ dùng cài đặt, không lộ API cũ ra ngoài)
class FahrenheitClassAdapter : public TemperatureSensor, private UsFahrenheitSensor {
public:
explicit FahrenheitClassAdapter(const char* serial) : UsFahrenheitSensor(serial) {}
std::string name() const override { return std::string("ClassAdapter[") + getSerialNumber() + "]"; }
float readCelsius() override { return (getTempF() - 32.0f) * 5.0f / 9.0f; }
};
int main() {
Dht11Sensor dht;
UsFahrenheitSensor usSensor("US-77");
FahrenheitSensorAdapter adapter(usSensor);
FahrenheitClassAdapter classAdapter("US-99");
TemperatureSensor* sensors[] = { &dht, &adapter, &classAdapter };
Dashboard dashboard;
for (TemperatureSensor* s : sensors) dashboard.show(*s);
}
g++ -std=c++17 07-adapter.cpp -o app && ./app28.5°C 🙂 Ổn <- DHT11 (nội địa) 35.0°C 🔥 Nóng <- Adapter[US-77] 35.0°C 🔥 Nóng <- ClassAdapter[US-99]
Giải thích code C++17
- Object Adapter giữ tham chiếu
UsFahrenheitSensor&— không sở hữu, không cấp phát. - Class Adapter dùng đa kế thừa:
public TemperatureSensor(lộ ra interface mới) vàprivate UsFahrenheitSensor(chỉ mượn cài đặt, ẩn API cũ). - Mảng
TemperatureSensor* sensors[]chứa cả ba loại — client xử lý như nhau.
"""Adapter – Python 3."""
from abc import ABC, abstractmethod
class TemperatureSensor(ABC): # Target
@abstractmethod
def name(self) -> str: ...
@abstractmethod
def read_celsius(self) -> float: ...
class Dashboard: # Client
def show(self, s: TemperatureSensor):
c = s.read_celsius()
print(f"{c:5.1f}°C {'🔥 Nóng' if c > 30 else '🙂 Ổn '} <- {s.name()}")
class Dht11Sensor(TemperatureSensor):
def name(self): return "DHT11 (nội địa)"
def read_celsius(self): return 28.5
class UsFahrenheitSensor: # Adaptee – thư viện bên thứ ba
def __init__(self, serial): self._serial = serial
def get_serial_number(self): return self._serial
def get_temp_f(self): return 95.0
class FahrenheitSensorAdapter(TemperatureSensor): # Object Adapter
def __init__(self, adaptee: UsFahrenheitSensor):
self._adaptee = adaptee
def name(self): return f"Adapter[{self._adaptee.get_serial_number()}]"
def read_celsius(self): return (self._adaptee.get_temp_f() - 32) * 5 / 9
class FahrenheitClassAdapter(TemperatureSensor, UsFahrenheitSensor): # Class Adapter (đa kế thừa)
def name(self): return f"ClassAdapter[{self.get_serial_number()}]"
def read_celsius(self): return (self.get_temp_f() - 32) * 5 / 9
if __name__ == "__main__":
sensors = [Dht11Sensor(),
FahrenheitSensorAdapter(UsFahrenheitSensor("US-77")),
FahrenheitClassAdapter("US-99")]
dashboard = Dashboard()
for s in sensors:
dashboard.show(s)
python 07-adapter.py28.5°C 🙂 Ổn <- DHT11 (nội địa) 35.0°C 🔥 Nóng <- Adapter[US-77] 35.0°C 🔥 Nóng <- ClassAdapter[US-99]
Giải thích code Python 3
- Class Adapter trong Python cũng dùng đa kế thừa
class FahrenheitClassAdapter(TemperatureSensor, UsFahrenheitSensor). - Do duck typing, đôi khi chỉ cần một hàm bọc nhỏ thay vì cả một lớp adapter.
import java.util.List;
public class AdapterDemo {
public static void main(String[] args) {
// Hệ thống của ta chỉ hiểu TemperatureSensor (độ C).
// Cảm biến Mỹ mua về lại trả về độ F và có API khác hẳn -> dùng Adapter.
List<TemperatureSensor> sensors = List.of(
new Dht11Sensor(),
new FahrenheitSensorAdapter(new UsFahrenheitSensor("US-77")), // Object Adapter
new FahrenheitClassAdapter("US-99") // Class Adapter
);
Dashboard dashboard = new Dashboard();
for (TemperatureSensor s : sensors) dashboard.show(s);
}
}
/** Target – giao diện mà client mong đợi. */
interface TemperatureSensor {
String name();
double readCelsius();
}
/** Client – chỉ làm việc với Target. */
class Dashboard {
void show(TemperatureSensor s) {
double c = s.readCelsius();
System.out.printf("%-28s %.1f°C %s%n", s.name(), c, c > 30 ? "🔥 Nóng" : "🙂 Ổn");
}
}
/** Lớp có sẵn đã tương thích. */
class Dht11Sensor implements TemperatureSensor {
public String name() { return "DHT11 (nội địa)"; }
public double readCelsius() { return 28.5; }
}
/** Adaptee – thư viện bên thứ ba, KHÔNG được sửa mã nguồn. */
class UsFahrenheitSensor {
private final String serial;
UsFahrenheitSensor(String serial) { this.serial = serial; }
public String getSerialNumber() { return serial; }
public double getTempF() { return 95.0; } // 95°F = 35°C
}
/** Cách 1 – Object Adapter: CHỨA adaptee bên trong (composition) – được khuyên dùng. */
class FahrenheitSensorAdapter implements TemperatureSensor {
private final UsFahrenheitSensor adaptee;
FahrenheitSensorAdapter(UsFahrenheitSensor adaptee) { this.adaptee = adaptee; }
public String name() { return "Adapter[" + adaptee.getSerialNumber() + "]"; }
public double readCelsius() { return (adaptee.getTempF() - 32) * 5 / 9; } // chuyển đổi dữ liệu
}
/** Cách 2 – Class Adapter: KẾ THỪA adaptee và implement target. */
class FahrenheitClassAdapter extends UsFahrenheitSensor implements TemperatureSensor {
FahrenheitClassAdapter(String serial) { super(serial); }
public String name() { return "ClassAdapter[" + getSerialNumber() + "]"; }
public double readCelsius() { return (getTempF() - 32) * 5 / 9; }
}
java 07-adapter.javaDHT11 (nội địa) 28.5°C 🙂 Ổn Adapter[US-77] 35.0°C 🔥 Nóng ClassAdapter[US-99] 35.0°C 🔥 Nóng
Giải thích code Java 17+
DashboardvàUsFahrenheitSensorkhông bị sửa một dòng nào.FahrenheitSensorAdapter(object adapter) vừa đổi tên hàm vừa đổi đơn vị: 95°F → 35°C.FahrenheitClassAdapterlàm cùng việc bằng kế thừa — gọi thẳnggetTempF()của lớp cha.- Cả ba cảm biến nằm chung một
List<TemperatureSensor>và được xử lý giống hệt nhau.
6Áp dụng trong lập trình nhúng
- Tình huống hằng ngày: thư viện cảm biến của hãng A trả về
int16_tthô, của hãng B trả vềfloatđã đổi đơn vị — viết adapter đưa về một interfaceISensorchung. - Bọc HAL của nhà sản xuất (STM32 HAL, ESP-IDF) sau interface của riêng bạn → đổi chip chỉ cần viết adapter mới.
- Bọc thư viện C (hàm + struct) thành lớp C++ (RAII) cũng là một dạng adapter.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Tái sử dụng lớp có sẵn mà không sửa mã nguồn.
- Single Responsibility: logic chuyển đổi tách riêng khỏi nghiệp vụ.
- Open/Closed: thêm adapter mới không ảnh hưởng client.
✘ Nhược điểm
- Thêm lớp trung gian, tăng độ phức tạp.
- Đôi khi sửa trực tiếp lớp dịch vụ cho tương thích lại đơn giản hơn (nếu được phép).
8Khi nào nên dùng
- Muốn dùng một lớp có sẵn (thư viện bên thứ ba, code cũ) nhưng interface không khớp.
- Tích hợp hệ thống cũ (legacy) với hệ thống mới.
- Chuẩn hoá nhiều nguồn dữ liệu khác định dạng về một interface chung.
9Gặp ở đâu trong thực tế
C++ / nhúng
std::stack,std::queue— “container adapter” bọcstd::dequestd::back_inserter(iterator adapter)- Arduino core cho ESP32: bọc API ESP-IDF thành API Arduino
Python
io.TextIOWrapperbọc luồng byte thành luồng văn bảncsv.DictReader
Java
Arrays.asList()— biến mảng thành ListInputStreamReader— chuyển byte stream thành character streamCollections.enumeration(),Collections.list()
10Liên hệ với các mẫu khác
Bridge được thiết kế từ đầu để tách hai phần; Adapter dùng để chữa cháy cho các lớp đã có. Decorator giữ nguyên interface và thêm hành vi; Adapter đổi interface. Facade tạo interface mới cho cả hệ thống con; Adapter chỉ bọc một đối tượng.
11Bài tập thực hành
🌉 Bridge — Cầu nối
Mục đích: Tách phần trừu tượng (abstraction) ra khỏi phần cài đặt (implementation) để hai phần có thể phát triển độc lập với nhau.
1Vấn đề cần giải quyết
Có 2 loại điều khiển (Cơ bản, Nâng cao) và 2 loại thiết bị (TV, Radio). Nếu dùng kế thừa thuần tuý, ta cần các lớp: BasicTvRemote, BasicRadioRemote, AdvancedTvRemote, AdvancedRadioRemote.
Thêm thiết bị Quạt → thêm 2 lớp. Thêm điều khiển Giọng nói → thêm 3 lớp nữa. Số lớp = M × N — “bùng nổ lớp”, và rất nhiều code trùng lặp.
2Giải pháp
Tách thành hai cây phân cấp riêng biệt và nối bằng composition:
- Abstraction (
RemoteControl): phần điều khiển cấp cao, giữ tham chiếu tới mộtDevice— chính tham chiếu này là “cây cầu”. - Implementation (
Device): phần thao tác cấp thấp (bật, tắt, chỉnh âm lượng).
Số lớp giờ chỉ là M + N. Mỗi bên mở rộng độc lập: thêm điều khiển mới không cần đụng tới thiết bị, và ngược lại.
3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Abstraction (RemoteControl) | Định nghĩa logic điều khiển cấp cao, uỷ quyền công việc cho Implementor. |
| RefinedAbstraction (AdvancedRemote) | Mở rộng Abstraction với chức năng mới. |
| Implementor (Device) | Interface cho các thao tác cấp thấp. |
| ConcreteImplementor (Tv, Radio) | Cài đặt cụ thể cho từng nền tảng/thiết bị. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Bridge – C++17
#include <algorithm>
#include <iostream>
#include <string>
// ===== Implementor =====
class Device {
public:
virtual ~Device() = default;
virtual bool isEnabled() const = 0;
virtual void enable() = 0;
virtual void disable() = 0;
virtual int getVolume() const = 0;
virtual void setVolume(int percent) = 0;
virtual std::string name() const = 0;
void printStatus() const {
std::cout << " " << name() << ": " << (isEnabled() ? "BẬT" : "TẮT") << ", âm lượng " << getVolume() << "%\n";
}
};
// Gom code chung của các thiết bị
class BaseDevice : public Device {
public:
bool isEnabled() const override { return on_; }
void enable() override { on_ = true; }
void disable() override { on_ = false; }
int getVolume() const override { return volume_; }
void setVolume(int p) override { volume_ = std::clamp(p, 0, 100); }
private:
bool on_ = false;
int volume_ = 30;
};
// ===== Concrete Implementors =====
class Tv : public BaseDevice { public: std::string name() const override { return "Tv"; } };
class Radio : public BaseDevice { public: std::string name() const override { return "Radio"; } };
// ===== Abstraction – giữ tham chiếu tới Implementor: đó chính là "cây cầu" =====
class RemoteControl {
public:
explicit RemoteControl(Device& d) : device_(d) {}
virtual ~RemoteControl() = default;
void togglePower() {
if (device_.isEnabled()) device_.disable(); else device_.enable();
std::cout << "Nút nguồn -> " << (device_.isEnabled() ? "bật" : "tắt") << "\n";
}
void volumeUp() { device_.setVolume(device_.getVolume() + 10); std::cout << "Tăng âm lượng\n"; }
void volumeDown() { device_.setVolume(device_.getVolume() - 10); std::cout << "Giảm âm lượng\n"; }
protected:
Device& device_;
};
// ===== Refined Abstraction =====
class AdvancedRemote : public RemoteControl {
public:
using RemoteControl::RemoteControl; // kế thừa constructor
void mute() { device_.setVolume(0); std::cout << "Tắt tiếng (mute)\n"; }
};
int main() {
Tv tv;
RemoteControl basic(tv);
basic.togglePower();
basic.volumeUp();
tv.printStatus();
std::cout << "-----\n";
Radio radio;
AdvancedRemote advanced(radio);
advanced.togglePower();
advanced.volumeUp();
advanced.mute();
radio.printStatus();
}
g++ -std=c++17 08-bridge.cpp -o app && ./appNút nguồn -> bật Tăng âm lượng Tv: BẬT, âm lượng 40% ----- Nút nguồn -> bật Tăng âm lượng Tắt tiếng (mute) Radio: BẬT, âm lượng 0%
Giải thích code C++17
RemoteControlgiữDevice& device_— tham chiếu tới implementor chính là cây cầu.using RemoteControl::RemoteControl;giúp lớp con kế thừa constructor, khỏi viết lại.std::clamp(C++17) giới hạn âm lượng trong 0–100.
"""Bridge – Python 3."""
from abc import ABC
class Device(ABC): # Implementor (+ code chung)
def __init__(self):
self._on = False
self._volume = 30
def is_enabled(self): return self._on
def enable(self): self._on = True
def disable(self): self._on = False
def get_volume(self): return self._volume
def set_volume(self, p): self._volume = max(0, min(100, p))
def print_status(self):
print(f" {type(self).__name__}: {'BẬT' if self._on else 'TẮT'}, âm lượng {self._volume}%")
class Tv(Device): pass
class Radio(Device): pass
class RemoteControl: # Abstraction
def __init__(self, device: Device):
self.device = device # "cây cầu"
def toggle_power(self):
self.device.disable() if self.device.is_enabled() else self.device.enable()
print("Nút nguồn ->", "bật" if self.device.is_enabled() else "tắt")
def volume_up(self):
self.device.set_volume(self.device.get_volume() + 10)
print("Tăng âm lượng")
class AdvancedRemote(RemoteControl): # Refined Abstraction
def mute(self):
self.device.set_volume(0)
print("Tắt tiếng (mute)")
if __name__ == "__main__":
tv = Tv()
basic = RemoteControl(tv)
basic.toggle_power()
basic.volume_up()
tv.print_status()
print("-----")
radio = Radio()
adv = AdvancedRemote(radio)
adv.toggle_power()
adv.volume_up()
adv.mute()
radio.print_status()
python 08-bridge.pyNút nguồn -> bật Tăng âm lượng Tv: BẬT, âm lượng 40% ----- Nút nguồn -> bật Tăng âm lượng Tắt tiếng (mute) Radio: BẬT, âm lượng 0%
Giải thích code Python 3
- Code gần như dịch 1–1; cây cầu là thuộc tính
self.device. Tv,Radiochỉ cầnpassvì đã kế thừa đủ hành vi từDevice.
public class BridgeDemo {
public static void main(String[] args) {
// 2 loại điều khiển × 2 loại thiết bị = 4 tổ hợp, nhưng chỉ cần 2 + 2 = 4 lớp.
// Nếu dùng kế thừa thuần: BasicRemoteTv, BasicRemoteRadio, AdvancedRemoteTv, ... (bùng nổ lớp)
Device tv = new Tv();
RemoteControl basic = new RemoteControl(tv);
basic.togglePower();
basic.volumeUp();
tv.printStatus();
System.out.println("-----");
Device radio = new Radio();
AdvancedRemote advanced = new AdvancedRemote(radio);
advanced.togglePower();
advanced.volumeUp();
advanced.mute();
radio.printStatus();
}
}
// ===== Implementor: phía "thực thi" =====
interface Device {
boolean isEnabled();
void enable();
void disable();
int getVolume();
void setVolume(int percent);
void printStatus();
}
// ===== Concrete Implementors =====
abstract class BaseDevice implements Device {
private boolean on = false;
private int volume = 30;
public boolean isEnabled() { return on; }
public void enable() { on = true; }
public void disable() { on = false; }
public int getVolume() { return volume; }
public void setVolume(int p) { volume = Math.max(0, Math.min(100, p)); }
public void printStatus() {
System.out.println(" " + getClass().getSimpleName() + ": " + (on ? "BẬT" : "TẮT") + ", âm lượng " + volume + "%");
}
}
class Tv extends BaseDevice { }
class Radio extends BaseDevice { }
// ===== Abstraction: phía "điều khiển", giữ tham chiếu tới Implementor (cây cầu) =====
class RemoteControl {
protected final Device device;
RemoteControl(Device device) { this.device = device; }
void togglePower() {
if (device.isEnabled()) device.disable(); else device.enable();
System.out.println("Nút nguồn -> " + (device.isEnabled() ? "bật" : "tắt"));
}
void volumeUp() { device.setVolume(device.getVolume() + 10); System.out.println("Tăng âm lượng"); }
void volumeDown() { device.setVolume(device.getVolume() - 10); System.out.println("Giảm âm lượng"); }
}
// ===== Refined Abstraction: mở rộng phía điều khiển mà không đụng tới thiết bị =====
class AdvancedRemote extends RemoteControl {
AdvancedRemote(Device device) { super(device); }
void mute() { device.setVolume(0); System.out.println("Tắt tiếng (mute)"); }
}
java 08-bridge.javaNút nguồn -> bật Tăng âm lượng Tv: BẬT, âm lượng 40% ----- Nút nguồn -> bật Tăng âm lượng Tắt tiếng (mute) Radio: BẬT, âm lượng 0%
Giải thích code Java 17+
RemoteControlgiữDevice device— mọi thao tác đều uỷ quyền qua tham chiếu này.AdvancedRemotethêm nútmute()mà không cần biết thiết bị là TV hay Radio.- Ghép tuỳ ý lúc chạy:
new AdvancedRemote(new Radio()),new RemoteControl(new Tv())… BaseDevicelà lớp trừu tượng gom code chung của các thiết bị (không bắt buộc trong mẫu).
6Áp dụng trong lập trình nhúng
- Tách logic ứng dụng (điều khiển động cơ, hiển thị) khỏi driver phần cứng (L298N, DRV8833; OLED SSD1306, LCD1602): hai chiều thay đổi độc lập.
- Thư viện Adafruit_GFX: lớp vẽ chữ/hình (abstraction) ↔ hàng chục driver màn hình (implementation) — ví dụ kinh điển của Bridge.
- Giúp viết unit test trên PC: thay driver thật bằng driver giả (mock) mà không đổi code logic.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Tránh bùng nổ lớp M × N.
- Abstraction và Implementation mở rộng độc lập (Open/Closed).
- Có thể đổi implementation lúc chạy.
- Che giấu chi tiết nền tảng khỏi client.
✘ Nhược điểm
- Thiết kế phức tạp hơn khi áp dụng cho lớp vốn đã gắn kết chặt.
- Cần xác định đúng hai chiều biến đổi ngay từ đầu.
8Khi nào nên dùng
- Một lớp có hai (hoặc nhiều) chiều thay đổi độc lập (loại điều khiển × loại thiết bị; hình dạng × màu sắc; giao diện × hệ điều hành).
- Cần đổi implementation lúc chạy.
- Viết code đa nền tảng / đa driver.
9Gặp ở đâu trong thực tế
C++ / nhúng
- Adafruit_GFX ↔ Adafruit_SSD1306 / ST7735 / ILI9341
- Kỹ thuật PIMPL (pointer to implementation) trong C++
Printclass của Arduino:print()↔write()của từng thiết bị
Python
logging:Logger↔Handler(file, console, mạng)pathlib:Path↔PosixPath/WindowsPath
Java
- JDBC:
DriverManager/Connection(abstraction) ↔ driver MySQL, PostgreSQL (implementation) - SLF4J: API ghi log ↔ Logback, Log4j
- AWT:
java.awt.Component↔ peer của từng hệ điều hành
10Liên hệ với các mẫu khác
Bridge thường được thiết kế từ đầu; Adapter dùng cho hệ thống đã có. Abstract Factory có thể dùng để tạo cặp Abstraction–Implementor phù hợp. Cấu trúc giống Strategy nhưng mục đích khác: Bridge tách cấu trúc, Strategy hoán đổi thuật toán.
11Bài tập thực hành
🌳 Composite — Hỗn hợp / Cây đối tượng
Mục đích: Tổ chức các đối tượng thành cấu trúc cây để biểu diễn quan hệ bộ phận – toàn thể; cho phép client xử lý đối tượng đơn lẻ và nhóm đối tượng theo cùng một cách.
1Vấn đề cần giải quyết
Cần tính dung lượng một thư mục dự án. Thư mục chứa file và các thư mục con; thư mục con lại chứa file và thư mục con nữa…
Nếu xử lý riêng từng loại: if (item instanceof File) … else if (item instanceof Folder) { for … } — code đầy kiểm tra kiểu, phải biết độ sâu của cây, và mỗi khi thêm loại phần tử mới (Shortcut, ZipFile) lại phải sửa mọi chỗ.
2Giải pháp
Định nghĩa interface chung Component (FileSystemItem) cho cả phần tử lá và phần tử chứa. Phần tử chứa (Composite, Folder) giữ danh sách Component con và cài đặt các phương thức bằng cách đệ quy gọi xuống các con rồi tổng hợp kết quả.
Client gọi root.getSize() — không cần biết root là một file hay cả một cây khổng lồ.
add()/remove(): trong Component (minh bạch — client đối xử mọi thứ như nhau, nhưng lá phải ném lỗi) hoặc chỉ trong Composite (an toàn kiểu — như ví dụ này).3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Component (FileSystemItem) | Interface chung cho mọi phần tử trong cây. |
| Leaf (FileItem) | Phần tử không có con, thực hiện công việc thật. |
| Composite (Folder) | Chứa các Component con, uỷ quyền công việc cho con và tổng hợp kết quả. |
| Client | Làm việc với mọi phần tử qua Component. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Composite – C++17
#include <iostream>
#include <memory>
#include <string>
#include <vector>
// ===== Component =====
class FileSystemItem {
public:
virtual ~FileSystemItem() = default;
virtual std::string getName() const = 0;
virtual int getSize() const = 0; // KB
virtual void print(const std::string& indent) const = 0;
};
// ===== Leaf =====
class FileItem : public FileSystemItem {
public:
FileItem(std::string name, int size) : name_(std::move(name)), size_(size) {}
std::string getName() const override { return name_; }
int getSize() const override { return size_; }
void print(const std::string& indent) const override {
std::cout << indent << "📄 " << name_ << " (" << size_ << " KB)\n";
}
private:
std::string name_;
int size_;
};
// ===== Composite – SỞ HỮU các con qua unique_ptr (huỷ cha sẽ tự huỷ cả cây) =====
class Folder : public FileSystemItem {
public:
explicit Folder(std::string name) : name_(std::move(name)) {}
// Trả về con trỏ thô (không sở hữu) để tiện thêm tiếp vào thư mục con
template <typename T, typename... Args>
T* add(Args&&... args) {
auto child = std::make_unique<T>(std::forward<Args>(args)...);
T* raw = child.get();
children_.push_back(std::move(child));
return raw;
}
std::string getName() const override { return name_; }
int getSize() const override { // đệ quy
int total = 0;
for (const auto& c : children_) total += c->getSize();
return total;
}
void print(const std::string& indent) const override {
std::cout << indent << "📁 " << name_ << "/ (" << getSize() << " KB)\n";
for (const auto& c : children_) c->print(indent + " ");
}
private:
std::string name_;
std::vector<std::unique_ptr<FileSystemItem>> children_;
};
int main() {
Folder root("du-an-robot");
Folder* src = root.add<Folder>("src");
src->add<FileItem>("main.cpp", 12);
src->add<FileItem>("motor.cpp", 8);
Folder* docs = root.add<Folder>("docs");
docs->add<FileItem>("bao-cao.pdf", 2048);
Folder* images = docs->add<Folder>("images");
images->add<FileItem>("so-do-mach.png", 512);
root.add<FileItem>("README.md", 3);
root.print("");
std::cout << "Tổng dung lượng dự án: " << root.getSize() << " KB\n";
std::cout << "Dung lượng thư mục docs: " << docs->getSize() << " KB\n";
}
g++ -std=c++17 09-composite.cpp -o app && ./app📁 du-an-robot/ (2583 KB)
📁 src/ (20 KB)
📄 main.cpp (12 KB)
📄 motor.cpp (8 KB)
📁 docs/ (2560 KB)
📄 bao-cao.pdf (2048 KB)
📁 images/ (512 KB)
📄 so-do-mach.png (512 KB)
📄 README.md (3 KB)
Tổng dung lượng dự án: 2583 KB
Dung lượng thư mục docs: 2560 KBGiải thích code C++17
Foldersở hữu các con quastd::vector<std::unique_ptr<FileSystemItem>>— huỷ thư mục gốc sẽ tự huỷ cả cây (RAII).- Hàm template
add<T>(args...)tạo con ngay tại chỗ và trả về con trỏ thô (không sở hữu) để tiện thêm tiếp. getSize()vàprint()gọi đệ quy qua hàm ảo.
"""Composite – Python 3."""
from abc import ABC, abstractmethod
class FileSystemItem(ABC): # Component
def __init__(self, name): self.name = name
@abstractmethod
def get_size(self) -> int: ...
@abstractmethod
def print(self, indent=""): ...
class FileItem(FileSystemItem): # Leaf
def __init__(self, name, size):
super().__init__(name)
self.size = size
def get_size(self): return self.size
def print(self, indent=""): print(f"{indent}📄 {self.name} ({self.size} KB)")
class Folder(FileSystemItem): # Composite
def __init__(self, name):
super().__init__(name)
self.children: list[FileSystemItem] = []
def add(self, item):
self.children.append(item)
return item # trả về item để tiện thêm tiếp
def get_size(self):
return sum(c.get_size() for c in self.children) # đệ quy
def print(self, indent=""):
print(f"{indent}📁 {self.name}/ ({self.get_size()} KB)")
for c in self.children:
c.print(indent + " ")
if __name__ == "__main__":
root = Folder("du-an-robot")
src = root.add(Folder("src"))
src.add(FileItem("main.cpp", 12))
src.add(FileItem("motor.cpp", 8))
docs = root.add(Folder("docs"))
docs.add(FileItem("bao-cao.pdf", 2048))
docs.add(Folder("images")).add(FileItem("so-do-mach.png", 512))
root.add(FileItem("README.md", 3))
root.print()
print("Tổng dung lượng dự án:", root.get_size(), "KB")
print("Dung lượng thư mục docs:", docs.get_size(), "KB")
python 09-composite.py📁 du-an-robot/ (2583 KB)
📁 src/ (20 KB)
📄 main.cpp (12 KB)
📄 motor.cpp (8 KB)
📁 docs/ (2560 KB)
📄 bao-cao.pdf (2048 KB)
📁 images/ (512 KB)
📄 so-do-mach.png (512 KB)
📄 README.md (3 KB)
Tổng dung lượng dự án: 2583 KB
Dung lượng thư mục docs: 2560 KBGiải thích code Python 3
get_size()chỉ một dòng nhờsum(...)trên generator.add()trả về chính phần tử vừa thêm nên viết đượcdocs.add(Folder("images")).add(...).
import java.util.ArrayList;
import java.util.List;
public class CompositeDemo {
public static void main(String[] args) {
Folder root = new Folder("du-an-robot");
Folder src = new Folder("src");
src.add(new FileItem("main.cpp", 12));
src.add(new FileItem("motor.cpp", 8));
Folder docs = new Folder("docs");
docs.add(new FileItem("bao-cao.pdf", 2048));
Folder images = new Folder("images");
images.add(new FileItem("so-do-mach.png", 512));
docs.add(images);
root.add(src);
root.add(docs);
root.add(new FileItem("README.md", 3));
// Client gọi CÙNG một phương thức cho file đơn lẻ lẫn cả cây thư mục
root.print("");
System.out.println("Tổng dung lượng dự án: " + root.getSize() + " KB");
System.out.println("Dung lượng thư mục docs: " + docs.getSize() + " KB");
}
}
/** Component – giao diện chung cho lá (Leaf) và nhánh (Composite). */
interface FileSystemItem {
String getName();
int getSize(); // KB
void print(String indent);
}
/** Leaf – phần tử không chứa phần tử con. */
class FileItem implements FileSystemItem {
private final String name;
private final int size;
FileItem(String name, int size) { this.name = name; this.size = size; }
public String getName() { return name; }
public int getSize() { return size; }
public void print(String indent) { System.out.println(indent + "📄 " + name + " (" + size + " KB)"); }
}
/** Composite – chứa danh sách Component con (có thể là Leaf hoặc Composite khác). */
class Folder implements FileSystemItem {
private final String name;
private final List<FileSystemItem> children = new ArrayList<>();
Folder(String name) { this.name = name; }
void add(FileSystemItem item) { children.add(item); }
void remove(FileSystemItem item) { children.remove(item); }
public String getName() { return name; }
/** Đệ quy: kích thước thư mục = tổng kích thước các con. */
public int getSize() {
int total = 0;
for (FileSystemItem c : children) total += c.getSize();
return total;
}
public void print(String indent) {
System.out.println(indent + "📁 " + name + "/ (" + getSize() + " KB)");
for (FileSystemItem c : children) c.print(indent + " ");
}
}
java 09-composite.java📁 du-an-robot/ (2583 KB)
📁 src/ (20 KB)
📄 main.cpp (12 KB)
📄 motor.cpp (8 KB)
📁 docs/ (2560 KB)
📄 bao-cao.pdf (2048 KB)
📁 images/ (512 KB)
📄 so-do-mach.png (512 KB)
📄 README.md (3 KB)
Tổng dung lượng dự án: 2583 KB
Dung lượng thư mục docs: 2560 KBGiải thích code Java 17+
Folder.getSize()gọigetSize()của từng con — con là file thì trả ngay, con là thư mục thì lại đệ quy tiếp.print()cũng đệ quy, mỗi cấp thụt lề thêm 3 dấu cách → in ra cây thư mục đẹp mắt.- Client chỉ gọi
root.print("")vàroot.getSize(); có thể gọi trên bất kỳ nút nào (ví dụdocs.getSize()).
6Áp dụng trong lập trình nhúng
- Hệ thống menu nhiều cấp trên màn hình LCD/OLED: menu chứa mục chọn và menu con, nút OK/Back duyệt cây.
- Cây giao diện của thư viện đồ hoạ (LVGL:
lv_objchứa cáclv_objcon). - Nhóm cảm biến: “Phòng” chứa nhiều cảm biến;
readAverage()trên phòng = trung bình các con. - Trên MCU RAM nhỏ, có thể dựng cây tĩnh bằng mảng thay vì cấp phát động.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Làm việc với cấu trúc cây phức tạp một cách thuận tiện nhờ đa hình và đệ quy.
- Open/Closed: thêm loại phần tử mới không sửa code client.
- Client đơn giản hơn — không cần phân biệt lá và nhánh.
✘ Nhược điểm
- Khó giới hạn loại phần tử con (ví dụ: thư mục chỉ chứa ảnh) — phải kiểm tra lúc chạy.
- Interface chung có thể trở nên quá tổng quát.
8Khi nào nên dùng
- Dữ liệu có cấu trúc cây: thư mục, menu đa cấp, sơ đồ tổ chức, cây DOM HTML, bản vẽ gồm nhóm hình.
- Muốn client xử lý đối tượng đơn và nhóm như nhau.
9Gặp ở đâu trong thực tế
C++ / nhúng
- LVGL: cây
lv_obj_t - Qt:
QObjectcha – con - ArduinoJson:
JsonObjectchứaJsonVariant
Python
xml.etree.ElementTree- Tkinter: widget chứa widget
astmodule (cây cú pháp)
Java
java.awt.Containerchứa cácComponent(một Panel chứa Button và Panel khác)- Cây DOM:
org.w3c.dom.Node - JavaFX:
Parent/Node
10Liên hệ với các mẫu khác
Builder dùng để tạo cây Composite; Iterator dùng để duyệt cây; Visitor thực hiện thao tác trên toàn cây; Chain of Responsibility thường chạy dọc theo quan hệ cha – con của Composite. Decorator có cấu trúc giống nhưng chỉ có một con.
11Bài tập thực hành
🎁 Decorator — Người trang trí
Mục đích: Gắn thêm hành vi mới cho đối tượng một cách động bằng cách bọc nó trong các đối tượng “trang trí” có cùng interface — một giải pháp linh hoạt thay cho kế thừa.
1Vấn đề cần giải quyết
Quán trà sữa có nhiều đồ uống gốc và hàng chục loại topping (trân châu, kem cheese, pudding…), khách được thêm bao nhiêu tuỳ thích, thêm 2 lần cùng một loại cũng được.
Dùng kế thừa: MilkTeaWithPearl, MilkTeaWithPearlAndCheese, GreenTeaWithPuddingAndDoublePearl… → bùng nổ tổ hợp. Thêm biến boolean hasPearl, hasCheese vào lớp cha → mỗi topping mới phải sửa lớp cha, và không xử lý được “thêm 2 lần”.
2Giải pháp
Tạo lớp Decorator vừa cài đặt interface Drink vừa chứa một Drink bên trong. Mỗi decorator uỷ quyền lời gọi cho đối tượng bên trong rồi thêm hành vi của mình (cộng giá, nối mô tả).
Vì decorator cũng là một Drink, ta có thể bọc lồng nhau nhiều lớp: new Pearl(new CheeseFoam(new MilkTea())). Client vẫn thấy một Drink bình thường.
3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Component (Drink) | Interface chung cho đối tượng gốc và decorator. |
| ConcreteComponent (MilkTea) | Đối tượng gốc được bọc. |
| BaseDecorator (ToppingDecorator) | Giữ tham chiếu tới Component bên trong, mặc định uỷ quyền mọi lời gọi. |
| ConcreteDecorator (Pearl…) | Thêm hành vi trước/sau khi uỷ quyền. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Decorator – C++17
#include <cstdio>
#include <memory>
#include <string>
// ===== Component =====
class Drink {
public:
virtual ~Drink() = default;
virtual std::string getDescription() const = 0;
virtual int cost() const = 0;
};
// ===== Concrete Components =====
class MilkTea : public Drink {
public:
std::string getDescription() const override { return "Trà sữa"; }
int cost() const override { return 25000; }
};
class GreenTea : public Drink {
public:
std::string getDescription() const override { return "Trà xanh"; }
int cost() const override { return 20000; }
};
// ===== Base Decorator: LÀ một Drink và CÓ một Drink =====
class ToppingDecorator : public Drink {
public:
explicit ToppingDecorator(std::unique_ptr<Drink> inner) : inner_(std::move(inner)) {}
std::string getDescription() const override { return inner_->getDescription(); }
int cost() const override { return inner_->cost(); }
protected:
std::unique_ptr<Drink> inner_; // decorator sở hữu đối tượng được bọc
};
// ===== Concrete Decorators =====
class Pearl : public ToppingDecorator {
public:
using ToppingDecorator::ToppingDecorator;
std::string getDescription() const override { return inner_->getDescription() + " + trân châu"; }
int cost() const override { return inner_->cost() + 5000; }
};
class CheeseFoam : public ToppingDecorator {
public:
using ToppingDecorator::ToppingDecorator;
std::string getDescription() const override { return inner_->getDescription() + " + kem cheese"; }
int cost() const override { return inner_->cost() + 10000; }
};
class Pudding : public ToppingDecorator {
public:
using ToppingDecorator::ToppingDecorator;
std::string getDescription() const override { return inner_->getDescription() + " + pudding"; }
int cost() const override { return inner_->cost() + 7000; }
};
void print(const Drink& d) { std::printf("%7d đ %s\n", d.cost(), d.getDescription().c_str()); }
int main() {
using std::make_unique;
auto order1 = make_unique<MilkTea>();
print(*order1);
std::unique_ptr<Drink> order2 = make_unique<Pearl>(make_unique<CheeseFoam>(make_unique<MilkTea>()));
print(*order2);
std::unique_ptr<Drink> order3 =
make_unique<Pearl>(make_unique<Pearl>(make_unique<Pudding>(make_unique<GreenTea>())));
print(*order3);
}
g++ -std=c++17 10-decorator.cpp -o app && ./app25000 đ Trà sữa 40000 đ Trà sữa + kem cheese + trân châu 37000 đ Trà xanh + pudding + trân châu + trân châu
Giải thích code C++17
- Decorator sở hữu đối tượng được bọc qua
std::unique_ptr<Drink> inner_— huỷ lớp ngoài cùng sẽ huỷ cả chồng. make_unique<Pearl>(make_unique<CheeseFoam>(make_unique<MilkTea>()))— đọc từ trong ra ngoài.using ToppingDecorator::ToppingDecorator;kế thừa constructor.
"""Decorator (mẫu thiết kế) – Python 3.
Lưu ý: cú pháp @decorator của Python là một khái niệm LIÊN QUAN nhưng khác:
nó bọc HÀM lúc định nghĩa; còn mẫu Decorator bọc ĐỐI TƯỢNG lúc chạy. Cuối file có ví dụ cả hai."""
from abc import ABC, abstractmethod
import functools
class Drink(ABC): # Component
@abstractmethod
def get_description(self) -> str: ...
@abstractmethod
def cost(self) -> int: ...
class MilkTea(Drink):
def get_description(self): return "Trà sữa"
def cost(self): return 25_000
class GreenTea(Drink):
def get_description(self): return "Trà xanh"
def cost(self): return 20_000
class ToppingDecorator(Drink): # Base Decorator
def __init__(self, inner: Drink): self.inner = inner
def get_description(self): return self.inner.get_description()
def cost(self): return self.inner.cost()
class Pearl(ToppingDecorator):
def get_description(self): return self.inner.get_description() + " + trân châu"
def cost(self): return self.inner.cost() + 5_000
class CheeseFoam(ToppingDecorator):
def get_description(self): return self.inner.get_description() + " + kem cheese"
def cost(self): return self.inner.cost() + 10_000
class Pudding(ToppingDecorator):
def get_description(self): return self.inner.get_description() + " + pudding"
def cost(self): return self.inner.cost() + 7_000
# ----- Cú pháp @decorator của Python: bọc một HÀM -----
def log_call(func):
@functools.wraps(func)
def wrapper(*args):
result = func(*args)
print(f" (log) {func.__name__}{args} = {result}")
return result
return wrapper
@log_call
def read_sensor(pin):
return 512 # giả lập analogRead
if __name__ == "__main__":
for d in (MilkTea(),
Pearl(CheeseFoam(MilkTea())),
Pearl(Pearl(Pudding(GreenTea())))):
print(f"{d.cost():>7,} đ {d.get_description()}")
read_sensor(34)
python 10-decorator.py25,000 đ Trà sữa 40,000 đ Trà sữa + kem cheese + trân châu 37,000 đ Trà xanh + pudding + trân châu + trân châu (log) read_sensor(34,) = 512
Giải thích code Python 3
- Mẫu Decorator (bọc đối tượng lúc chạy) khác với cú pháp
@decoratorcủa Python (bọc hàm lúc định nghĩa) — nhưng cùng một ý tưởng “bọc để thêm hành vi”. - Ví dụ
@log_callở cuối file ghi log mỗi lần đọc cảm biến mà không sửa hàmread_sensor. functools.wrapsgiữ nguyên tên và docstring của hàm gốc.
public class DecoratorDemo {
public static void main(String[] args) {
// Gọi một ly trà sữa cơ bản
Drink order1 = new MilkTea();
print(order1);
// "Bọc" thêm topping – mỗi lớp bọc thêm hành vi + giá tiền
Drink order2 = new Pearl(new CheeseFoam(new MilkTea()));
print(order2);
// Bọc nhiều lần cùng một loại topping cũng được
Drink order3 = new Pearl(new Pearl(new Pudding(new GreenTea())));
print(order3);
}
static void print(Drink d) {
System.out.printf("%-55s %,7d đ%n", d.getDescription(), d.cost());
}
}
/** Component */
interface Drink {
String getDescription();
int cost();
}
/** Concrete Components – đồ uống gốc */
class MilkTea implements Drink {
public String getDescription() { return "Trà sữa"; }
public int cost() { return 25_000; }
}
class GreenTea implements Drink {
public String getDescription() { return "Trà xanh"; }
public int cost() { return 20_000; }
}
/** Base Decorator – cùng kiểu Drink VÀ chứa một Drink bên trong */
abstract class ToppingDecorator implements Drink {
protected final Drink inner;
ToppingDecorator(Drink inner) { this.inner = inner; }
public String getDescription() { return inner.getDescription(); }
public int cost() { return inner.cost(); }
}
/** Concrete Decorators – thêm hành vi TRƯỚC/SAU khi uỷ quyền cho inner */
class Pearl extends ToppingDecorator {
Pearl(Drink inner) { super(inner); }
public String getDescription() { return inner.getDescription() + " + trân châu"; }
public int cost() { return inner.cost() + 5_000; }
}
class CheeseFoam extends ToppingDecorator {
CheeseFoam(Drink inner) { super(inner); }
public String getDescription() { return inner.getDescription() + " + kem cheese"; }
public int cost() { return inner.cost() + 10_000; }
}
class Pudding extends ToppingDecorator {
Pudding(Drink inner) { super(inner); }
public String getDescription() { return inner.getDescription() + " + pudding"; }
public int cost() { return inner.cost() + 7_000; }
}
java 10-decorator.javaTrà sữa 25,000 đ Trà sữa + kem cheese + trân châu 40,000 đ Trà xanh + pudding + trân châu + trân châu 37,000 đ
Giải thích code Java 17+
ToppingDecoratorvừa là Drink (implements) vừa có Drink (fieldinner) — hai mối quan hệ này là “chữ ký” của Decorator.- Lời gọi
cost()đi xuyên qua các lớp vỏ: Pearl → CheeseFoam → MilkTea, rồi cộng dồn ngược ra: 25 000 + 10 000 + 5 000. - Bọc
Pearlhai lần hoàn toàn hợp lệ — điều mà kế thừa không làm được. - Thứ tự bọc quyết định thứ tự xuất hiện trong mô tả.
6Áp dụng trong lập trình nhúng
- Bọc luồng dữ liệu nhiều lớp:
Checksum(Encrypt(Compress(UartStream)))— mỗi lớp một nhiệm vụ, bật/tắt tuỳ cấu hình. - Bọc driver cảm biến để thêm bộ lọc:
MovingAverage(MedianFilter(AdcSensor))— rất hữu ích khi tín hiệu ADC nhiễu. - Thêm ghi log/đo thời gian thực thi cho một driver khi debug mà không sửa driver.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Thêm/bớt trách nhiệm lúc chạy, kết hợp tuỳ ý.
- Tránh bùng nổ lớp con.
- Single Responsibility: mỗi decorator chỉ lo một tính năng.
✘ Nhược điểm
- Khó gỡ một decorator cụ thể ra khỏi giữa chồng bọc.
- Hành vi có thể phụ thuộc thứ tự bọc.
- Nhiều đối tượng nhỏ, khó debug; code khởi tạo dài.
8Khi nào nên dùng
- Cần thêm chức năng cho đối tượng lúc chạy mà không ảnh hưởng đối tượng khác cùng lớp.
- Kế thừa không khả thi (lớp
final) hoặc gây bùng nổ tổ hợp. - Các tính năng có thể bật/tắt độc lập: nén, mã hoá, đệm, ghi log…
9Gặp ở đâu trong thực tế
C++ / nhúng
- Các lớp lọc tín hiệu trong thư viện DSP
- Boost.Iostreams filters
- Arduino
Streambọc bởi lớp đệm/nén
Python
@functools.lru_cache,@property,@staticmethodio.BufferedReaderbọcFileIOgzip.GzipFilebọc file object
Java
java.io:new BufferedReader(new InputStreamReader(new FileInputStream(f)))Collections.unmodifiableList(),Collections.synchronizedList()- Servlet:
HttpServletRequestWrapper
10Liên hệ với các mẫu khác
Adapter đổi interface, Decorator giữ nguyên interface và thêm hành vi. Proxy cũng bọc đối tượng nhưng thường tự quản lý vòng đời đối tượng thật và không xếp chồng. Composite có nhiều con, Decorator chỉ có một. Strategy thay “ruột” của đối tượng, Decorator thay “vỏ”.
11Bài tập thực hành
🏠 Facade — Mặt tiền
Mục đích: Cung cấp một interface đơn giản, thống nhất cho một tập hợp các interface phức tạp trong một hệ thống con.
1Vấn đề cần giải quyết
Ngôi nhà thông minh có đèn, điều hoà, rèm, máy chiếu, loa… Để xem phim, chương trình phải gọi đúng thứ tự 7 thao tác trên 5 hệ thống khác nhau. Mỗi nơi muốn “bật chế độ xem phim” (app điện thoại, nút bấm, trợ lý giọng nói) lại lặp lại đoạn code dài này.
Client bị phụ thuộc chặt vào mọi chi tiết của hệ thống con: thay máy chiếu loại khác → sửa mọi nơi.
2Giải pháp
Tạo lớp Facade (SmartHomeFacade) giữ tham chiếu tới các hệ thống con và cung cấp vài phương thức cấp cao (startMovieMode(), leaveHome()). Facade biết cần gọi ai, theo thứ tự nào.
Client chỉ nói chuyện với Facade. Hệ thống con không biết Facade tồn tại và vẫn có thể được dùng trực tiếp khi cần điều khiển chi tiết.
3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Facade (SmartHomeFacade) | Biết hệ thống con nào xử lý yêu cầu nào; điều phối các lời gọi. |
| Subsystem classes (Lights, Projector…) | Thực hiện công việc thật; không biết tới Facade. |
| Client | Dùng Facade thay vì gọi trực tiếp hệ thống con. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Facade – C++17. Trong firmware: các hàm như digitalWrite(), WiFi.begin()
// chính là facade che giấu hàng chục thao tác thanh ghi phía dưới.
#include <iostream>
#include <string>
// ===== Subsystems – mỗi lớp điều khiển một khối phần cứng =====
class Lights {
public:
void dim(int p) { std::cout << " 💡 Đèn giảm còn " << p << "% (PWM)\n"; }
void off() { std::cout << " 💡 Tắt đèn\n"; }
};
class AirConditioner {
public:
void setTemperature(int c) { std::cout << " ❄️ Điều hoà đặt " << c << "°C (gửi mã IR)\n"; }
void off() { std::cout << " ❄️ Tắt điều hoà\n"; }
};
class Curtains {
public:
void close() { std::cout << " 🪟 Kéo rèm (động cơ bước)\n"; }
};
class Projector {
public:
void on() { std::cout << " 📽️ Bật máy chiếu\n"; }
void setInput(const std::string& s){ std::cout << " 📽️ Chọn nguồn " << s << "\n"; }
void off() { std::cout << " 📽️ Tắt máy chiếu\n"; }
};
class SoundSystem {
public:
void on() { std::cout << " 🔊 Bật loa\n"; }
void setVolume(int v) { std::cout << " 🔊 Âm lượng " << v << "\n"; }
void off() { std::cout << " 🔊 Tắt loa\n"; }
};
// ===== Facade =====
class SmartHomeFacade {
public:
SmartHomeFacade(Lights& l, AirConditioner& a, Curtains& c, Projector& p, SoundSystem& s)
: lights_(l), ac_(a), curtains_(c), projector_(p), sound_(s) {}
void startMovieMode(const std::string& movie) {
std::cout << "🎬 Chế độ xem phim: " << movie << "\n";
curtains_.close();
lights_.dim(10);
ac_.setTemperature(26);
projector_.on();
projector_.setInput("HDMI-1");
sound_.on();
sound_.setVolume(40);
}
void leaveHome() {
std::cout << "🚪 Chế độ ra khỏi nhà\n";
projector_.off();
sound_.off();
ac_.off();
lights_.off();
}
private:
Lights& lights_;
AirConditioner& ac_;
Curtains& curtains_;
Projector& projector_;
SoundSystem& sound_;
};
int main() {
// Các đối tượng phần cứng tạo tĩnh một lần (thường là biến toàn cục trong sketch Arduino)
Lights lights; AirConditioner ac; Curtains curtains; Projector projector; SoundSystem sound;
SmartHomeFacade home(lights, ac, curtains, projector, sound);
home.startMovieMode("Big Hero 6");
std::cout << "-----\n";
home.leaveHome();
}
g++ -std=c++17 11-facade.cpp -o app && ./app🎬 Chế độ xem phim: Big Hero 6 🪟 Kéo rèm (động cơ bước) 💡 Đèn giảm còn 10% (PWM) ❄️ Điều hoà đặt 26°C (gửi mã IR) 📽️ Bật máy chiếu 📽️ Chọn nguồn HDMI-1 🔊 Bật loa 🔊 Âm lượng 40 ----- 🚪 Chế độ ra khỏi nhà 📽️ Tắt máy chiếu 🔊 Tắt loa ❄️ Tắt điều hoà 💡 Tắt đèn
Giải thích code C++17
- Facade giữ tham chiếu tới các hệ thống con — các đối tượng phần cứng được tạo tĩnh một lần trong
main(). - Mỗi hệ thống con có chú thích cơ chế thật (PWM, mã IR, động cơ bước) để thấy Facade che giấu bao nhiêu chi tiết.
"""Facade – Python 3."""
class Lights:
def dim(self, p): print(f" 💡 Đèn giảm còn {p}%")
def off(self): print(" 💡 Tắt đèn")
class AirConditioner:
def set_temperature(self, c): print(f" ❄️ Điều hoà đặt {c}°C")
def off(self): print(" ❄️ Tắt điều hoà")
class Curtains:
def close(self): print(" 🪟 Kéo rèm")
class Projector:
def on(self): print(" 📽️ Bật máy chiếu")
def set_input(self, s): print(f" 📽️ Chọn nguồn {s}")
def off(self): print(" 📽️ Tắt máy chiếu")
class SoundSystem:
def on(self): print(" 🔊 Bật loa")
def set_volume(self, v): print(f" 🔊 Âm lượng {v}")
def off(self): print(" 🔊 Tắt loa")
class SmartHomeFacade:
def __init__(self):
# Facade có thể tự tạo các hệ thống con (hoặc nhận từ bên ngoài)
self.lights, self.ac, self.curtains = Lights(), AirConditioner(), Curtains()
self.projector, self.sound = Projector(), SoundSystem()
def start_movie_mode(self, movie):
print(f"🎬 Chế độ xem phim: {movie}")
self.curtains.close()
self.lights.dim(10)
self.ac.set_temperature(26)
self.projector.on()
self.projector.set_input("HDMI-1")
self.sound.on()
self.sound.set_volume(40)
def leave_home(self):
print("🚪 Chế độ ra khỏi nhà")
for device in (self.projector, self.sound, self.ac, self.lights):
device.off()
if __name__ == "__main__":
home = SmartHomeFacade()
home.start_movie_mode("Big Hero 6")
print("-----")
home.leave_home()
python 11-facade.py🎬 Chế độ xem phim: Big Hero 6 🪟 Kéo rèm 💡 Đèn giảm còn 10% ❄️ Điều hoà đặt 26°C 📽️ Bật máy chiếu 📽️ Chọn nguồn HDMI-1 🔊 Bật loa 🔊 Âm lượng 40 ----- 🚪 Chế độ ra khỏi nhà 📽️ Tắt máy chiếu 🔊 Tắt loa ❄️ Tắt điều hoà 💡 Tắt đèn
Giải thích code Python 3
- Facade tự tạo các hệ thống con trong
__init__— cách đơn giản khi không cần thay thế chúng. leave_home()gọioff()trên một tuple thiết bị nhờ duck typing.
public class FacadeDemo {
public static void main(String[] args) {
// Không có Facade: client phải tự gọi đúng thứ tự 6–7 hệ thống con.
// Có Facade: chỉ 1 dòng lệnh.
SmartHomeFacade home = new SmartHomeFacade(
new Lights(), new AirConditioner(), new Curtains(), new Projector(), new SoundSystem());
home.startMovieMode("Big Hero 6");
System.out.println("-----");
home.leaveHome();
}
}
// ===== Các hệ thống con (subsystems) – mỗi lớp có API riêng, chi tiết =====
class Lights {
void dim(int percent) { System.out.println(" 💡 Đèn giảm còn " + percent + "%"); }
void off() { System.out.println(" 💡 Tắt đèn"); }
}
class AirConditioner {
void setTemperature(int c) { System.out.println(" ❄️ Điều hoà đặt " + c + "°C"); }
void off() { System.out.println(" ❄️ Tắt điều hoà"); }
}
class Curtains {
void close() { System.out.println(" 🪟 Kéo rèm"); }
}
class Projector {
void on() { System.out.println(" 📽️ Bật máy chiếu"); }
void setInput(String src) { System.out.println(" 📽️ Chọn nguồn " + src); }
void off() { System.out.println(" 📽️ Tắt máy chiếu"); }
}
class SoundSystem {
void on() { System.out.println(" 🔊 Bật loa"); }
void setVolume(int v) { System.out.println(" 🔊 Âm lượng " + v); }
void off() { System.out.println(" 🔊 Tắt loa"); }
}
// ===== Facade – một giao diện đơn giản che giấu sự phức tạp phía sau =====
class SmartHomeFacade {
private final Lights lights;
private final AirConditioner ac;
private final Curtains curtains;
private final Projector projector;
private final SoundSystem sound;
SmartHomeFacade(Lights l, AirConditioner a, Curtains c, Projector p, SoundSystem s) {
this.lights = l; this.ac = a; this.curtains = c; this.projector = p; this.sound = s;
}
void startMovieMode(String movie) {
System.out.println("🎬 Chế độ xem phim: " + movie);
curtains.close();
lights.dim(10);
ac.setTemperature(26);
projector.on();
projector.setInput("HDMI-1");
sound.on();
sound.setVolume(40);
}
void leaveHome() {
System.out.println("🚪 Chế độ ra khỏi nhà");
projector.off();
sound.off();
ac.off();
lights.off();
}
}
java 11-facade.java🎬 Chế độ xem phim: Big Hero 6 🪟 Kéo rèm 💡 Đèn giảm còn 10% ❄️ Điều hoà đặt 26°C 📽️ Bật máy chiếu 📽️ Chọn nguồn HDMI-1 🔊 Bật loa 🔊 Âm lượng 40 ----- 🚪 Chế độ ra khỏi nhà 📽️ Tắt máy chiếu 🔊 Tắt loa ❄️ Tắt điều hoà 💡 Tắt đèn
Giải thích code Java 17+
main()chỉ gọi 2 phương thức của Facade, trong khi Facade gọi 11 thao tác trên 5 hệ thống con.- Thứ tự hợp lý (kéo rèm trước khi giảm đèn, bật máy chiếu trước khi chọn nguồn) được đóng gói một chỗ.
- Các lớp hệ thống con hoàn toàn không biết
SmartHomeFacadetồn tại.
6Áp dụng trong lập trình nhúng
- Gần như mọi thư viện Arduino là Facade:
WiFi.begin(ssid, pass)che giấu hàng trăm dòng ESP-IDF (event loop, netif, DHCP…). digitalWrite(pin, HIGH)che giấu thao tác thanh ghiGPIOx->BSRR.- Trong dự án robot: lớp
Robotvớiforward(),turnLeft()là facade cho 2 driver động cơ + encoder + IMU.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Cô lập client khỏi sự phức tạp của hệ thống con.
- Giảm phụ thuộc (loose coupling) — thay đổi hệ thống con ít ảnh hưởng client.
- Dễ dùng, dễ học cho người mới.
✘ Nhược điểm
- Facade có thể phình to thành “God object” ôm mọi thứ.
- Che giấu quá kỹ khiến người dùng nâng cao khó tuỳ biến (nên vẫn cho phép truy cập trực tiếp hệ thống con).
8Khi nào nên dùng
- Cần interface đơn giản cho một hệ thống con phức tạp.
- Muốn phân tầng hệ thống: mỗi tầng giao tiếp với tầng khác qua một facade.
- Bọc thư viện bên thứ ba phức tạp để dễ thay thế sau này.
9Gặp ở đâu trong thực tế
C++ / nhúng
- Arduino API (
digitalWrite,analogRead,WiFi) - ESP-IDF
esp_wifi - Thư viện
Servo.h
Python
requests.get()che giấuurllib3shutil.copytree()- MicroPython
network.WLAN
Java
javax.faces.context.FacesContext- SLF4J
LoggerFactory - Tầng Service trong kiến trúc MVC/Spring — một facade cho nhiều Repository
- Thư viện
Arduino.h:digitalWrite()che giấu thao tác thanh ghi phức tạp
10Liên hệ với các mẫu khác
Adapter bọc một đối tượng và đổi interface; Facade định nghĩa interface mới cho cả hệ thống con. Mediator cũng điều phối nhiều đối tượng, nhưng các đối tượng biết Mediator và giao tiếp hai chiều. Facade thường là Singleton.
11Bài tập thực hành
🪶 Flyweight — Hạng ruồi / Dùng chung
Mục đích: Tiết kiệm bộ nhớ khi có số lượng rất lớn đối tượng giống nhau, bằng cách chia sẻ phần trạng thái chung giữa chúng thay vì mỗi đối tượng giữ một bản riêng.
1Vấn đề cần giải quyết
Game cần vẽ một khu rừng 100 000 cây. Mỗi đối tượng Tree chứa toạ độ (x, y) và dữ liệu loại cây: tên, màu, texture, mô hình 3D… (giả sử ~2 KB).
100 000 × 2 KB ≈ 200 MB RAM — trong khi thực tế chỉ có 3 loại cây! Phần lớn bộ nhớ chứa dữ liệu giống hệt nhau bị nhân bản.
2Giải pháp
Tách trạng thái của đối tượng thành hai phần:
- Intrinsic (nội tại): dữ liệu chung, không đổi (tên, màu, texture) → đưa vào đối tượng Flyweight (
TreeType), được dùng chung. - Extrinsic (ngoại tại): dữ liệu riêng của từng đối tượng (x, y) → giữ trong đối tượng ngữ cảnh nhỏ gọn (
Tree) hoặc truyền vào khi gọi phương thức.
Một Flyweight Factory quản lý bộ đệm: yêu cầu loại cây nào thì trả về bản đã có, chỉ tạo mới khi chưa tồn tại.
3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Flyweight (TreeType) | Chứa trạng thái intrinsic, bất biến, dùng chung. |
| FlyweightFactory (TreeFactory) | Tạo và cache flyweight, đảm bảo không trùng lặp. |
| Context (Tree) | Chứa trạng thái extrinsic + tham chiếu tới flyweight. |
| Client (Forest) | Tính toán/lưu trạng thái extrinsic, lấy flyweight qua factory. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Flyweight – C++17. Trên MCU có vài chục KB RAM, Flyweight rất đáng giá:
// ví dụ font chữ/bitmap icon nằm 1 lần trong Flash, mỗi ký tự trên màn hình chỉ giữ (x, y, con trỏ).
#include <cstdio>
#include <map>
#include <memory>
#include <random>
#include <string>
#include <vector>
// ===== Flyweight – trạng thái NỘI TẠI, dùng chung, bất biến =====
class TreeType {
public:
static constexpr size_t APPROX_BYTES = 2000; // giả sử texture ~2KB
TreeType(std::string name, std::string color) : name_(std::move(name)), color_(std::move(color)) {}
void draw(int x, int y) const { // trạng thái NGOẠI TẠI truyền vào
std::printf(" Vẽ cây %s (%s) tại (%d, %d)\n", name_.c_str(), color_.c_str(), x, y);
}
private:
const std::string name_, color_;
};
// ===== Flyweight Factory =====
class TreeFactory {
public:
static const TreeType* get(const std::string& name, const std::string& color) {
auto key = name + "|" + color;
auto it = cache().find(key);
if (it == cache().end())
it = cache().emplace(key, std::make_unique<TreeType>(name, color)).first;
return it->second.get();
}
static size_t count() { return cache().size(); }
private:
static std::map<std::string, std::unique_ptr<TreeType>>& cache() {
static std::map<std::string, std::unique_ptr<TreeType>> c;
return c;
}
};
// ===== Context – rất nhỏ: 2 số nguyên + 1 con trỏ =====
struct Tree {
short x, y; // tiết kiệm: short thay vì int
const TreeType* type; // con trỏ tới flyweight dùng chung
void draw() const { type->draw(x, y); }
};
int main() {
std::vector<Tree> forest;
forest.reserve(100000);
std::mt19937 rng(42); // seed cố định -> kết quả lặp lại
const char* kinds[][2] = { {"Phượng", "đỏ"}, {"Bàng", "xanh đậm"}, {"Thông", "xanh lá"} };
for (int i = 0; i < 100000; ++i) {
auto& k = kinds[rng() % 3];
forest.push_back({ short(rng() % 1000), short(rng() % 1000), TreeFactory::get(k[0], k[1]) });
}
for (int i = 0; i < 3; ++i) forest[i].draw();
std::printf("Số cây đã trồng : %zu\n", forest.size());
std::printf("Số TreeType thực sự tạo : %zu\n", TreeFactory::count());
std::printf("sizeof(Tree) : %zu byte\n", sizeof(Tree));
size_t without = forest.size() * (sizeof(Tree) + TreeType::APPROX_BYTES);
size_t with = forest.size() * sizeof(Tree) + TreeFactory::count() * TreeType::APPROX_BYTES;
std::printf("Bộ nhớ ước tính KHÔNG dùng Flyweight: %zu KB\n", without / 1024);
std::printf("Bộ nhớ ước tính CÓ dùng Flyweight : %zu KB\n", with / 1024);
}
g++ -std=c++17 12-flyweight.cpp -o app && ./appVẽ cây Phượng (đỏ) tại (67, 876) Vẽ cây Bàng (xanh đậm) tại (26, 335) Vẽ cây Bàng (xanh đậm) tại (924, 950) Số cây đã trồng : 100000 Số TreeType thực sự tạo : 3 sizeof(Tree) : 16 byte Bộ nhớ ước tính KHÔNG dùng Flyweight: 196875 KB Bộ nhớ ước tính CÓ dùng Flyweight : 1568 KB
Giải thích code C++17
Treechỉ cònshort x, y+ một con trỏ —sizeof(Tree)in ra 16 byte (8 byte con trỏ + padding).TreeFactorytrả vềconst TreeType*: con trỏ tới hằng nhấn mạnh flyweight là bất biến.std::mt19937 rng(42)seed cố định để kết quả lặp lại.
"""Flyweight – Python 3. functools.lru_cache biến một hàm tạo đối tượng thành Flyweight Factory.
__slots__ giúp mỗi đối tượng nhỏ gọn hơn (không có __dict__) – hữu ích cả trên MicroPython."""
import random
import sys
from functools import lru_cache
class TreeType: # Flyweight – intrinsic
__slots__ = ("name", "color")
def __init__(self, name, color):
self.name, self.color = name, color
def draw(self, x, y):
print(f" Vẽ cây {self.name} ({self.color}) tại ({x}, {y})")
@lru_cache(maxsize=None) # Flyweight Factory
def get_tree_type(name, color):
return TreeType(name, color)
class Tree: # Context – extrinsic
__slots__ = ("x", "y", "type")
def __init__(self, x, y, tree_type):
self.x, self.y, self.type = x, y, tree_type
def draw(self): self.type.draw(self.x, self.y)
if __name__ == "__main__":
rnd = random.Random(42)
kinds = [("Phượng", "đỏ"), ("Bàng", "xanh đậm"), ("Thông", "xanh lá")]
forest = []
for _ in range(100_000):
name, color = rnd.choice(kinds)
forest.append(Tree(rnd.randrange(1000), rnd.randrange(1000), get_tree_type(name, color)))
for t in forest[:3]:
t.draw()
info = get_tree_type.cache_info()
print("Số cây đã trồng :", len(forest))
print("Số TreeType thực sự tạo :", info.currsize)
print("Số lần lấy lại từ cache :", info.hits)
print("sys.getsizeof(Tree) :", sys.getsizeof(forest[0]), "byte")
python 12-flyweight.pyVẽ cây Thông (xanh lá) tại (114, 25) Vẽ cây Thông (xanh lá) tại (281, 250) Vẽ cây Phượng (đỏ) tại (142, 754) Số cây đã trồng : 100000 Số TreeType thực sự tạo : 3 Số lần lấy lại từ cache : 99997 sys.getsizeof(Tree) : 56 byte
Giải thích code Python 3
@lru_cache(maxsize=None)biến hàm thành Flyweight Factory chỉ với một dòng;cache_info()cho biết 99 997 lần lấy lại từ cache.__slots__loại bỏ__dict__của từng đối tượng — giảm đáng kể bộ nhớ khi có hàng trăm nghìn đối tượng (cũng hữu ích trên MicroPython).
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
public class FlyweightDemo {
public static void main(String[] args) {
Forest forest = new Forest();
Random rnd = new Random(42); // seed cố định để kết quả lặp lại được
String[][] kinds = {
{"Phượng", "đỏ"}, {"Bàng", "xanh đậm"}, {"Thông", "xanh lá"}
};
// Trồng 100 000 cây nhưng chỉ có 3 loại cây
for (int i = 0; i < 100_000; i++) {
String[] k = kinds[rnd.nextInt(kinds.length)];
forest.plantTree(rnd.nextInt(1000), rnd.nextInt(1000), k[0], k[1]);
}
forest.drawFirst(3);
System.out.println("Số cây đã trồng : " + forest.size());
System.out.println("Số TreeType thực sự tạo : " + TreeFactory.count());
long withoutFlyweight = forest.size() * (8L + TreeType.APPROX_BYTES);
long withFlyweight = forest.size() * 8L + TreeFactory.count() * TreeType.APPROX_BYTES;
System.out.printf("Bộ nhớ ước tính KHÔNG dùng Flyweight: %,d KB%n", withoutFlyweight / 1024);
System.out.printf("Bộ nhớ ước tính CÓ dùng Flyweight : %,d KB%n", withFlyweight / 1024);
}
}
/** Flyweight – chứa trạng thái NỘI TẠI (intrinsic), dùng chung, bất biến. */
final class TreeType {
static final int APPROX_BYTES = 2_000; // giả sử texture/mesh nặng ~2KB
private final String name;
private final String color;
TreeType(String name, String color) { this.name = name; this.color = color; }
/** Trạng thái NGOẠI TẠI (extrinsic) – x, y – được truyền vào khi dùng. */
void draw(int x, int y) { System.out.println(" Vẽ cây " + name + " (" + color + ") tại (" + x + ", " + y + ")"); }
}
/** Flyweight Factory – đảm bảo mỗi loại chỉ tạo một lần. */
final class TreeFactory {
private static final Map<String, TreeType> cache = new HashMap<>();
static TreeType get(String name, String color) {
return cache.computeIfAbsent(name + "|" + color, k -> new TreeType(name, color));
}
static int count() { return cache.size(); }
}
/** Context – đối tượng nhỏ, chỉ giữ trạng thái ngoại tại + tham chiếu tới flyweight. */
final class Tree {
private final int x, y;
private final TreeType type;
Tree(int x, int y, TreeType type) { this.x = x; this.y = y; this.type = type; }
void draw() { type.draw(x, y); }
}
/** Client */
final class Forest {
private final List<Tree> trees = new ArrayList<>();
void plantTree(int x, int y, String name, String color) {
trees.add(new Tree(x, y, TreeFactory.get(name, color)));
}
void drawFirst(int n) { for (int i = 0; i < n; i++) trees.get(i).draw(); }
int size() { return trees.size(); }
}
java 12-flyweight.javaVẽ cây Thông (xanh lá) tại (763, 248) Vẽ cây Thông (xanh lá) tại (970, 525) Vẽ cây Thông (xanh lá) tại (918, 519) Số cây đã trồng : 100000 Số TreeType thực sự tạo : 3 Bộ nhớ ước tính KHÔNG dùng Flyweight: 196,093 KB Bộ nhớ ước tính CÓ dùng Flyweight : 787 KB
Giải thích code Java 17+
- 100 000 cây được trồng nhưng
TreeFactory.count()chỉ là 3. computeIfAbsentgiúp factory chỉ tạoTreeTypekhi khoá chưa có trong cache.- Ước tính bộ nhớ giảm từ ~196 MB xuống dưới 1 MB (mỗi
Treechỉ còn giữ 2 số nguyên + 1 tham chiếu). Random(42)với seed cố định giúp mỗi lần chạy cho cùng kết quả — thuận tiện khi kiểm tra.
6Áp dụng trong lập trình nhúng
- Font chữ và icon bitmap trên màn hình OLED/TFT: dữ liệu glyph nằm một lần trong Flash (
PROGMEM/const), mỗi ký tự hiển thị chỉ lưu vị trí + con trỏ. - Chuỗi hằng trong Flash (
F("...")trên AVR) thay vì nhân bản vào RAM. - Bảng tra (lookup table) dùng chung: sin/cos, hệ số lọc, bảng CRC.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Giảm mạnh bộ nhớ khi có rất nhiều đối tượng tương tự.
✘ Nhược điểm
- Code phức tạp hơn — phải tách trạng thái.
- Có thể tốn CPU hơn nếu phải tính lại trạng thái extrinsic mỗi lần dùng.
- Chỉ đáng dùng khi số lượng đối tượng thực sự lớn.
8Khi nào nên dùng
- Ứng dụng tạo cực nhiều đối tượng và đang thiếu RAM.
- Phần lớn trạng thái của đối tượng có thể đưa ra ngoài (extrinsic).
- Ví dụ: ký tự trong trình soạn thảo, hạt/viên đạn/cây trong game, ô bản đồ (tile).
9Gặp ở đâu trong thực tế
C++ / nhúng
- Font của Adafruit_GFX / U8g2 lưu trong
PROGMEM std::string_viewtrỏ vào chuỗi dùng chung- Bảng CRC
consttrong Flash
Python
sys.intern()- Cache số nguyên nhỏ (-5…256) của CPython
functools.lru_cache
Java
Stringpool /String.intern()Integer.valueOf(int),Boolean.valueOf(),Character.valueOf()cache- Font glyph trong các trình soạn thảo văn bản
10Liên hệ với các mẫu khác
Factory của Flyweight thường dùng Singleton. Các nút lá của Composite có thể là Flyweight để tiết kiệm bộ nhớ. Khác Singleton: Flyweight có nhiều thể hiện (mỗi loại một), và luôn bất biến.
11Bài tập thực hành
🛡️ Proxy — Người đại diện
Mục đích: Cung cấp một vật thay thế (đại diện) cho đối tượng khác để kiểm soát việc truy cập tới nó — cho phép làm gì đó trước hoặc sau khi yêu cầu tới được đối tượng thật.
1Vấn đề cần giải quyết
Một số đối tượng cần được “canh cửa”:
- Ảnh 4K rất nặng — tải tất cả lúc mở album thì chậm, trong khi người dùng có thể không bao giờ xem tới.
- Dịch vụ điểm số — học sinh chỉ được xem, giáo viên mới được sửa.
- Truy vấn cơ sở dữ liệu chậm — cùng một câu hỏi lặp lại nhiều lần.
Nhét các logic này vào chính lớp gốc sẽ làm nó phình to và vi phạm Single Responsibility; bắt client tự kiểm tra thì lặp code khắp nơi.
2Giải pháp
Tạo lớp Proxy cài đặt cùng interface với đối tượng thật và giữ tham chiếu tới nó. Client dùng proxy như dùng đối tượng thật; proxy làm thêm việc của mình rồi mới (hoặc không) chuyển yêu cầu.
| Loại proxy | Mục đích |
|---|---|
| Virtual proxy | Khởi tạo trễ đối tượng nặng (lazy loading) |
| Protection proxy | Kiểm tra quyền truy cập |
| Caching proxy | Lưu kết quả để trả nhanh lần sau |
| Remote proxy | Đại diện cho đối tượng ở máy khác (RMI, gRPC) |
| Logging proxy | Ghi lại lịch sử các lời gọi |
3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Subject (Image) | Interface chung của Proxy và RealSubject. |
| RealSubject (RealImage) | Đối tượng thật thực hiện công việc chính. |
| Proxy (LazyImageProxy) | Giữ tham chiếu tới RealSubject, kiểm soát truy cập, có thể tạo/huỷ RealSubject. |
| Client | Làm việc với Subject, không phân biệt proxy hay đối tượng thật. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Proxy – C++17
#include <cstdio>
#include <iostream>
#include <map>
#include <memory>
#include <optional>
#include <string>
// ===== Ví dụ 1: Virtual Proxy =====
class Image {
public:
virtual ~Image() = default;
virtual void display() = 0;
};
class RealImage : public Image {
public:
explicit RealImage(std::string file) : file_(std::move(file)) {
std::cout << " ⏳ Đang tải " << file_ << " từ thẻ SD (rất chậm)...\n";
}
void display() override { std::cout << " 🖼️ Hiển thị " << file_ << "\n"; }
private:
std::string file_;
};
class LazyImageProxy : public Image {
public:
explicit LazyImageProxy(std::string file) : file_(std::move(file)) {}
void display() override {
if (!real_) real_ = std::make_unique<RealImage>(file_); // chỉ tạo khi cần
real_->display();
}
private:
std::string file_;
std::unique_ptr<RealImage> real_;
};
// ===== Ví dụ 2: Protection + Caching Proxy =====
class ScoreService {
public:
virtual ~ScoreService() = default;
virtual std::string getScore(const std::string& student) = 0;
virtual void updateScore(const std::string& student, double score) = 0;
};
class RealScoreService : public ScoreService {
public:
std::string getScore(const std::string& s) override {
std::cout << " (truy vấn cơ sở dữ liệu...)\n";
char buf[64];
std::snprintf(buf, sizeof buf, "%s: %.1f", s.c_str(), db_[s]);
return buf;
}
void updateScore(const std::string& s, double v) override {
db_[s] = v;
std::cout << " Đã cập nhật điểm " << s << " = " << v << "\n";
}
private:
std::map<std::string, double> db_{ {"An", 8.0} };
};
class ScoreServiceProxy : public ScoreService {
public:
ScoreServiceProxy(RealScoreService& real, std::string role) : real_(real), role_(std::move(role)) {}
std::string getScore(const std::string& s) override { // caching
auto it = cache_.find(s);
if (it != cache_.end()) return it->second;
return cache_[s] = real_.getScore(s);
}
void updateScore(const std::string& s, double v) override { // protection
if (role_ != "giaovien") {
std::cout << " ⛔ " << role_ << " không có quyền sửa điểm!\n";
return;
}
real_.updateScore(s, v);
cache_.erase(s); // làm mới cache
}
private:
RealScoreService& real_;
std::string role_;
std::map<std::string, std::string> cache_;
};
int main() {
LazyImageProxy photo("anh-lop-hoc-4k.jpg");
std::cout << "Đã tạo proxy, ảnh CHƯA được tải.\n";
photo.display();
photo.display();
std::cout << "-----\n";
RealScoreService db;
ScoreServiceProxy teacher(db, "giaovien");
ScoreServiceProxy student(db, "hocsinh");
std::cout << teacher.getScore("An") << "\n";
std::cout << teacher.getScore("An") << "\n";
teacher.updateScore("An", 9.5);
std::cout << teacher.getScore("An") << "\n";
student.updateScore("An", 10);
}
g++ -std=c++17 13-proxy.cpp -o app && ./appĐã tạo proxy, ảnh CHƯA được tải. ⏳ Đang tải anh-lop-hoc-4k.jpg từ thẻ SD (rất chậm)... 🖼️ Hiển thị anh-lop-hoc-4k.jpg 🖼️ Hiển thị anh-lop-hoc-4k.jpg ----- (truy vấn cơ sở dữ liệu...) An: 8.0 An: 8.0 Đã cập nhật điểm An = 9.5 (truy vấn cơ sở dữ liệu...) An: 9.5 ⛔ hocsinh không có quyền sửa điểm!
Giải thích code C++17
- Virtual proxy giữ
std::unique_ptr<RealImage>rỗng cho tới lầndisplay()đầu tiên. - Protection + caching proxy giữ tham chiếu tới dịch vụ thật dùng chung; cache là
std::map. - Smart pointer trong C++ (
unique_ptr,shared_ptr) bản thân chúng chính là một dạng proxy.
"""Proxy – Python 3."""
from abc import ABC, abstractmethod
class Image(ABC):
@abstractmethod
def display(self): ...
class RealImage(Image):
def __init__(self, file):
self.file = file
print(f" ⏳ Đang tải {file} từ ổ đĩa (rất chậm)...")
def display(self): print(f" 🖼️ Hiển thị {self.file}")
class LazyImageProxy(Image):
def __init__(self, file):
self.file = file
self._real = None
def display(self):
if self._real is None:
self._real = RealImage(self.file)
self._real.display()
class RealScoreService:
def __init__(self): self._db = {"An": 8.0}
def get_score(self, s):
print(" (truy vấn cơ sở dữ liệu...)")
return f"{s}: {self._db[s]}"
def update_score(self, s, v):
self._db[s] = v
print(f" Đã cập nhật điểm {s} = {v}")
class ScoreServiceProxy:
def __init__(self, real, role):
self._real, self._role, self._cache = real, role, {}
def get_score(self, s):
if s not in self._cache:
self._cache[s] = self._real.get_score(s)
return self._cache[s]
def update_score(self, s, v):
if self._role != "giaovien":
print(f" ⛔ {self._role} không có quyền sửa điểm!")
return
self._real.update_score(s, v)
self._cache.pop(s, None)
def __getattr__(self, name):
"""Mọi phương thức khác chưa định nghĩa -> chuyển thẳng cho đối tượng thật."""
return getattr(self._real, name)
if __name__ == "__main__":
photo = LazyImageProxy("anh-lop-hoc-4k.jpg")
print("Đã tạo proxy, ảnh CHƯA được tải.")
photo.display()
photo.display()
print("-----")
db = RealScoreService()
teacher, student = ScoreServiceProxy(db, "giaovien"), ScoreServiceProxy(db, "hocsinh")
print(teacher.get_score("An"))
print(teacher.get_score("An"))
teacher.update_score("An", 9.5)
print(teacher.get_score("An"))
student.update_score("An", 10)
python 13-proxy.pyĐã tạo proxy, ảnh CHƯA được tải. ⏳ Đang tải anh-lop-hoc-4k.jpg từ ổ đĩa (rất chậm)... 🖼️ Hiển thị anh-lop-hoc-4k.jpg 🖼️ Hiển thị anh-lop-hoc-4k.jpg ----- (truy vấn cơ sở dữ liệu...) An: 8.0 An: 8.0 Đã cập nhật điểm An = 9.5 (truy vấn cơ sở dữ liệu...) An: 9.5 ⛔ hocsinh không có quyền sửa điểm!
Giải thích code Python 3
__getattr__chuyển mọi thuộc tính/phương thức chưa định nghĩa sang đối tượng thật — viết proxy tổng quát rất gọn.- Proxy không cần kế thừa từ lớp thật nhờ duck typing.
import java.util.HashMap;
import java.util.Map;
public class ProxyDemo {
public static void main(String[] args) {
// 1. Virtual Proxy – trì hoãn việc tải ảnh nặng tới khi thực sự cần hiển thị
Image photo = new LazyImageProxy("anh-lop-hoc-4k.jpg");
System.out.println("Đã tạo proxy, ảnh CHƯA được tải.");
photo.display(); // lần đầu: tải rồi hiển thị
photo.display(); // lần sau: dùng lại
System.out.println("-----");
// 2. Protection + Caching Proxy – kiểm tra quyền và cache kết quả
ScoreService student = new ScoreServiceProxy(new RealScoreService(), "hocsinh");
ScoreService teacher = new ScoreServiceProxy(new RealScoreService(), "giaovien");
System.out.println(teacher.getScore("An"));
System.out.println(teacher.getScore("An")); // lấy từ cache, không gọi DB
teacher.updateScore("An", 9.5);
System.out.println(teacher.getScore("An")); // cache đã bị xoá sau khi cập nhật
student.updateScore("An", 10); // bị chặn
}
}
// ===== Ví dụ 1: Virtual Proxy =====
interface Image { void display(); }
class RealImage implements Image {
private final String file;
RealImage(String file) {
this.file = file;
System.out.println(" ⏳ Đang tải " + file + " từ ổ đĩa (rất chậm)...");
}
public void display() { System.out.println(" 🖼️ Hiển thị " + file); }
}
class LazyImageProxy implements Image {
private final String file;
private RealImage real; // chưa tạo
LazyImageProxy(String file) { this.file = file; }
public void display() {
if (real == null) real = new RealImage(file); // chỉ tạo khi cần
real.display();
}
}
// ===== Ví dụ 2: Protection + Caching Proxy =====
interface ScoreService {
String getScore(String student);
void updateScore(String student, double score);
}
class RealScoreService implements ScoreService {
private final Map<String, Double> db = new HashMap<>(Map.of("An", 8.0));
public String getScore(String s) {
System.out.println(" (truy vấn cơ sở dữ liệu...)");
return s + ": " + db.get(s);
}
public void updateScore(String s, double v) { db.put(s, v); System.out.println(" Đã cập nhật điểm " + s + " = " + v); }
}
class ScoreServiceProxy implements ScoreService {
private final RealScoreService real;
private final String role;
private final Map<String, String> cache = new HashMap<>();
ScoreServiceProxy(RealScoreService real, String role) { this.real = real; this.role = role; }
public String getScore(String s) {
return cache.computeIfAbsent(s, real::getScore); // caching
}
public void updateScore(String s, double v) {
if (!role.equals("giaovien")) { // protection
System.out.println(" ⛔ " + role + " không có quyền sửa điểm!");
return;
}
real.updateScore(s, v);
cache.remove(s); // làm mới cache
}
}
java 13-proxy.javaĐã tạo proxy, ảnh CHƯA được tải. ⏳ Đang tải anh-lop-hoc-4k.jpg từ ổ đĩa (rất chậm)... 🖼️ Hiển thị anh-lop-hoc-4k.jpg 🖼️ Hiển thị anh-lop-hoc-4k.jpg ----- (truy vấn cơ sở dữ liệu...) An: 8.0 An: 8.0 Đã cập nhật điểm An = 9.5 (truy vấn cơ sở dữ liệu...) An: 9.5 ⛔ hocsinh không có quyền sửa điểm!
Giải thích code Java 17+
- Tạo
LazyImageProxygần như không tốn gì; ảnh chỉ thực sự được tải ở lầndisplay()đầu tiên. ScoreServiceProxykết hợp hai vai trò: caching (lần gọi thứ 2 không truy vấn DB) và protection (học sinh bị chặn khi sửa điểm).- Sau khi cập nhật điểm, proxy xoá cache để lần đọc sau lấy dữ liệu mới — đây là vấn đề “cache invalidation” kinh điển.
6Áp dụng trong lập trình nhúng
- Remote proxy: lớp
RemoteSensortrên điện thoại/PC có cùng interface với cảm biến, nhưng bên trong gửi lệnh qua Bluetooth/MQTT tới ESP32. - Caching proxy: đọc cảm biến chậm (DHT22 cần ≥ 2 giây giữa hai lần đọc) → proxy trả giá trị cũ nếu chưa đủ thời gian.
- Protection proxy: chặn ghi vào vùng Flash/EEPROM quan trọng khi chưa mở khoá.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Kiểm soát đối tượng thật mà client không hay biết.
- Quản lý vòng đời đối tượng thật (lazy).
- Open/Closed: thêm proxy mới không sửa đối tượng thật hay client.
✘ Nhược điểm
- Thêm tầng trung gian → phản hồi có thể chậm hơn.
- Tăng số lượng lớp.
8Khi nào nên dùng
- Lazy initialization cho đối tượng nặng.
- Kiểm soát quyền truy cập.
- Cache, ghi log, đếm số lần gọi.
- Làm việc với đối tượng ở xa (remote).
9Gặp ở đâu trong thực tế
C++ / nhúng
std::unique_ptr,std::shared_ptrstd::vector<bool>::reference- Thư viện DHT của Adafruit (giới hạn tần suất đọc)
Python
weakref.proxy()unittest.mock.Mockxmlrpc.client.ServerProxy(remote proxy)
Java
java.lang.reflect.Proxy— dynamic proxy- Spring AOP:
@Transactional,@Cacheablehoạt động nhờ proxy - Hibernate lazy loading của entity
- Java RMI stub
10Liên hệ với các mẫu khác
Adapter đổi interface; Proxy giữ nguyên interface; Decorator mở rộng interface/hành vi. Decorator do client chủ động xếp chồng; Proxy thường tự quản lý đối tượng thật. Facade giống Proxy ở chỗ đứng chắn trước đối tượng phức tạp, nhưng Facade có interface khác.
11Bài tập thực hành
⛓️ Chain of Responsibility — Chuỗi trách nhiệm
Mục đích: Chuyển yêu cầu dọc theo một chuỗi các bộ xử lý. Mỗi bộ xử lý quyết định tự xử lý hoặc chuyển tiếp cho mắt xích kế tiếp — người gửi không cần biết ai sẽ xử lý.
1Vấn đề cần giải quyết
Đơn xin nghỉ của giáo viên cần được duyệt theo thẩm quyền: ≤ 2 ngày tổ trưởng duyệt, ≤ 5 ngày hiệu phó, ≤ 30 ngày hiệu trưởng.
Viết một hàm với chuỗi if/else dài thì: mỗi khi đổi quy trình (thêm cấp “Trưởng phòng GD”, đổi ngưỡng) phải sửa hàm đó; người gửi đơn bị gắn chặt với toàn bộ bộ máy duyệt; không thể cấu hình quy trình khác nhau cho từng trường.
2Giải pháp
Mỗi cấp duyệt là một đối tượng Handler riêng, có tham chiếu next tới cấp kế tiếp. Khi nhận yêu cầu, handler kiểm tra: đủ thẩm quyền thì xử lý, không thì gọi next.handle().
Client chỉ gửi yêu cầu vào đầu chuỗi. Chuỗi được lắp ráp lúc chạy, có thể thêm/bớt/đổi thứ tự mắt xích mà không sửa các handler.
3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Handler (Approver) | Khai báo interface xử lý và giữ liên kết tới handler kế tiếp. |
| ConcreteHandler (TeamLeader…) | Xử lý yêu cầu nếu có thể, ngược lại chuyển tiếp. |
| Client | Lắp chuỗi và gửi yêu cầu tới handler đầu tiên. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Chain of Responsibility – C++17
#include <iostream>
#include <string>
struct LeaveRequest {
std::string teacher;
int days;
std::string reason;
};
// ===== Handler =====
class Approver {
public:
virtual ~Approver() = default;
// Trả về next để nối chuỗi: a.setNext(b).setNext(c)
Approver& setNext(Approver& next) { next_ = &next; return next; }
// Không virtual: lớp con không thể phá logic chuyển tiếp
void handle(const LeaveRequest& r) const {
if (canApprove(r)) {
std::cout << "✅ " << role() << " duyệt: " << r.teacher << " nghỉ " << r.days
<< " ngày (" << r.reason << ")\n";
} else if (next_) {
std::cout << " " << role() << " chuyển tiếp đơn của " << r.teacher << "...\n";
next_->handle(r);
} else {
std::cout << "❌ Không ai đủ thẩm quyền duyệt đơn " << r.days << " ngày của " << r.teacher << "\n";
}
}
protected:
virtual bool canApprove(const LeaveRequest& r) const = 0;
virtual const char* role() const = 0;
private:
Approver* next_ = nullptr; // con trỏ không sở hữu – các handler sống ở nơi khác
};
// ===== Concrete Handlers =====
class TeamLeader : public Approver {
protected:
bool canApprove(const LeaveRequest& r) const override { return r.days <= 2; }
const char* role() const override { return "Tổ trưởng"; }
};
class VicePrincipal : public Approver {
protected:
bool canApprove(const LeaveRequest& r) const override { return r.days <= 5; }
const char* role() const override { return "Hiệu phó"; }
};
class Principal : public Approver {
protected:
bool canApprove(const LeaveRequest& r) const override { return r.days <= 30; }
const char* role() const override { return "Hiệu trưởng"; }
};
int main() {
TeamLeader lead;
VicePrincipal vice;
Principal principal;
lead.setNext(vice).setNext(principal); // lắp chuỗi – không cấp phát động
lead.handle({"Cô Lan", 1, "Đi khám bệnh"});
lead.handle({"Thầy Nam", 4, "Tập huấn chuyên môn"});
lead.handle({"Cô Hoa", 10, "Nghỉ phép năm"});
lead.handle({"Thầy Bình", 45, "Đi du học"});
}
g++ -std=c++17 14-chain-of-responsibility.cpp -o app && ./app✅ Tổ trưởng duyệt: Cô Lan nghỉ 1 ngày (Đi khám bệnh) Tổ trưởng chuyển tiếp đơn của Thầy Nam... ✅ Hiệu phó duyệt: Thầy Nam nghỉ 4 ngày (Tập huấn chuyên môn) Tổ trưởng chuyển tiếp đơn của Cô Hoa... Hiệu phó chuyển tiếp đơn của Cô Hoa... ✅ Hiệu trưởng duyệt: Cô Hoa nghỉ 10 ngày (Nghỉ phép năm) Tổ trưởng chuyển tiếp đơn của Thầy Bình... Hiệu phó chuyển tiếp đơn của Thầy Bình... ❌ Không ai đủ thẩm quyền duyệt đơn 45 ngày của Thầy Bình
Giải thích code C++17
setNext()trả vềApprover&để nối chuỗi; con trỏnext_là con trỏ không sở hữu.handle()không phải hàm ảo → lớp con không thể ghi đè logic chuyển tiếp (tương đươngfinal).- Ba handler là biến cục bộ trong
main()— chuỗi không cần cấp phát động. - Yêu cầu là
struct LeaveRequest, khởi tạo gọn bằng{"Cô Lan", 1, "..."}.
"""Chain of Responsibility – Python 3."""
from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass(frozen=True)
class LeaveRequest:
teacher: str
days: int
reason: str
class Approver(ABC):
role = "?"
def __init__(self):
self._next = None
def set_next(self, nxt):
self._next = nxt
return nxt
def handle(self, r: LeaveRequest):
if self.can_approve(r):
print(f"✅ {self.role:<12} duyệt: {r.teacher} nghỉ {r.days} ngày ({r.reason})")
elif self._next:
print(f" {self.role:<12} chuyển tiếp đơn của {r.teacher}...")
self._next.handle(r)
else:
print(f"❌ Không ai đủ thẩm quyền duyệt đơn {r.days} ngày của {r.teacher}")
@abstractmethod
def can_approve(self, r) -> bool: ...
class TeamLeader(Approver):
role = "Tổ trưởng"
def can_approve(self, r): return r.days <= 2
class VicePrincipal(Approver):
role = "Hiệu phó"
def can_approve(self, r): return r.days <= 5
class Principal(Approver):
role = "Hiệu trưởng"
def can_approve(self, r): return r.days <= 30
if __name__ == "__main__":
chain = TeamLeader()
chain.set_next(VicePrincipal()).set_next(Principal())
for req in (LeaveRequest("Cô Lan", 1, "Đi khám bệnh"),
LeaveRequest("Thầy Nam", 4, "Tập huấn chuyên môn"),
LeaveRequest("Cô Hoa", 10, "Nghỉ phép năm"),
LeaveRequest("Thầy Bình", 45, "Đi du học")):
chain.handle(req)
python 14-chain-of-responsibility.py✅ Tổ trưởng duyệt: Cô Lan nghỉ 1 ngày (Đi khám bệnh) Tổ trưởng chuyển tiếp đơn của Thầy Nam... ✅ Hiệu phó duyệt: Thầy Nam nghỉ 4 ngày (Tập huấn chuyên môn) Tổ trưởng chuyển tiếp đơn của Cô Hoa... Hiệu phó chuyển tiếp đơn của Cô Hoa... ✅ Hiệu trưởng duyệt: Cô Hoa nghỉ 10 ngày (Nghỉ phép năm) Tổ trưởng chuyển tiếp đơn của Thầy Bình... Hiệu phó chuyển tiếp đơn của Thầy Bình... ❌ Không ai đủ thẩm quyền duyệt đơn 45 ngày của Thầy Bình
Giải thích code Python 3
rolelà thuộc tính lớp, mỗi lớp con ghi đè một giá trị.@dataclass(frozen=True)cho đối tượng yêu cầu bất biến.
public class ChainDemo {
public static void main(String[] args) {
// Xây dựng chuỗi xử lý: Tổ trưởng -> Hiệu phó -> Hiệu trưởng
Approver chain = new TeamLeader();
chain.setNext(new VicePrincipal())
.setNext(new Principal());
chain.handle(new LeaveRequest("Cô Lan", 1, "Đi khám bệnh"));
chain.handle(new LeaveRequest("Thầy Nam", 4, "Tập huấn chuyên môn"));
chain.handle(new LeaveRequest("Cô Hoa", 10, "Nghỉ phép năm"));
chain.handle(new LeaveRequest("Thầy Bình", 45, "Đi du học"));
}
}
/** Request – dữ liệu được chuyền qua chuỗi. */
record LeaveRequest(String teacher, int days, String reason) { }
/** Handler – định nghĩa giao diện xử lý + liên kết tới mắt xích kế tiếp. */
abstract class Approver {
private Approver next;
/** Trả về next để có thể nối chuỗi kiểu a.setNext(b).setNext(c). */
public Approver setNext(Approver next) {
this.next = next;
return next;
}
/** Template: tự xử lý nếu được, không thì chuyển tiếp. */
public final void handle(LeaveRequest r) {
if (canApprove(r)) {
System.out.printf("✅ %-14s duyệt: %s nghỉ %d ngày (%s)%n", role(), r.teacher(), r.days(), r.reason());
} else if (next != null) {
System.out.printf(" %-14s chuyển tiếp đơn của %s...%n", role(), r.teacher());
next.handle(r);
} else {
System.out.printf("❌ Không ai đủ thẩm quyền duyệt đơn %d ngày của %s%n", r.days(), r.teacher());
}
}
protected abstract boolean canApprove(LeaveRequest r);
protected abstract String role();
}
/** Concrete Handlers */
class TeamLeader extends Approver {
protected boolean canApprove(LeaveRequest r) { return r.days() <= 2; }
protected String role() { return "Tổ trưởng"; }
}
class VicePrincipal extends Approver {
protected boolean canApprove(LeaveRequest r) { return r.days() <= 5; }
protected String role() { return "Hiệu phó"; }
}
class Principal extends Approver {
protected boolean canApprove(LeaveRequest r) { return r.days() <= 30; }
protected String role() { return "Hiệu trưởng"; }
}
java 14-chain-of-responsibility.java✅ Tổ trưởng duyệt: Cô Lan nghỉ 1 ngày (Đi khám bệnh) Tổ trưởng chuyển tiếp đơn của Thầy Nam... ✅ Hiệu phó duyệt: Thầy Nam nghỉ 4 ngày (Tập huấn chuyên môn) Tổ trưởng chuyển tiếp đơn của Cô Hoa... Hiệu phó chuyển tiếp đơn của Cô Hoa... ✅ Hiệu trưởng duyệt: Cô Hoa nghỉ 10 ngày (Nghỉ phép năm) Tổ trưởng chuyển tiếp đơn của Thầy Bình... Hiệu phó chuyển tiếp đơn của Thầy Bình... ❌ Không ai đủ thẩm quyền duyệt đơn 45 ngày của Thầy Bình
Giải thích code Java 17+
setNext()trả về chính handler kế tiếp nên lắp chuỗi được bằng một câu lệnh nối tiếp.handle()được khai báofinal— lớp con chỉ định nghĩa điều kiện (canApprove), không thể phá vỡ logic chuyển tiếp.- Đơn 45 ngày đi hết chuỗi mà không ai duyệt → cần xử lý trường hợp “rơi khỏi chuỗi”.
LeaveRequestdùngrecord(Java 16+) — gọn và bất biến.
6Áp dụng trong lập trình nhúng
- Xử lý lệnh nhận qua UART: chuỗi
LedCommandHandler → MotorCommandHandler → ConfigCommandHandler → UnknownHandler; mỗi handler kiểm tra tiền tố lệnh. - Xử lý ngắt/sự kiện theo mức ưu tiên; bộ lọc gói tin mạng.
- Middleware của web server trên ESP32 (xác thực → giới hạn tần suất → xử lý).
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Giảm phụ thuộc giữa người gửi và người nhận.
- Thêm/bớt/sắp xếp lại handler linh hoạt lúc chạy.
- Single Responsibility: mỗi handler một nhiệm vụ.
✘ Nhược điểm
- Không đảm bảo yêu cầu luôn được xử lý.
- Chuỗi dài khó debug, có thể ảnh hưởng hiệu năng.
8Khi nào nên dùng
- Nhiều đối tượng có thể xử lý một yêu cầu và handler cụ thể chỉ được biết lúc chạy.
- Quy trình phê duyệt nhiều cấp.
- Chuỗi bộ lọc / middleware: xác thực, phân quyền, ghi log, nén dữ liệu.
9Gặp ở đâu trong thực tế
C++ / nhúng
- ESP-IDF
esp_http_server: chuỗi URI handler - Bảng vector ngắt dùng chung (shared IRQ)
- Qt: sự kiện lan truyền từ widget con lên cha
Python
- Middleware của Django / Flask (WSGI)
logging: log lan truyền lên logger cha- Chuỗi
excepttrongtry
Java
javax.servlet.Filter/FilterChain.doFilter()- Spring Security filter chain
java.util.logging.Loggerchuyển log lên logger cha- Cơ chế nổi bọt sự kiện (event bubbling) trong DOM
10Liên hệ với các mẫu khác
Thường kết hợp với Composite: yêu cầu chuyển từ con lên cha. Command có thể làm đối tượng yêu cầu. Decorator có cấu trúc gần giống nhưng mọi decorator đều xử lý và không được “cắt ngang” chuỗi.
11Bài tập thực hành
🎮 Command — Mệnh lệnh
Mục đích: Đóng gói một yêu cầu thành một đối tượng, nhờ đó có thể truyền yêu cầu như tham số, xếp hàng, ghi lịch sử và hoàn tác (undo).
1Vấn đề cần giải quyết
Chiếc điều khiển nhà thông minh có nhiều nút. Nếu mỗi nút gọi thẳng light.on(), fan.setSpeed(3)… thì:
- Điều khiển bị gắn chặt với mọi loại thiết bị; muốn gán lại chức năng cho nút phải sửa code.
- Không có cách nào hoàn tác thao tác vừa làm, hay ghi lại macro để phát lại.
- Cùng một hành động (bật đèn) có thể đến từ nút bấm, giọng nói, lịch hẹn giờ → code bị lặp.
2Giải pháp
Biến mỗi hành động thành một đối tượng cài đặt interface Command với execute() (và thường thêm undo()). Đối tượng command chứa sẵn: ai thực hiện (receiver), làm gì và tham số.
Invoker (điều khiển) chỉ biết gọi execute(), và lưu command vào ngăn xếp lịch sử để hoàn tác. Để undo đúng, command phải ghi nhớ trạng thái cũ trước khi thực thi (như prevSpeed).
3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Command | Interface với execute() (và undo()). |
| ConcreteCommand (FanSpeedCommand…) | Liên kết receiver với hành động; lưu trạng thái cần cho undo. |
| Receiver (Light, Fan) | Đối tượng biết làm việc thật. |
| Invoker (RemoteControl) | Kích hoạt command, quản lý lịch sử. |
| Client | Tạo command, gắn receiver và giao cho invoker. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Command – C++17. Trong nhúng: lệnh nhận qua UART/Bluetooth được đóng gói thành
// đối tượng Command rồi đưa vào hàng đợi để vòng loop() xử lý tuần tự.
#include <iostream>
#include <memory>
#include <stack>
#include <string>
// ===== Command =====
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0;
virtual void undo() = 0;
virtual std::string name() const = 0;
};
// ===== Receivers =====
class Light {
public:
explicit Light(std::string room) : room_(std::move(room)) {}
void on() { std::cout << " 💡 Bật đèn " << room_ << "\n"; }
void off() { std::cout << " 💡 Tắt đèn " << room_ << "\n"; }
private:
std::string room_;
};
class Fan {
public:
int getSpeed() const { return speed_; }
void setSpeed(int s) { speed_ = s; std::cout << " 🌀 Quạt số " << s << "\n"; }
private:
int speed_ = 0;
};
// ===== Concrete Commands =====
class LightOnCommand : public Command {
public:
explicit LightOnCommand(Light& l) : light_(l) {}
void execute() override { light_.on(); }
void undo() override { light_.off(); }
std::string name() const override { return "Bật đèn"; }
private:
Light& light_;
};
class LightOffCommand : public Command {
public:
explicit LightOffCommand(Light& l) : light_(l) {}
void execute() override { light_.off(); }
void undo() override { light_.on(); }
std::string name() const override { return "Tắt đèn"; }
private:
Light& light_;
};
class FanSpeedCommand : public Command {
public:
FanSpeedCommand(Fan& f, int s) : fan_(f), newSpeed_(s) {}
void execute() override { prevSpeed_ = fan_.getSpeed(); fan_.setSpeed(newSpeed_); }
void undo() override { fan_.setSpeed(prevSpeed_); }
std::string name() const override { return "Quạt số " + std::to_string(newSpeed_); }
private:
Fan& fan_;
int newSpeed_, prevSpeed_ = 0; // lưu trạng thái cũ để undo
};
// ===== Invoker =====
class RemoteControl {
public:
void press(std::unique_ptr<Command> c) {
std::cout << "▶ " << c->name() << "\n";
c->execute();
history_.push(std::move(c));
}
void undo() {
if (history_.empty()) { std::cout << "↩ Không còn lệnh để hoàn tác\n"; return; }
std::cout << "↩ Hoàn tác: " << history_.top()->name() << "\n";
history_.top()->undo();
history_.pop();
}
private:
std::stack<std::unique_ptr<Command>> history_;
};
int main() {
Light light("phòng khách");
Fan fan;
RemoteControl remote;
remote.press(std::make_unique<LightOnCommand>(light));
remote.press(std::make_unique<FanSpeedCommand>(fan, 3));
remote.press(std::make_unique<FanSpeedCommand>(fan, 1));
remote.press(std::make_unique<LightOffCommand>(light));
std::cout << "--- Hoàn tác (Undo) từng bước ---\n";
for (int i = 0; i < 5; ++i) remote.undo();
}
g++ -std=c++17 15-command.cpp -o app && ./app▶ Bật đèn 💡 Bật đèn phòng khách ▶ Quạt số 3 🌀 Quạt số 3 ▶ Quạt số 1 🌀 Quạt số 1 ▶ Tắt đèn 💡 Tắt đèn phòng khách --- Hoàn tác (Undo) từng bước --- ↩ Hoàn tác: Tắt đèn 💡 Bật đèn phòng khách ↩ Hoàn tác: Quạt số 1 🌀 Quạt số 3 ↩ Hoàn tác: Quạt số 3 🌀 Quạt số 0 ↩ Hoàn tác: Bật đèn 💡 Tắt đèn phòng khách ↩ Không còn lệnh để hoàn tác
Giải thích code C++17
- Lịch sử là
std::stack<std::unique_ptr<Command>>— command bị pop sẽ tự giải phóng. - Command giữ tham chiếu tới receiver (
Light&,Fan&), không sở hữu thiết bị. FanSpeedCommandlưuprevSpeed_lúcexecute()đểundo()khôi phục chính xác.
"""Command – Python 3."""
from abc import ABC, abstractmethod
class Command(ABC):
@abstractmethod
def execute(self): ...
@abstractmethod
def undo(self): ...
name = "?"
class Light:
def __init__(self, room): self.room = room
def on(self): print(f" 💡 Bật đèn {self.room}")
def off(self): print(f" 💡 Tắt đèn {self.room}")
class Fan:
def __init__(self): self.speed = 0
def set_speed(self, s):
self.speed = s
print(f" 🌀 Quạt số {s}")
class LightOnCommand(Command):
name = "Bật đèn"
def __init__(self, light): self.light = light
def execute(self): self.light.on()
def undo(self): self.light.off()
class LightOffCommand(Command):
name = "Tắt đèn"
def __init__(self, light): self.light = light
def execute(self): self.light.off()
def undo(self): self.light.on()
class FanSpeedCommand(Command):
def __init__(self, fan, speed):
self.fan, self.new_speed, self.prev_speed = fan, speed, 0
self.name = f"Quạt số {speed}"
def execute(self):
self.prev_speed = self.fan.speed
self.fan.set_speed(self.new_speed)
def undo(self): self.fan.set_speed(self.prev_speed)
class RemoteControl:
def __init__(self): self._history = []
def press(self, c: Command):
print(f"▶ {c.name}")
c.execute()
self._history.append(c)
def undo(self):
if not self._history:
print("↩ Không còn lệnh để hoàn tác")
return
c = self._history.pop()
print(f"↩ Hoàn tác: {c.name}")
c.undo()
if __name__ == "__main__":
light, fan, remote = Light("phòng khách"), Fan(), RemoteControl()
for cmd in (LightOnCommand(light), FanSpeedCommand(fan, 3), FanSpeedCommand(fan, 1), LightOffCommand(light)):
remote.press(cmd)
print("--- Hoàn tác (Undo) từng bước ---")
for _ in range(5):
remote.undo()
python 15-command.py▶ Bật đèn 💡 Bật đèn phòng khách ▶ Quạt số 3 🌀 Quạt số 3 ▶ Quạt số 1 🌀 Quạt số 1 ▶ Tắt đèn 💡 Tắt đèn phòng khách --- Hoàn tác (Undo) từng bước --- ↩ Hoàn tác: Tắt đèn 💡 Bật đèn phòng khách ↩ Hoàn tác: Quạt số 1 🌀 Quạt số 3 ↩ Hoàn tác: Quạt số 3 🌀 Quạt số 0 ↩ Hoàn tác: Bật đèn 💡 Tắt đèn phòng khách ↩ Không còn lệnh để hoàn tác
Giải thích code Python 3
- Lịch sử chỉ là một
listdùngappend/popnhư ngăn xếp. - Khi không cần undo, command trong Python thường chỉ là một hàm hoặc
functools.partial.
import java.util.ArrayDeque;
import java.util.Deque;
public class CommandDemo {
public static void main(String[] args) {
// Receivers – các thiết bị thật
Light light = new Light("phòng khách");
Fan fan = new Fan();
// Invoker – chiếc điều khiển không hề biết bên trong lệnh làm gì
RemoteControl remote = new RemoteControl();
remote.press(new LightOnCommand(light));
remote.press(new FanSpeedCommand(fan, 3));
remote.press(new FanSpeedCommand(fan, 1));
remote.press(new LightOffCommand(light));
System.out.println("--- Hoàn tác (Undo) từng bước ---");
remote.undo();
remote.undo();
remote.undo();
remote.undo();
remote.undo(); // không còn gì để hoàn tác
}
}
/** Command */
interface Command {
void execute();
void undo();
String name();
}
// ===== Receivers =====
class Light {
private final String room;
Light(String room) { this.room = room; }
void on() { System.out.println(" 💡 Bật đèn " + room); }
void off() { System.out.println(" 💡 Tắt đèn " + room); }
}
class Fan {
private int speed = 0;
int getSpeed() { return speed; }
void setSpeed(int s) { speed = s; System.out.println(" 🌀 Quạt số " + s); }
}
// ===== Concrete Commands – đóng gói "receiver + hành động + tham số" =====
class LightOnCommand implements Command {
private final Light light;
LightOnCommand(Light l) { light = l; }
public void execute() { light.on(); }
public void undo() { light.off(); }
public String name() { return "Bật đèn"; }
}
class LightOffCommand implements Command {
private final Light light;
LightOffCommand(Light l) { light = l; }
public void execute() { light.off(); }
public void undo() { light.on(); }
public String name() { return "Tắt đèn"; }
}
class FanSpeedCommand implements Command {
private final Fan fan;
private final int newSpeed;
private int prevSpeed; // lưu trạng thái cũ để undo
FanSpeedCommand(Fan f, int s) { fan = f; newSpeed = s; }
public void execute() { prevSpeed = fan.getSpeed(); fan.setSpeed(newSpeed); }
public void undo() { fan.setSpeed(prevSpeed); }
public String name() { return "Quạt số " + newSpeed; }
}
/** Invoker – lưu lịch sử lệnh để hoàn tác. */
class RemoteControl {
private final Deque<Command> history = new ArrayDeque<>();
void press(Command c) {
System.out.println("▶ " + c.name());
c.execute();
history.push(c);
}
void undo() {
if (history.isEmpty()) { System.out.println("↩ Không còn lệnh để hoàn tác"); return; }
Command c = history.pop();
System.out.println("↩ Hoàn tác: " + c.name());
c.undo();
}
}
java 15-command.java▶ Bật đèn 💡 Bật đèn phòng khách ▶ Quạt số 3 🌀 Quạt số 3 ▶ Quạt số 1 🌀 Quạt số 1 ▶ Tắt đèn 💡 Tắt đèn phòng khách --- Hoàn tác (Undo) từng bước --- ↩ Hoàn tác: Tắt đèn 💡 Bật đèn phòng khách ↩ Hoàn tác: Quạt số 1 🌀 Quạt số 3 ↩ Hoàn tác: Quạt số 3 🌀 Quạt số 0 ↩ Hoàn tác: Bật đèn 💡 Tắt đèn phòng khách ↩ Không còn lệnh để hoàn tác
Giải thích code Java 17+
RemoteControlkhông importLighthayFan— nó chỉ biếtCommand.FanSpeedCommandlưuprevSpeedtrước khi đổi tốc độ → undo khôi phục chính xác: 1 → 3 → 0.- Lịch sử là một ngăn xếp (
ArrayDeque): lệnh làm sau được hoàn tác trước (LIFO). - Lần undo thứ 5 không còn gì → invoker xử lý an toàn.
6Áp dụng trong lập trình nhúng
- Lệnh từ app điện thoại/Bluetooth/UART được đóng gói thành Command và đưa vào hàng đợi (FreeRTOS queue); task xử lý lấy ra thực thi tuần tự — tách phần nhận lệnh khỏi phần điều khiển phần cứng.
- Ghi và phát lại macro: robot học chuỗi chuyển động rồi lặp lại.
- Bảng ánh xạ phím IR remote → Command cho phép gán lại nút mà không sửa code xử lý.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Tách đối tượng gọi lệnh khỏi đối tượng thực hiện.
- Hỗ trợ undo/redo, macro (gộp nhiều command), xếp hàng, lập lịch, ghi log.
- Open/Closed: thêm command mới không sửa invoker.
✘ Nhược điểm
- Mỗi hành động là một lớp → nhiều lớp nhỏ (có thể giảm bằng lambda nếu không cần undo).
8Khi nào nên dùng
- Muốn tham số hoá đối tượng bằng hành động (nút bấm, menu, phím tắt).
- Cần undo/redo, lịch sử thao tác.
- Cần xếp hàng, trì hoãn, lập lịch hoặc gửi yêu cầu qua mạng.
- Giao dịch (transaction): ghi lại các bước để rollback.
9Gặp ở đâu trong thực tế
C++ / nhúng
- FreeRTOS queue chứa struct lệnh
std::functiontrong hàng đợi tác vụ- Qt:
QUndoCommand,QUndoStack
Python
functools.partialconcurrent.futures(submit tác vụ)- Tkinter:
command=của nút bấm
Java
java.lang.Runnable,java.util.concurrent.Callablejavax.swing.Action- Hàng đợi tác vụ trong
ExecutorService - Lệnh undo/redo trong mọi trình soạn thảo
10Liên hệ với các mẫu khác
Memento thường đi kèm Command để lưu trạng thái cho undo. Chain of Responsibility có thể chuyển command dọc chuỗi. Command có thể dùng Prototype để sao chép trước khi đưa vào lịch sử. Strategy cũng đóng gói hành vi nhưng mục đích là chọn thuật toán, không phải biểu diễn yêu cầu.
11Bài tập thực hành
🗣️ Interpreter — Thông dịch
Mục đích: Với một ngôn ngữ nhỏ, định nghĩa biểu diễn ngữ pháp của nó dưới dạng các lớp, cùng một bộ thông dịch dùng biểu diễn đó để diễn giải câu trong ngôn ngữ.
1Vấn đề cần giải quyết
Ứng dụng cần tính các công thức do người dùng nhập, ví dụ cho robot: x 2 + y 1 - * với x, y là giá trị cảm biến. Công thức thay đổi thường xuyên, không thể viết cứng trong code.
Xử lý bằng if/else trên chuỗi thì rối rắm, khó mở rộng thêm toán tử, và không tái sử dụng được kết quả phân tích.
2Giải pháp
Mỗi quy tắc ngữ pháp trở thành một lớp cài đặt interface Expression với phương thức interpret(context):
- Terminal expression: phần tử không thể chia nhỏ — số, biến.
- Nonterminal expression: phần tử ghép từ biểu thức khác — cộng, trừ, nhân (chứa 2 biểu thức con).
Câu lệnh được phân tích (parse) thành cây cú pháp trừu tượng (AST) — chính là một cây Composite. Gọi interpret() trên gốc → đệ quy tính toàn cây. Context chứa thông tin bên ngoài như giá trị biến.
3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| AbstractExpression (Expression) | Khai báo interpret(context). |
| TerminalExpression (NumberExpr, VariableExpr) | Cài đặt cho ký hiệu kết thúc của ngữ pháp. |
| NonterminalExpression (AddExpr…) | Mỗi quy tắc ghép là một lớp, chứa các biểu thức con. |
| Context | Thông tin toàn cục cho thông dịch (bảng giá trị biến). |
| Client | Xây/nhận AST (thường qua Parser) và gọi interpret. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Interpreter – C++17. Ứng dụng nhúng: firmware nhận "công thức" qua Serial
// (ví dụ ngưỡng bật quạt = "x 2 + y 1 - *") mà không cần nạp lại chương trình.
#include <cctype>
#include <iostream>
#include <map>
#include <memory>
#include <sstream>
#include <stack>
#include <string>
using Context = std::map<std::string, int>;
// ===== AbstractExpression =====
class Expression {
public:
virtual ~Expression() = default;
virtual int interpret(const Context& ctx) const = 0;
virtual std::string str() const = 0;
};
using ExprPtr = std::unique_ptr<Expression>;
// ===== Terminal Expressions =====
class NumberExpr : public Expression {
public:
explicit NumberExpr(int v) : value_(v) {}
int interpret(const Context&) const override { return value_; }
std::string str() const override { return std::to_string(value_); }
private:
int value_;
};
class VariableExpr : public Expression {
public:
explicit VariableExpr(std::string n) : name_(std::move(n)) {}
int interpret(const Context& ctx) const override {
auto it = ctx.find(name_);
return it == ctx.end() ? 0 : it->second;
}
std::string str() const override { return name_; }
private:
std::string name_;
};
// ===== Nonterminal Expression – một lớp mẫu dùng cho cả + - * =====
template <char Op>
class BinaryExpr : public Expression {
public:
BinaryExpr(ExprPtr l, ExprPtr r) : l_(std::move(l)), r_(std::move(r)) {}
int interpret(const Context& ctx) const override {
int a = l_->interpret(ctx), b = r_->interpret(ctx);
if constexpr (Op == '+') return a + b;
else if constexpr (Op == '-') return a - b;
else return a * b;
}
std::string str() const override { return "(" + l_->str() + " " + Op + " " + r_->str() + ")"; }
private:
ExprPtr l_, r_;
};
using AddExpr = BinaryExpr<'+'>;
using SubtractExpr = BinaryExpr<'-'>;
using MultiplyExpr = BinaryExpr<'*'>;
// ===== Parser: chuỗi hậu tố -> cây cú pháp (AST) =====
ExprPtr parse(const std::string& source) {
std::stack<ExprPtr> st;
std::istringstream in(source);
std::string tok;
auto pop = [&] { ExprPtr e = std::move(st.top()); st.pop(); return e; };
while (in >> tok) {
if (tok == "+" || tok == "-" || tok == "*") {
ExprPtr r = pop(), l = pop();
if (tok == "+") st.push(std::make_unique<AddExpr>(std::move(l), std::move(r)));
else if (tok == "-") st.push(std::make_unique<SubtractExpr>(std::move(l), std::move(r)));
else st.push(std::make_unique<MultiplyExpr>(std::move(l), std::move(r)));
} else if (std::isdigit(static_cast<unsigned char>(tok[0]))) {
st.push(std::make_unique<NumberExpr>(std::stoi(tok)));
} else {
st.push(std::make_unique<VariableExpr>(tok));
}
}
return pop();
}
int main() {
Context ctx{ {"x", 5}, {"y", 3} };
for (const char* p : { "2 3 +", "x y * 4 -", "x 2 + y 1 - *" }) {
ExprPtr ast = parse(p);
std::cout << p << " => cây: " << ast->str() << " = " << ast->interpret(ctx) << "\n";
}
ctx["x"] = 10; // đổi ngữ cảnh, giữ nguyên cây
ExprPtr ast = parse("x 2 + y 1 - *");
std::cout << "Khi x = 10: " << ast->str() << " = " << ast->interpret(ctx) << "\n";
}
g++ -std=c++17 16-interpreter.cpp -o app && ./app2 3 + => cây: (2 + 3) = 5 x y * 4 - => cây: ((x * y) - 4) = 11 x 2 + y 1 - * => cây: ((x + 2) * (y - 1)) = 14 Khi x = 10: ((x + 2) * (y - 1)) = 24
Giải thích code C++17
- Một lớp template
BinaryExpr<char Op>+if constexprthay cho ba lớp riêng — trình biên dịch sinh ra ba lớp khi cần. - AST sở hữu các nút con qua
std::unique_ptr; parser dùngstd::stack. Contextlàstd::map<std::string,int>chứa giá trị biến.
"""Interpreter – Python 3."""
from abc import ABC, abstractmethod
from dataclasses import dataclass
class Expression(ABC):
@abstractmethod
def interpret(self, ctx: dict) -> int: ...
@dataclass
class NumberExpr(Expression): # Terminal
value: int
def interpret(self, ctx): return self.value
def __str__(self): return str(self.value)
@dataclass
class VariableExpr(Expression): # Terminal
name: str
def interpret(self, ctx): return ctx.get(self.name, 0)
def __str__(self): return self.name
@dataclass
class BinaryExpr(Expression): # Nonterminal (dùng chung cho + - *)
op: str
left: Expression
right: Expression
OPS = {"+": lambda a, b: a + b, "-": lambda a, b: a - b, "*": lambda a, b: a * b}
def interpret(self, ctx):
return self.OPS[self.op](self.left.interpret(ctx), self.right.interpret(ctx))
def __str__(self): return f"({self.left} {self.op} {self.right})"
def parse(source: str) -> Expression:
stack = []
for tok in source.split():
if tok in BinaryExpr.OPS:
right, left = stack.pop(), stack.pop()
stack.append(BinaryExpr(tok, left, right))
elif tok.lstrip("-").isdigit():
stack.append(NumberExpr(int(tok)))
else:
stack.append(VariableExpr(tok))
return stack.pop()
if __name__ == "__main__":
ctx = {"x": 5, "y": 3}
for p in ("2 3 +", "x y * 4 -", "x 2 + y 1 - *"):
ast = parse(p)
print(f"{p:<16}=> cây: {str(ast):<22} = {ast.interpret(ctx)}")
ctx["x"] = 10
ast = parse("x 2 + y 1 - *")
print(f"Khi x = 10: {ast} = {ast.interpret(ctx)}")
python 16-interpreter.py2 3 + => cây: (2 + 3) = 5 x y * 4 - => cây: ((x * y) - 4) = 11 x 2 + y 1 - * => cây: ((x + 2) * (y - 1)) = 14 Khi x = 10: ((x + 2) * (y - 1)) = 24
Giải thích code Python 3
@dataclassgiúp mỗi nút cây chỉ vài dòng; bảngOPSánh xạ toán tử → lambda.- Python có sẵn
astvàevalnhưng không bao giờ nênevalchuỗi do người dùng gửi tới — tự viết interpreter nhỏ an toàn hơn.
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
public class InterpreterDemo {
public static void main(String[] args) {
// Ngữ pháp (BNF) của ngôn ngữ mini – biểu thức hậu tố (Reverse Polish Notation):
// expression ::= number | variable | expression expression operator
// operator ::= '+' | '-' | '*'
Map<String, Integer> context = new HashMap<>();
context.put("x", 5);
context.put("y", 3);
String[] programs = {
"2 3 +", // 2 + 3
"x y * 4 -", // x * y - 4
"x 2 + y 1 - *" // (x + 2) * (y - 1)
};
for (String p : programs) {
Expression ast = Parser.parse(p);
System.out.printf("%-16s => cây: %-22s = %d%n", p, ast, ast.interpret(context));
}
// Đổi ngữ cảnh -> cùng một cây cú pháp, kết quả khác
context.put("x", 10);
Expression ast = Parser.parse("x 2 + y 1 - *");
System.out.println("Khi x = 10: " + ast + " = " + ast.interpret(context));
}
}
/** AbstractExpression */
interface Expression {
int interpret(Map<String, Integer> ctx);
}
/** TerminalExpression – số */
record NumberExpr(int value) implements Expression {
public int interpret(Map<String, Integer> ctx) { return value; }
public String toString() { return String.valueOf(value); }
}
/** TerminalExpression – biến, tra giá trị trong Context */
record VariableExpr(String name) implements Expression {
public int interpret(Map<String, Integer> ctx) { return ctx.getOrDefault(name, 0); }
public String toString() { return name; }
}
/** NonterminalExpressions – mỗi quy tắc ngữ pháp là một lớp */
record AddExpr(Expression l, Expression r) implements Expression {
public int interpret(Map<String, Integer> ctx) { return l.interpret(ctx) + r.interpret(ctx); }
public String toString() { return "(" + l + " + " + r + ")"; }
}
record SubtractExpr(Expression l, Expression r) implements Expression {
public int interpret(Map<String, Integer> ctx) { return l.interpret(ctx) - r.interpret(ctx); }
public String toString() { return "(" + l + " - " + r + ")"; }
}
record MultiplyExpr(Expression l, Expression r) implements Expression {
public int interpret(Map<String, Integer> ctx) { return l.interpret(ctx) * r.interpret(ctx); }
public String toString() { return "(" + l + " * " + r + ")"; }
}
/** Parser – chuyển chuỗi thành cây cú pháp (AST) bằng ngăn xếp. */
class Parser {
static Expression parse(String source) {
Deque<Expression> stack = new ArrayDeque<>();
for (String token : source.trim().split("\\s+")) {
switch (token) {
case "+" -> { Expression r = stack.pop(), l = stack.pop(); stack.push(new AddExpr(l, r)); }
case "-" -> { Expression r = stack.pop(), l = stack.pop(); stack.push(new SubtractExpr(l, r)); }
case "*" -> { Expression r = stack.pop(), l = stack.pop(); stack.push(new MultiplyExpr(l, r)); }
default -> stack.push(token.matches("-?\\d+")
? new NumberExpr(Integer.parseInt(token))
: new VariableExpr(token));
}
}
return stack.pop();
}
}
java 16-interpreter.java2 3 + => cây: (2 + 3) = 5 x y * 4 - => cây: ((x * y) - 4) = 11 x 2 + y 1 - * => cây: ((x + 2) * (y - 1)) = 14 Khi x = 10: ((x + 2) * (y - 1)) = 24
Giải thích code Java 17+
Parser.parsedùng ngăn xếp: gặp số/biến thì đẩy vào; gặp toán tử thì lấy 2 phần tử ra, ghép thành nút mới rồi đẩy lại.- Phương thức
toString()của mỗi nút in lại cây dưới dạng biểu thức trung tố có ngoặc — giúp “nhìn thấy” AST. - Cùng một AST, đổi context (
x = 10) → kết quả mới mà không cần parse lại. - Dùng
recordvàswitchmũi tên (Java 14+) giúp mỗi quy tắc chỉ còn một dòng.
6Áp dụng trong lập trình nhúng
- Firmware nhận công thức/luật qua Serial hoặc MQTT (“bật quạt khi
t 30 >”) và thực thi mà không cần nạp lại chương trình. - Bộ thông dịch lệnh AT của module SIM/ESP8266; G-code trong máy in 3D/CNC (Marlin, GRBL) là ví dụ thực tế lớn.
- Ngôn ngữ kịch bản nhỏ cho robot:
FORWARD 10,TURN 90.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Dễ thay đổi, mở rộng ngữ pháp — mỗi quy tắc một lớp.
- Cài đặt ngữ pháp đơn giản trở nên trực quan.
✘ Nhược điểm
- Ngữ pháp phức tạp → số lớp bùng nổ, khó bảo trì.
- Hiệu năng không cao so với parser chuyên dụng.
8Khi nào nên dùng
- Có một ngôn ngữ đơn giản cần thông dịch: công thức, quy tắc lọc, lệnh điều khiển robot.
- Ngữ pháp nhỏ và hiệu năng không phải ưu tiên hàng đầu.
9Gặp ở đâu trong thực tế
C++ / nhúng
- Marlin / GRBL: thông dịch G-code
- Bộ xử lý lệnh AT
std::regex
Python
- Module
ast,re - MicroPython chính là một trình thông dịch chạy trên MCU
string.Template
Java
java.util.regex.Patternjava.text.Formatvà các lớp con- Spring Expression Language (SpEL)
- Bộ tính công thức trong bảng tính
10Liên hệ với các mẫu khác
Cây AST là một Composite. Visitor dùng để thêm thao tác mới trên AST (in, tối ưu, kiểm tra kiểu). Iterator có thể duyệt AST. Flyweight có thể chia sẻ các nút terminal.
11Bài tập thực hành
🔁 Iterator — Bộ duyệt
Mục đích: Cung cấp cách truy cập tuần tự các phần tử của một tập hợp mà không làm lộ cấu trúc bên trong của nó (mảng, danh sách liên kết, cây…).
1Vấn đề cần giải quyết
Lớp Playlist lưu bài hát trong một mảng. Nếu để client duyệt bằng songs[i]:
- Client phải biết bên trong là mảng; khi đổi sang
LinkedListhay cây, mọi vòng lặp ở client đều hỏng. - Muốn nhiều kiểu duyệt (xuôi, ngược, ngẫu nhiên, chỉ bài < 4 phút) → nhồi hết vào lớp
Playlist, làm nó phình to.
2Giải pháp
Tách logic duyệt ra một đối tượng riêng — Iterator — giữ vị trí hiện tại và cung cấp hasNext(), next(). Collection chỉ cần có phương thức tạo iterator (iterator()).
Nhiều iterator có thể duyệt độc lập, đồng thời trên cùng một collection, mỗi cái giữ trạng thái riêng.
• C++: iterator là nền tảng của STL — lớp nào có
begin()/end() trả về đối tượng hỗ trợ *, ++, != sẽ dùng được range-for và mọi thuật toán <algorithm>.• Python: giao thức
__iter__/__next__; hàm có yield (generator) là iterator ngắn gọn nhất.• Java:
Iterator + Iterable cho vòng lặp for-each.3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Iterator | Interface duyệt: hasNext(), next(). |
| ConcreteIterator (ForwardIterator…) | Cài đặt thuật toán duyệt, giữ vị trí hiện tại. |
| Aggregate (Iterable) | Interface có phương thức tạo iterator. |
| ConcreteAggregate (Playlist) | Trả về iterator phù hợp với cấu trúc bên trong. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Iterator – C++17. Trong C++, iterator là khái niệm cốt lõi của STL:
// chỉ cần cung cấp begin()/end() và một kiểu iterator có *, ++, != là dùng được range-for.
#include <array>
#include <iostream>
#include <string>
struct Song {
std::string title;
int minutes;
};
// ConcreteAggregate – lưu trong mảng tĩnh (kiểu bộ đệm vòng/FIFO trên MCU)
template <size_t Capacity>
class Playlist {
public:
void add(Song s) { if (count_ < Capacity) songs_[count_++] = std::move(s); }
// ===== ConcreteIterator – duyệt xuôi =====
class Iterator {
public:
Iterator(const Song* p) : p_(p) {}
const Song& operator*() const { return *p_; }
Iterator& operator++() { ++p_; return *this; }
bool operator!=(const Iterator& o) const { return p_ != o.p_; }
private:
const Song* p_;
};
Iterator begin() const { return Iterator(songs_.data()); }
Iterator end() const { return Iterator(songs_.data() + count_); }
// ===== Iterator kiểu Java (hasNext/next) – duyệt ngược =====
class ReverseIterator {
public:
ReverseIterator(const Playlist& pl) : pl_(pl), index_(int(pl.count_) - 1) {}
bool hasNext() const { return index_ >= 0; }
const Song& next() { return pl_.songs_[index_--]; }
private:
const Playlist& pl_;
int index_;
};
ReverseIterator reverseIterator() const { return ReverseIterator(*this); }
private:
std::array<Song, Capacity> songs_{}; // client không bao giờ thấy mảng này
size_t count_ = 0;
};
int main() {
Playlist<5> playlist;
playlist.add({"Lạc Trôi", 4});
playlist.add({"Nơi này có anh", 5});
playlist.add({"Hãy trao cho anh", 4});
playlist.add({"See tình", 3});
std::cout << "Phát theo thứ tự (range-for dùng begin()/end()):\n";
for (const Song& s : playlist) std::cout << " ▶ " << s.title << " (" << s.minutes << "')\n";
std::cout << "Phát ngược:\n";
auto rev = playlist.reverseIterator();
while (rev.hasNext()) {
const Song& s = rev.next();
std::cout << " ◀ " << s.title << " (" << s.minutes << "')\n";
}
int total = 0;
for (auto it = playlist.begin(); it != playlist.end(); ++it) total += (*it).minutes;
std::cout << "Tổng thời lượng: " << total << " phút\n";
}
g++ -std=c++17 17-iterator.cpp -o app && ./appPhát theo thứ tự (range-for dùng begin()/end()): ▶ Lạc Trôi (4') ▶ Nơi này có anh (5') ▶ Hãy trao cho anh (4') ▶ See tình (3') Phát ngược: ◀ See tình (3') ◀ Hãy trao cho anh (4') ◀ Nơi này có anh (5') ◀ Lạc Trôi (4') Tổng thời lượng: 16 phút
Giải thích code C++17
Playlist::Iteratorchỉ cần 3 toán tử*,++,!=cùngbegin()/end()là dùng được range-for.- Dữ liệu nằm trong
std::arraykích thước cố định (templateCapacity) — kiểu bộ đệm tĩnh quen thuộc trên MCU. ReverseIteratorviết kiểuhasNext()/next()để so sánh với cách của Java.
"""Iterator – Python 3. Giao thức iterator của Python: __iter__() và __next__() (ném StopIteration).
Hàm có yield (generator) là cách viết iterator ngắn gọn nhất."""
from dataclasses import dataclass
@dataclass
class Song:
title: str
minutes: int
def __str__(self): return f"{self.title} ({self.minutes}')"
class Playlist:
def __init__(self):
self._songs = [] # cấu trúc bên trong được che giấu
def add(self, s): self._songs.append(s)
def __iter__(self): # cách 1: trả về đối tượng iterator tường minh
return ForwardIterator(self._songs)
def reverse(self): # cách 2: generator
for i in range(len(self._songs) - 1, -1, -1):
yield self._songs[i]
class ForwardIterator:
def __init__(self, items):
self._items, self._index = items, 0
def __iter__(self): return self
def __next__(self):
if self._index >= len(self._items):
raise StopIteration
item = self._items[self._index]
self._index += 1
return item
if __name__ == "__main__":
pl = Playlist()
for t, m in [("Lạc Trôi", 4), ("Nơi này có anh", 5), ("Hãy trao cho anh", 4), ("See tình", 3)]:
pl.add(Song(t, m))
print("Phát theo thứ tự:")
it = iter(pl)
print(" ▶", next(it)) # gọi thủ công
for s in it: # phần còn lại
print(" ▶", s)
print("Phát ngược:")
for s in pl.reverse():
print(" ◀", s)
print("Tổng thời lượng:", sum(s.minutes for s in pl), "phút")
python 17-iterator.pyPhát theo thứ tự: ▶ Lạc Trôi (4') ▶ Nơi này có anh (5') ▶ Hãy trao cho anh (4') ▶ See tình (3') Phát ngược: ◀ See tình (3') ◀ Hãy trao cho anh (4') ◀ Nơi này có anh (5') ◀ Lạc Trôi (4') Tổng thời lượng: 16 phút
Giải thích code Python 3
ForwardIteratorcài đặt giao thức__iter__/__next__và némStopIterationkhi hết.reverse()là generator (dùngyield) — cách viết iterator ngắn nhất.- Có thể gọi
next(it)thủ công rồi tiếp tụcfortrên cùng iterator.
import java.util.Iterator;
import java.util.NoSuchElementException;
public class IteratorDemo {
public static void main(String[] args) {
Playlist playlist = new Playlist(5);
playlist.add(new Song("Lạc Trôi", 4));
playlist.add(new Song("Nơi này có anh", 5));
playlist.add(new Song("Hãy trao cho anh", 4));
playlist.add(new Song("See tình", 3));
// 1. Duyệt bằng Iterator tường minh
System.out.println("Phát theo thứ tự:");
Iterator<Song> it = playlist.iterator();
while (it.hasNext()) System.out.println(" ▶ " + it.next());
// 2. Duyệt ngược – một kiểu iterator khác trên CÙNG một collection
System.out.println("Phát ngược:");
Iterator<Song> rev = playlist.reverseIterator();
while (rev.hasNext()) System.out.println(" ◀ " + rev.next());
// 3. Vì Playlist implements Iterable nên dùng được for-each
int total = 0;
for (Song s : playlist) total += s.minutes();
System.out.println("Tổng thời lượng: " + total + " phút");
}
}
record Song(String title, int minutes) {
public String toString() { return title + " (" + minutes + "')"; }
}
/** ConcreteAggregate – cấu trúc lưu trữ bên trong (mảng) được che giấu khỏi client. */
class Playlist implements Iterable<Song> {
private final Song[] songs;
private int count = 0;
Playlist(int capacity) { songs = new Song[capacity]; }
void add(Song s) { songs[count++] = s; }
/** Factory method tạo ConcreteIterator. */
@Override
public Iterator<Song> iterator() { return new ForwardIterator(); }
public Iterator<Song> reverseIterator() { return new ReverseIterator(); }
/** ConcreteIterator – giữ vị trí duyệt hiện tại. */
private class ForwardIterator implements Iterator<Song> {
private int index = 0;
public boolean hasNext() { return index < count; }
public Song next() {
if (!hasNext()) throw new NoSuchElementException();
return songs[index++];
}
}
private class ReverseIterator implements Iterator<Song> {
private int index = count - 1;
public boolean hasNext() { return index >= 0; }
public Song next() {
if (!hasNext()) throw new NoSuchElementException();
return songs[index--];
}
}
}
java 17-iterator.javaPhát theo thứ tự: ▶ Lạc Trôi (4') ▶ Nơi này có anh (5') ▶ Hãy trao cho anh (4') ▶ See tình (3') Phát ngược: ◀ See tình (3') ◀ Hãy trao cho anh (4') ◀ Nơi này có anh (5') ◀ Lạc Trôi (4') Tổng thời lượng: 16 phút
Giải thích code Java 17+
- Mảng
songslàprivate— client không bao giờ thấy nó. - Hai iterator là inner class nên truy cập trực tiếp được
songsvàcountcủa Playlist. next()némNoSuchElementExceptionkhi hết phần tử — đúng hợp đồng củajava.util.Iterator.- Nhờ
implements Iterable<Song>, vòngfor (Song s : playlist)hoạt động tự nhiên.
6Áp dụng trong lập trình nhúng
- Duyệt bộ đệm vòng (ring buffer) chứa mẫu ADC mà không lộ chỉ số head/tail.
- Duyệt danh sách thiết bị trên bus I2C/1-Wire (
OneWire::search()) hoặc các bản ghi log trong Flash. - Iterator giúp dùng lại thuật toán STL (
std::accumulate,std::max_element) trên cấu trúc tự viết.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Single Responsibility: tách thuật toán duyệt khỏi collection.
- Open/Closed: thêm kiểu duyệt mới không sửa collection.
- Duyệt song song nhiều iterator độc lập.
- Có thể tạm dừng và tiếp tục việc duyệt.
✘ Nhược điểm
- Dư thừa nếu collection đơn giản và chỉ có một kiểu duyệt.
- Có thể kém hiệu quả hơn truy cập trực tiếp với một số cấu trúc.
8Khi nào nên dùng
- Collection có cấu trúc phức tạp cần che giấu.
- Cần nhiều cách duyệt khác nhau.
- Muốn một cách duyệt thống nhất cho nhiều loại collection khác nhau.
9Gặp ở đâu trong thực tế
C++ / nhúng
- Toàn bộ STL:
std::vector::iterator,std::map::iterator… - ArduinoJson: duyệt
JsonArray - OneWire search
Python
for,range,enumerate,zip- Module
itertools - Đối tượng file (duyệt từng dòng)
Java
java.util.Iterator,ListIteratorjava.util.Scannerjava.sql.ResultSet.next()- Stream API (
Spliterator)
10Liên hệ với các mẫu khác
Thường dùng để duyệt cây Composite. Factory Method dùng để tạo iterator phù hợp. Memento có thể lưu trạng thái duyệt. Visitor + Iterator: Iterator duyệt, Visitor xử lý từng phần tử.
11Bài tập thực hành
🗼 Mediator — Người trung gian
Mục đích: Định nghĩa một đối tượng đóng gói cách các đối tượng khác tương tác với nhau. Các đối tượng không gọi nhau trực tiếp mà giao tiếp qua trung gian, nhờ đó giảm phụ thuộc chằng chịt.
1Vấn đề cần giải quyết
Nhóm chat lớp học có N thành viên. Nếu mỗi người giữ danh sách tất cả người khác và tự gửi tin cho từng người:
- Số liên kết là N × (N − 1) — với 40 học sinh là 1 560 liên kết!
- Thêm quy tắc (tin giáo viên được ghim, chặn người vi phạm) phải sửa lớp thành viên.
- Các lớp thành viên gắn chặt với nhau, không tái sử dụng được ở nhóm khác.
2Giải pháp
Tạo đối tượng Mediator (ChatRoom). Mỗi thành viên (Colleague) chỉ giữ tham chiếu tới mediator và báo cho nó khi có sự kiện. Mediator quyết định chuyển tin tới ai, theo quy tắc nào.
Quan hệ nhiều – nhiều chằng chịt trở thành quan hệ một – nhiều hình sao. Mọi luật lệ tương tác tập trung ở một chỗ.
3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Mediator (ChatRoom) | Interface giao tiếp giữa các colleague. |
| ConcreteMediator (ClassChatRoom) | Biết các colleague và điều phối tương tác. |
| Colleague (User) | Chỉ biết mediator; thông báo sự kiện cho mediator. |
| ConcreteColleague (Student, Teacher) | Các thành viên cụ thể. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Mediator – C++17
#include <iostream>
#include <string>
#include <vector>
class User;
// ===== Mediator =====
class ChatRoom {
public:
virtual ~ChatRoom() = default;
virtual void join(User& u) = 0;
virtual void broadcast(const std::string& msg, const User& from) = 0;
virtual void sendPrivate(const std::string& msg, const User& from, const std::string& to) = 0;
};
// ===== Colleague =====
class User {
public:
User(std::string name, ChatRoom& room) : name_(std::move(name)), room_(room) { room_.join(*this); }
virtual ~User() = default;
const std::string& getName() const { return name_; }
virtual bool isTeacher() const { return false; }
void send(const std::string& msg) {
std::cout << name_ << " gửi: " << msg << "\n";
room_.broadcast(msg, *this);
}
void sendPrivate(const std::string& to, const std::string& msg) {
std::cout << name_ << " nhắn riêng " << to << ": " << msg << "\n";
room_.sendPrivate(msg, *this, to);
}
void receive(const std::string& msg, const std::string& from) const {
std::cout << " -> " << name_ << " nhận từ " << from << ": " << msg << "\n";
}
protected:
std::string name_;
ChatRoom& room_; // chỉ biết mediator, không biết User khác
};
class Student : public User { public: using User::User; };
class Teacher : public User { public: using User::User; bool isTeacher() const override { return true; } };
// ===== Concrete Mediator =====
class ClassChatRoom : public ChatRoom {
public:
explicit ClassChatRoom(std::string name) : name_(std::move(name)) {}
void join(User& u) override {
users_.push_back(&u);
std::cout << "[" << name_ << "] " << u.getName() << " đã tham gia\n";
}
void broadcast(const std::string& msg, const User& from) override {
std::string tag = from.isTeacher() ? "📌 " : ""; // quy tắc nằm ở mediator
for (User* u : users_)
if (u != &from) u->receive(tag + msg, from.getName());
}
void sendPrivate(const std::string& msg, const User& from, const std::string& to) override {
for (User* u : users_)
if (u->getName() == to) { u->receive("(riêng) " + msg, from.getName()); return; }
}
private:
std::string name_;
std::vector<User*> users_;
};
int main() {
ClassChatRoom room("Lớp 10A1");
Teacher teacher("Thầy Tuyền", room);
Student an("An", room), binh("Bình", room), chi("Chi", room);
an.send("Mọi người làm bài tập Builder chưa?");
teacher.send("Hạn nộp là 20h tối nay nhé!");
binh.sendPrivate("Chi", "Cho mình mượn vở ghi với");
chi.send("Bài này dài quá :(");
}
g++ -std=c++17 18-mediator.cpp -o app && ./app[Lớp 10A1] Thầy Tuyền đã tham gia
[Lớp 10A1] An đã tham gia
[Lớp 10A1] Bình đã tham gia
[Lớp 10A1] Chi đã tham gia
An gửi: Mọi người làm bài tập Builder chưa?
-> Thầy Tuyền nhận từ An: Mọi người làm bài tập Builder chưa?
-> Bình nhận từ An: Mọi người làm bài tập Builder chưa?
-> Chi nhận từ An: Mọi người làm bài tập Builder chưa?
Thầy Tuyền gửi: Hạn nộp là 20h tối nay nhé!
-> An nhận từ Thầy Tuyền: 📌 Hạn nộp là 20h tối nay nhé!
-> Bình nhận từ Thầy Tuyền: 📌 Hạn nộp là 20h tối nay nhé!
-> Chi nhận từ Thầy Tuyền: 📌 Hạn nộp là 20h tối nay nhé!
Bình nhắn riêng Chi: Cho mình mượn vở ghi với
-> Chi nhận từ Bình: (riêng) Cho mình mượn vở ghi với
Chi gửi: Bài này dài quá :(
-> Thầy Tuyền nhận từ Chi: Bài này dài quá :(
-> An nhận từ Chi: Bài này dài quá :(
-> Bình nhận từ Chi: Bài này dài quá :(Giải thích code C++17
UsergiữChatRoom& room_; mediator giữstd::vector<User*>(không sở hữu).- Constructor của
Usertự gọiroom_.join(*this). - Kiểm tra vai trò bằng hàm ảo
isTeacher()thay chodynamic_cast.
"""Mediator – Python 3."""
class ClassChatRoom: # Concrete Mediator
def __init__(self, name):
self.name, self.users = name, []
def join(self, user):
self.users.append(user)
print(f"[{self.name}] {user.name} đã tham gia")
def broadcast(self, msg, sender):
tag = "📌 " if isinstance(sender, Teacher) else ""
for u in self.users:
if u is not sender:
u.receive(tag + msg, sender.name)
def send_private(self, msg, sender, to):
for u in self.users:
if u.name == to:
u.receive("(riêng) " + msg, sender.name)
class User: # Colleague
def __init__(self, name, room):
self.name, self.room = name, room
room.join(self)
def send(self, msg):
print(f"{self.name} gửi: {msg}")
self.room.broadcast(msg, self)
def send_private(self, to, msg):
print(f"{self.name} nhắn riêng {to}: {msg}")
self.room.send_private(msg, self, to)
def receive(self, msg, sender):
print(f" -> {self.name} nhận từ {sender}: {msg}")
class Student(User): pass
class Teacher(User): pass
if __name__ == "__main__":
room = ClassChatRoom("Lớp 10A1")
teacher = Teacher("Thầy Tuyền", room)
an, binh, chi = Student("An", room), Student("Bình", room), Student("Chi", room)
an.send("Mọi người làm bài tập Builder chưa?")
teacher.send("Hạn nộp là 20h tối nay nhé!")
binh.send_private("Chi", "Cho mình mượn vở ghi với")
chi.send("Bài này dài quá :(")
python 18-mediator.py[Lớp 10A1] Thầy Tuyền đã tham gia
[Lớp 10A1] An đã tham gia
[Lớp 10A1] Bình đã tham gia
[Lớp 10A1] Chi đã tham gia
An gửi: Mọi người làm bài tập Builder chưa?
-> Thầy Tuyền nhận từ An: Mọi người làm bài tập Builder chưa?
-> Bình nhận từ An: Mọi người làm bài tập Builder chưa?
-> Chi nhận từ An: Mọi người làm bài tập Builder chưa?
Thầy Tuyền gửi: Hạn nộp là 20h tối nay nhé!
-> An nhận từ Thầy Tuyền: 📌 Hạn nộp là 20h tối nay nhé!
-> Bình nhận từ Thầy Tuyền: 📌 Hạn nộp là 20h tối nay nhé!
-> Chi nhận từ Thầy Tuyền: 📌 Hạn nộp là 20h tối nay nhé!
Bình nhắn riêng Chi: Cho mình mượn vở ghi với
-> Chi nhận từ Bình: (riêng) Cho mình mượn vở ghi với
Chi gửi: Bài này dài quá :(
-> Thầy Tuyền nhận từ Chi: Bài này dài quá :(
-> An nhận từ Chi: Bài này dài quá :(
-> Bình nhận từ Chi: Bài này dài quá :(Giải thích code Python 3
- Dùng
isinstance(sender, Teacher)để áp quy tắc ghim tin. - Mediator không cần lớp trừu tượng riêng nhờ duck typing.
import java.util.ArrayList;
import java.util.List;
public class MediatorDemo {
public static void main(String[] args) {
// Các thành viên KHÔNG giữ tham chiếu tới nhau, chỉ biết phòng chat (mediator)
ChatRoom room = new ClassChatRoom("Lớp 10A1");
User teacher = new Teacher("Thầy Tuyền", room);
User an = new Student("An", room);
User binh = new Student("Bình", room);
User chi = new Student("Chi", room);
an.send("Mọi người làm bài tập Builder chưa?");
teacher.send("Hạn nộp là 20h tối nay nhé!");
binh.sendPrivate("Chi", "Cho mình mượn vở ghi với");
chi.send("Bài này dài quá :(");
}
}
/** Mediator */
interface ChatRoom {
void join(User user);
void broadcast(String message, User from);
void sendPrivate(String message, User from, String toName);
}
/** ConcreteMediator – chứa toàn bộ logic điều phối. */
class ClassChatRoom implements ChatRoom {
private final String name;
private final List<User> users = new ArrayList<>();
ClassChatRoom(String name) { this.name = name; }
public void join(User u) {
users.add(u);
System.out.println("[" + name + "] " + u.getName() + " đã tham gia");
}
public void broadcast(String message, User from) {
// Quy tắc nghiệp vụ nằm ở mediator: tin của giáo viên được đánh dấu quan trọng
String tag = (from instanceof Teacher) ? "📌 " : "";
for (User u : users) {
if (u != from) u.receive(tag + message, from.getName());
}
}
public void sendPrivate(String message, User from, String toName) {
users.stream()
.filter(u -> u.getName().equals(toName))
.findFirst()
.ifPresent(u -> u.receive("(riêng) " + message, from.getName()));
}
}
/** Colleague */
abstract class User {
protected final String name;
protected final ChatRoom room;
User(String name, ChatRoom room) { this.name = name; this.room = room; room.join(this); }
String getName() { return name; }
void send(String msg) {
System.out.println(name + " gửi: " + msg);
room.broadcast(msg, this);
}
void sendPrivate(String to, String msg) {
System.out.println(name + " nhắn riêng " + to + ": " + msg);
room.sendPrivate(msg, this, to);
}
void receive(String msg, String from) {
System.out.println(" -> " + name + " nhận từ " + from + ": " + msg);
}
}
/** ConcreteColleagues */
class Student extends User { Student(String n, ChatRoom r) { super(n, r); } }
class Teacher extends User { Teacher(String n, ChatRoom r) { super(n, r); } }
java 18-mediator.java[Lớp 10A1] Thầy Tuyền đã tham gia
[Lớp 10A1] An đã tham gia
[Lớp 10A1] Bình đã tham gia
[Lớp 10A1] Chi đã tham gia
An gửi: Mọi người làm bài tập Builder chưa?
-> Thầy Tuyền nhận từ An: Mọi người làm bài tập Builder chưa?
-> Bình nhận từ An: Mọi người làm bài tập Builder chưa?
-> Chi nhận từ An: Mọi người làm bài tập Builder chưa?
Thầy Tuyền gửi: Hạn nộp là 20h tối nay nhé!
-> An nhận từ Thầy Tuyền: 📌 Hạn nộp là 20h tối nay nhé!
-> Bình nhận từ Thầy Tuyền: 📌 Hạn nộp là 20h tối nay nhé!
-> Chi nhận từ Thầy Tuyền: 📌 Hạn nộp là 20h tối nay nhé!
Bình nhắn riêng Chi: Cho mình mượn vở ghi với
-> Chi nhận từ Bình: (riêng) Cho mình mượn vở ghi với
Chi gửi: Bài này dài quá :(
-> Thầy Tuyền nhận từ Chi: Bài này dài quá :(
-> An nhận từ Chi: Bài này dài quá :(
-> Bình nhận từ Chi: Bài này dài quá :(Giải thích code Java 17+
- Lớp
StudentvàTeacherkhông hề tham chiếu tới nhau — chỉ tớiChatRoom. - Quy tắc “tin giáo viên được ghim 📌” nằm trọn trong mediator; thành viên không cần biết.
- Tin nhắn riêng cũng đi qua mediator — dễ bổ sung kiểm duyệt, lưu lịch sử…
6Áp dụng trong lập trình nhúng
- MQTT broker là Mediator giữa các thiết bị IoT: cảm biến publish, màn hình/relay subscribe — không thiết bị nào biết nhau.
- Bộ điều phối trong robot: nhận dữ liệu từ cảm biến, ra quyết định và ra lệnh cho các cơ cấu chấp hành, thay vì để cảm biến gọi thẳng động cơ.
- Event bus trong firmware (ESP-IDF
esp_event).
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Giảm phụ thuộc giữa các thành phần (nhiều–nhiều → một–nhiều).
- Tập trung logic tương tác một chỗ, dễ hiểu, dễ sửa.
- Tái sử dụng colleague ở ngữ cảnh khác dễ hơn.
✘ Nhược điểm
- Mediator có thể trở thành “God object” phình to theo thời gian.
8Khi nào nên dùng
- Các đối tượng giao tiếp phức tạp, phụ thuộc chằng chịt.
- Form giao diện: thay đổi ô này làm ẩn/hiện/kiểm tra ô khác.
- Phòng chat, điều phối giao thông, hệ thống đặt chỗ.
9Gặp ở đâu trong thực tế
C++ / nhúng
- ESP-IDF
esp_eventloop - Mosquitto (MQTT broker)
- Qt: signal/slot qua đối tượng điều phối
Python
asyncioevent loop- Paho-MQTT client + broker
- Tkinter: controller điều phối widget
Java
java.util.Timer(điều phối cácTimerTask)java.util.concurrent.Executor- Controller trong kiến trúc MVC
- Message broker: MQTT broker giữa các thiết bị IoT
10Liên hệ với các mẫu khác
Facade che giấu hệ thống con và giao tiếp một chiều; Mediator điều phối hai chiều giữa các đồng nghiệp. Observer thường được dùng để cài đặt kênh liên lạc giữa colleague và mediator.
11Bài tập thực hành
💾 Memento — Vật lưu niệm
Mục đích: Lưu lại và khôi phục trạng thái trước đó của một đối tượng mà không làm lộ chi tiết cài đặt bên trong của nó (không phá vỡ tính đóng gói).
1Vấn đề cần giải quyết
Trình soạn thảo cần chức năng Undo. Cách đơn giản: một lớp bên ngoài đọc mọi field của Editor và lưu lại. Nhưng:
- Phải công khai (getter/setter) mọi field private → phá vỡ đóng gói, ai cũng có thể sửa trạng thái.
- Mỗi khi
Editorthêm field mới, lớp lưu trữ bên ngoài cũng phải sửa theo.
2Giải pháp
Giao việc chụp trạng thái cho chính đối tượng chủ (Originator): nó tạo ra một đối tượng Memento chứa bản chụp trạng thái và biết cách khôi phục từ memento.
Caretaker (History) chỉ lưu giữ các memento (thường trong ngăn xếp) và trả lại khi cần — nó không đọc, không sửa nội dung memento.
private và khai báo friend class Editor — Caretaker giữ được nhưng trình biên dịch cấm đọc. Python: dùng NamedTuple bất biến. Java: record lồng. Nếu lo bộ nhớ, có thể chỉ lưu phần thay đổi (diff) hoặc giới hạn số bản lưu.3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Originator (Editor) | Tạo memento chứa trạng thái hiện tại và khôi phục từ memento. |
| Memento (Editor.Memento) | Ảnh chụp trạng thái, bất biến. |
| Caretaker (History) | Giữ các memento, quyết định khi nào lưu/khôi phục; không xem nội dung. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Memento – C++17. Trong nhúng: lưu cấu hình vào EEPROM/Flash trước khi thử
// thông số mới (PID, ngưỡng cảm biến), nếu không ổn thì khôi phục.
#include <iostream>
#include <stack>
#include <string>
// ===== Originator =====
class Editor {
public:
// ===== Memento – chỉ Editor được đọc nội dung (friend), mọi thứ khác là private =====
class Memento {
friend class Editor;
Memento(std::string c, int f) : content_(std::move(c)), fontSize_(f) {}
std::string content_;
int fontSize_;
};
void type(const std::string& text) { content_ += text; }
void setFontSize(int size) { fontSize_ = size; }
Memento save() const { return Memento(content_, fontSize_); }
void restore(const Memento& m) { content_ = m.content_; fontSize_ = m.fontSize_; }
void print(const char* label) const {
std::cout << label << "\"" << content_ << "\" [cỡ chữ " << fontSize_ << "]\n";
}
private:
std::string content_;
int fontSize_ = 12;
};
// ===== Caretaker – giữ memento nhưng KHÔNG đọc được bên trong (biên dịch sẽ báo lỗi nếu thử) =====
class History {
public:
explicit History(Editor& e) : editor_(e) {}
void backup() { stack_.push(editor_.save()); }
void undo() {
if (stack_.empty()) return;
editor_.restore(stack_.top());
stack_.pop();
}
private:
Editor& editor_;
std::stack<Editor::Memento> stack_;
};
int main() {
Editor editor;
History history(editor);
history.backup();
editor.type("Design Pattern");
history.backup();
editor.type(" là các giải pháp");
editor.setFontSize(16);
history.backup();
editor.type(" ĐÃ VIẾT NHẦM!!!");
editor.setFontSize(40);
editor.print("Hiện tại : ");
history.undo(); editor.print("Undo 1 : ");
history.undo(); editor.print("Undo 2 : ");
history.undo(); editor.print("Undo 3 : ");
}
g++ -std=c++17 19-memento.cpp -o app && ./appHiện tại : "Design Pattern là các giải pháp ĐÃ VIẾT NHẦM!!!" [cỡ chữ 40] Undo 1 : "Design Pattern là các giải pháp" [cỡ chữ 16] Undo 2 : "Design Pattern" [cỡ chữ 12] Undo 3 : "" [cỡ chữ 12]
Giải thích code C++17
Editor::Mementocó mọi thành viên private và khai báofriend class Editor— chỉ Editor đọc được;Historygiữ memento nhưng truy cập vào trong sẽ lỗi biên dịch.- Đây là cách C++ thực thi đúng tinh thần “không phá vỡ đóng gói” của mẫu.
- Lịch sử là
std::stack<Editor::Memento>, lưu theo giá trị.
"""Memento – Python 3. Python không có private thật sự; quy ước dấu _ và
NamedTuple (bất biến) giúp thể hiện ý đồ "chỉ Originator được đọc"."""
from typing import NamedTuple
class Editor: # Originator
class _Memento(NamedTuple): # bất biến
content: str
font_size: int
def __init__(self):
self._content, self._font_size = "", 12
def type(self, text): self._content += text
def set_font_size(self, size): self._font_size = size
def save(self): return Editor._Memento(self._content, self._font_size)
def restore(self, m):
self._content, self._font_size = m.content, m.font_size
def __str__(self): return f'"{self._content}" [cỡ chữ {self._font_size}]'
class History: # Caretaker
def __init__(self, editor):
self._editor, self._stack = editor, []
def backup(self): self._stack.append(self._editor.save())
def undo(self):
if self._stack:
self._editor.restore(self._stack.pop())
if __name__ == "__main__":
editor = Editor()
history = History(editor)
history.backup()
editor.type("Design Pattern")
history.backup()
editor.type(" là các giải pháp")
editor.set_font_size(16)
history.backup()
editor.type(" ĐÃ VIẾT NHẦM!!!")
editor.set_font_size(40)
print("Hiện tại :", editor)
for i in range(1, 4):
history.undo()
print(f"Undo {i} :", editor)
python 19-memento.pyHiện tại : "Design Pattern là các giải pháp ĐÃ VIẾT NHẦM!!!" [cỡ chữ 40] Undo 1 : "Design Pattern là các giải pháp" [cỡ chữ 16] Undo 2 : "Design Pattern" [cỡ chữ 12] Undo 3 : "" [cỡ chữ 12]
Giải thích code Python 3
NamedTuplelồng trong lớp → memento bất biến.- Python không có private thật; dấu
_là quy ước “đừng chạm vào”.
import java.util.ArrayDeque;
import java.util.Deque;
public class MementoDemo {
public static void main(String[] args) {
Editor editor = new Editor();
History history = new History(editor);
history.backup();
editor.type("Design Pattern");
history.backup();
editor.type(" là các giải pháp");
editor.setFontSize(16);
history.backup();
editor.type(" ĐÃ VIẾT NHẦM!!!");
editor.setFontSize(40);
System.out.println("Hiện tại : " + editor);
history.undo();
System.out.println("Undo 1 : " + editor);
history.undo();
System.out.println("Undo 2 : " + editor);
history.undo();
System.out.println("Undo 3 : " + editor);
}
}
/** Originator – đối tượng có trạng thái cần lưu/khôi phục. */
class Editor {
private String content = "";
private int fontSize = 12;
void type(String text) { content += text; }
void setFontSize(int size) { fontSize = size; }
/** Tạo memento chứa ảnh chụp trạng thái hiện tại. */
Memento save() { return new Memento(content, fontSize); }
/** Khôi phục trạng thái từ memento. */
void restore(Memento m) {
this.content = m.content();
this.fontSize = m.fontSize();
}
@Override public String toString() { return "\"" + content + "\" [cỡ chữ " + fontSize + "]"; }
/**
* Memento – bất biến (record). Được khai báo lồng trong Editor và các field
* chỉ đọc, nên Caretaker giữ nó nhưng không thể thay đổi nội dung bên trong.
*/
record Memento(String content, int fontSize) { }
}
/** Caretaker – chỉ lưu giữ memento, không đọc/sửa nội dung của nó. */
class History {
private final Editor editor;
private final Deque<Editor.Memento> stack = new ArrayDeque<>();
History(Editor editor) { this.editor = editor; }
void backup() { stack.push(editor.save()); }
void undo() {
if (stack.isEmpty()) return;
editor.restore(stack.pop());
}
}
java 19-memento.javaHiện tại : "Design Pattern là các giải pháp ĐÃ VIẾT NHẦM!!!" [cỡ chữ 40] Undo 1 : "Design Pattern là các giải pháp" [cỡ chữ 16] Undo 2 : "Design Pattern" [cỡ chữ 12] Undo 3 : "" [cỡ chữ 12]
Giải thích code Java 17+
Editor.Mementolàrecordlồng bên trong — bất biến, Caretaker không thể sửa.Historychỉ gọieditor.save()vàeditor.restore(m); nó không biết Editor có những field gì.- Undo khôi phục cả nội dung và cỡ chữ — mọi trạng thái được chụp đồng thời.
- Undo lần 3 về trạng thái rỗng ban đầu (được backup ngay từ đầu).
6Áp dụng trong lập trình nhúng
- Trước khi thử bộ thông số PID mới, lưu memento cấu hình cũ; robot chạy không ổn thì khôi phục.
- Lưu memento xuống EEPROM/Flash (struct cấu hình + CRC) để giữ trạng thái qua lần mất điện; khi khởi động đọc lại.
- Cập nhật firmware OTA dùng hai phân vùng — phân vùng cũ là “memento” để rollback khi bản mới lỗi.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Chụp trạng thái mà không phá vỡ đóng gói.
- Đơn giản hoá Originator — việc quản lý lịch sử giao cho Caretaker.
✘ Nhược điểm
- Tốn bộ nhớ nếu chụp thường xuyên hoặc trạng thái lớn.
- Caretaker phải theo dõi vòng đời để xoá memento cũ.
8Khi nào nên dùng
- Cần undo/redo, khôi phục, checkpoint, save game.
- Truy cập trực tiếp vào trạng thái sẽ phá vỡ đóng gói.
- Transaction cần rollback khi lỗi.
9Gặp ở đâu trong thực tế
C++ / nhúng
- Arduino
EEPROM.put()/EEPROM.get()với struct cấu hình - ESP-IDF NVS (Non-Volatile Storage)
- Cơ chế rollback OTA của ESP32
Python
pickle,copy.deepcopyjson.dumptrạng thái- Lệnh undo của IDLE
Java
java.io.Serializable(lưu trạng thái đối tượng)- Undo trong trình soạn thảo, Photoshop
- Git commit — mỗi commit là một bản chụp trạng thái
10Liên hệ với các mẫu khác
Command + Memento: command thực thi, memento lưu trạng thái để undo. Iterator có thể dùng memento lưu vị trí duyệt. Prototype là cách đơn giản để tạo memento khi trạng thái không quá phức tạp.
11Bài tập thực hành
👀 Observer — Người quan sát
Mục đích: Định nghĩa quan hệ một – nhiều giữa các đối tượng: khi một đối tượng thay đổi trạng thái, mọi đối tượng phụ thuộc đều được thông báo và cập nhật tự động.
1Vấn đề cần giải quyết
Trạm thời tiết đo nhiệt độ và độ ẩm. Màn hình LCD, quạt tự động, ứng dụng điện thoại… đều cần dữ liệu mới.
- Nếu các thiết bị liên tục hỏi trạm (polling) → lãng phí, có độ trễ.
- Nếu trạm gọi thẳng
lcd.show(),fan.check(),app.push()→ trạm gắn chặt với mọi thiết bị; thêm thiết bị mới phải sửa trạm.
2Giải pháp
Đối tượng phát (Subject/Publisher) giữ danh sách người đăng ký và cung cấp subscribe()/unsubscribe(). Mọi người đăng ký (Observer/Subscriber) cài đặt chung interface có update().
Khi trạng thái thay đổi, subject duyệt danh sách và gọi update() trên từng observer. Subject chỉ biết interface Observer — không biết đó là LCD hay quạt.
update(t, h) (như ví dụ). Pull — subject chỉ báo “có thay đổi”, observer tự gọi getter để lấy dữ liệu nó cần.3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Subject | Interface quản lý danh sách observer. |
| ConcreteSubject (WeatherStation) | Lưu trạng thái; thông báo cho observer khi trạng thái đổi. |
| Observer | Interface với phương thức update(). |
| ConcreteObserver (LcdDisplay…) | Phản ứng với thông báo theo cách riêng. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Observer – C++17. Rất hợp với firmware IoT: một cảm biến, nhiều nơi cần dữ liệu
// (màn hình, relay, gửi MQTT). Danh sách observer dùng mảng tĩnh – không cấp phát động.
#include <array>
#include <iostream>
#include <string>
// ===== Observer =====
class Observer {
public:
virtual ~Observer() = default;
virtual void update(float temperature, float humidity) = 0;
};
// ===== Subject =====
class Subject {
public:
virtual ~Subject() = default;
virtual bool subscribe(Observer& o) = 0;
virtual void unsubscribe(Observer& o) = 0;
virtual void notifyObservers() = 0;
};
// ===== ConcreteSubject =====
class WeatherStation : public Subject {
public:
static constexpr size_t MAX_OBSERVERS = 8;
bool subscribe(Observer& o) override {
for (auto& slot : observers_) if (!slot) { slot = &o; return true; }
return false; // hết chỗ
}
void unsubscribe(Observer& o) override {
for (auto& slot : observers_) if (slot == &o) slot = nullptr;
}
void notifyObservers() override {
for (Observer* o : observers_) if (o) o->update(temperature_, humidity_);
}
void setMeasurements(float t, float h) { // gọi từ loop() sau khi đọc cảm biến
std::cout << "📡 Trạm đo: " << t << "°C, " << h << "%\n";
temperature_ = t; humidity_ = h;
notifyObservers();
}
private:
std::array<Observer*, MAX_OBSERVERS> observers_{}; // toàn nullptr lúc đầu
float temperature_ = 0, humidity_ = 0;
};
// ===== ConcreteObservers =====
class LcdDisplay : public Observer {
public:
void update(float t, float h) override { std::cout << " 🖥️ LCD: " << t << "°C | " << h << "%\n"; }
};
class AutoFan : public Observer { // điều khiển relay quạt
public:
explicit AutoFan(float threshold) : threshold_(threshold) {}
void update(float t, float) override {
std::cout << " 🌀 Quạt: " << (t > threshold_ ? "BẬT (relay HIGH)" : "tắt") << "\n";
}
private:
float threshold_;
};
class PhoneApp : public Observer { // gửi MQTT lên điện thoại
public:
explicit PhoneApp(std::string owner) : owner_(std::move(owner)) {}
void update(float t, float) override { std::cout << " 📱 MQTT tới " << owner_ << ": nhiệt độ " << t << "°C\n"; }
private:
std::string owner_;
};
int main() {
WeatherStation station;
LcdDisplay lcd;
AutoFan fan(30);
PhoneApp app("An");
station.subscribe(lcd);
station.subscribe(fan);
station.subscribe(app);
station.setMeasurements(27.5f, 70);
station.setMeasurements(32.0f, 55);
std::cout << "-- App của An huỷ đăng ký --\n";
station.unsubscribe(app);
station.setMeasurements(29.0f, 60);
}
g++ -std=c++17 20-observer.cpp -o app && ./app📡 Trạm đo: 27.5°C, 70% 🖥️ LCD: 27.5°C | 70% 🌀 Quạt: tắt 📱 MQTT tới An: nhiệt độ 27.5°C 📡 Trạm đo: 32°C, 55% 🖥️ LCD: 32°C | 55% 🌀 Quạt: BẬT (relay HIGH) 📱 MQTT tới An: nhiệt độ 32°C -- App của An huỷ đăng ký -- 📡 Trạm đo: 29°C, 60% 🖥️ LCD: 29°C | 60% 🌀 Quạt: tắt
Giải thích code C++17
- Danh sách observer là
std::array<Observer*, 8>cố định — không cấp phát;subscribe()trả vềfalsekhi hết chỗ. setMeasurements()được gọi từloop()sau khi đọc cảm biến, rồi tự động thông báo.- Lưu ý: observer phải sống lâu hơn subject hoặc tự
unsubscribe()trước khi bị huỷ — nếu không sẽ có con trỏ treo.
"""Observer – Python 3. Trong Python, observer có thể chỉ là một HÀM (callable) – rất gọn."""
class WeatherStation: # Subject
def __init__(self):
self._observers = []
self.temperature = self.humidity = 0.0
def subscribe(self, callback):
self._observers.append(callback)
def unsubscribe(self, callback):
self._observers.remove(callback)
def notify_observers(self):
for cb in self._observers:
cb(self.temperature, self.humidity)
def set_measurements(self, t, h):
print(f"📡 Trạm đo: {t}°C, {h}%")
self.temperature, self.humidity = t, h
self.notify_observers()
class AutoFan: # observer có trạng thái -> dùng lớp có __call__
def __init__(self, threshold): self.threshold = threshold
def __call__(self, t, h):
print(" 🌀 Quạt:", f"BẬT (quá {self.threshold}°C)" if t > self.threshold else "tắt")
def lcd_display(t, h): # observer là hàm thường
print(f" 🖥️ LCD: {t}°C | {h}%")
def make_phone_app(owner): # observer là closure
def notify(t, h):
print(f" 📱 Thông báo tới {owner}: nhiệt độ {t}°C")
return notify
if __name__ == "__main__":
station = WeatherStation()
app = make_phone_app("An")
station.subscribe(lcd_display)
station.subscribe(AutoFan(30))
station.subscribe(app)
station.set_measurements(27.5, 70)
station.set_measurements(32.0, 55)
print("-- App của An huỷ đăng ký --")
station.unsubscribe(app)
station.set_measurements(29.0, 60)
python 20-observer.py📡 Trạm đo: 27.5°C, 70% 🖥️ LCD: 27.5°C | 70% 🌀 Quạt: tắt 📱 Thông báo tới An: nhiệt độ 27.5°C 📡 Trạm đo: 32.0°C, 55% 🖥️ LCD: 32.0°C | 55% 🌀 Quạt: BẬT (quá 30°C) 📱 Thông báo tới An: nhiệt độ 32.0°C -- App của An huỷ đăng ký -- 📡 Trạm đo: 29.0°C, 60% 🖥️ LCD: 29.0°C | 60% 🌀 Quạt: tắt
Giải thích code Python 3
- Observer chỉ cần là callable: một hàm thường (
lcd_display), một closure (make_phone_app) hoặc đối tượng có__call__(AutoFan). - Không cần interface — gọn hơn nhiều so với C++/Java.
import java.util.ArrayList;
import java.util.List;
public class ObserverDemo {
public static void main(String[] args) {
WeatherStation station = new WeatherStation();
Observer lcd = new LcdDisplay();
Observer fan = new AutoFan(30);
Observer app = new PhoneApp("An");
station.subscribe(lcd);
station.subscribe(fan);
station.subscribe(app);
station.setMeasurements(27.5, 70);
station.setMeasurements(32.0, 55);
System.out.println("-- App của An huỷ đăng ký --");
station.unsubscribe(app);
station.setMeasurements(29.0, 60);
}
}
/** Observer */
interface Observer {
void update(double temperature, double humidity);
}
/** Subject */
interface Subject {
void subscribe(Observer o);
void unsubscribe(Observer o);
void notifyObservers();
}
/** ConcreteSubject – trạm thời tiết đọc cảm biến. */
class WeatherStation implements Subject {
private final List<Observer> observers = new ArrayList<>();
private double temperature, humidity;
public void subscribe(Observer o) { observers.add(o); }
public void unsubscribe(Observer o) { observers.remove(o); }
public void notifyObservers() {
for (Observer o : observers) o.update(temperature, humidity); // "đẩy" (push) dữ liệu
}
/** Khi dữ liệu đổi -> tự động báo cho mọi người đăng ký. */
void setMeasurements(double t, double h) {
System.out.println("📡 Trạm đo: " + t + "°C, " + h + "%");
this.temperature = t;
this.humidity = h;
notifyObservers();
}
}
/** ConcreteObservers – mỗi bên phản ứng theo cách riêng. */
class LcdDisplay implements Observer {
public void update(double t, double h) { System.out.println(" 🖥️ LCD: " + t + "°C | " + h + "%"); }
}
class AutoFan implements Observer {
private final double threshold;
AutoFan(double threshold) { this.threshold = threshold; }
public void update(double t, double h) {
System.out.println(" 🌀 Quạt: " + (t > threshold ? "BẬT (quá " + threshold + "°C)" : "tắt"));
}
}
class PhoneApp implements Observer {
private final String owner;
PhoneApp(String owner) { this.owner = owner; }
public void update(double t, double h) { System.out.println(" 📱 Thông báo tới " + owner + ": nhiệt độ " + t + "°C"); }
}
java 20-observer.java📡 Trạm đo: 27.5°C, 70.0% 🖥️ LCD: 27.5°C | 70.0% 🌀 Quạt: tắt 📱 Thông báo tới An: nhiệt độ 27.5°C 📡 Trạm đo: 32.0°C, 55.0% 🖥️ LCD: 32.0°C | 55.0% 🌀 Quạt: BẬT (quá 30.0°C) 📱 Thông báo tới An: nhiệt độ 32.0°C -- App của An huỷ đăng ký -- 📡 Trạm đo: 29.0°C, 60.0% 🖥️ LCD: 29.0°C | 60.0% 🌀 Quạt: tắt
Giải thích code Java 17+
WeatherStationchỉ có mộtList<Observer>— không có field nào kiểuLcdDisplayhayAutoFan.- Mỗi lần
setMeasurements()là ba observer phản ứng theo ba cách khác nhau. - Sau khi
PhoneApphuỷ đăng ký, nó không còn nhận thông báo. - Thêm observer mới (ví dụ ghi log ra thẻ SD) không phải sửa
WeatherStation.
6Áp dụng trong lập trình nhúng
- Một cảm biến, nhiều nơi cần dữ liệu (màn hình, relay, thẻ SD, MQTT) — đúng bài toán của Observer.
- Callback ngắt (
attachInterrupt) và event của ESP-IDF là dạng observer. - Cẩn thận:
update()gọi trong ngữ cảnh ngắt (ISR) phải cực ngắn — thường chỉ đặt cờ hoặc đẩy vào hàng đợi, xử lý sau trong task.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Open/Closed: thêm subscriber mới không sửa publisher.
- Thiết lập quan hệ giữa các đối tượng lúc chạy.
- Liên kết lỏng giữa publisher và subscriber.
✘ Nhược điểm
- Thứ tự thông báo không được đảm bảo.
- Quên huỷ đăng ký → rò rỉ bộ nhớ (lapsed listener).
- Chuỗi cập nhật dây chuyền khó theo dõi, dễ lặp vô hạn.
8Khi nào nên dùng
- Thay đổi trạng thái của một đối tượng cần cập nhật nhiều đối tượng khác, mà danh sách đó thay đổi động.
- Hệ thống hướng sự kiện: giao diện người dùng, IoT, thông báo.
- Tách tầng dữ liệu (Model) khỏi tầng hiển thị (View) trong MVC.
9Gặp ở đâu trong thực tế
C++ / nhúng
- Arduino
attachInterrupt(pin, callback, mode) - ESP-IDF
esp_event_handler_register - Qt signal/slot; Boost.Signals2
Python
- Tkinter
bind(),trace_add() - Paho-MQTT
on_message - MicroPython
Pin.irq(handler=...)
Java
- Event listener trong Swing/JavaFX:
addActionListener() java.beans.PropertyChangeListenerjava.util.concurrent.Flow(Reactive Streams), RxJava- MQTT publish/subscribe trong IoT;
addEventListenertrong JavaScript
10Liên hệ với các mẫu khác
Mediator thường dùng Observer để liên lạc. Khác Mediator: Observer là kết nối động một–nhiều, Mediator loại bỏ phụ thuộc lẫn nhau giữa các thành phần. Chain of Responsibility chuyển tuần tự; Observer phát đồng thời cho tất cả.
11Bài tập thực hành
🚦 State — Trạng thái
Mục đích: Cho phép một đối tượng thay đổi hành vi khi trạng thái bên trong của nó thay đổi — trông như thể đối tượng đã đổi sang một lớp khác.
1Vấn đề cần giải quyết
Máy bán nước tự động có các trạng thái: chờ tiền, đã có tiền, hết hàng. Mỗi hành động (bỏ tiền, trả tiền, bấm nút) phản ứng khác nhau theo trạng thái.
Cài đặt bằng biến int state + switch trong mọi phương thức: 3 trạng thái × 3 hành động = 9 nhánh rải rác. Thêm trạng thái “bảo trì” → sửa tất cả phương thức; logic của một trạng thái bị chia nhỏ ra khắp nơi, rất dễ sót.
2Giải pháp
Mỗi trạng thái thành một lớp riêng cài đặt interface State, chứa toàn bộ hành vi ứng với trạng thái đó. Đối tượng chính (Context) giữ tham chiếu tới đối tượng trạng thái hiện tại và uỷ quyền mọi hành động cho nó.
Chuyển trạng thái = gán một đối tượng State khác cho Context. Việc chuyển có thể do chính các lớp State quyết định (như ví dụ).
3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Context (VendingMachine) | Giữ trạng thái hiện tại; uỷ quyền hành vi cho nó. |
| State | Interface khai báo các hành vi phụ thuộc trạng thái. |
| ConcreteState (NoCoinState…) | Cài đặt hành vi cho một trạng thái; có thể chuyển Context sang trạng thái khác. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// State – C++17. Máy trạng thái (FSM) là "xương sống" của firmware.
// Các đối tượng trạng thái không có dữ liệu riêng nên dùng chung bản static
// (kết hợp Singleton/Flyweight) – không cấp phát động khi chuyển trạng thái.
#include <iostream>
class VendingMachine;
// ===== State =====
class State {
public:
virtual ~State() = default;
virtual void insertCoin(VendingMachine& m) const = 0;
virtual void ejectCoin(VendingMachine& m) const = 0;
virtual void pressButton(VendingMachine& m) const = 0;
virtual const char* name() const = 0;
};
// ===== Concrete States (khai báo) =====
class NoCoinState : public State {
public:
void insertCoin(VendingMachine& m) const override;
void ejectCoin(VendingMachine&) const override { std::cout << " Bạn chưa bỏ tiền\n"; }
void pressButton(VendingMachine&) const override { std::cout << " Vui lòng bỏ tiền trước\n"; }
const char* name() const override { return "Chờ tiền"; }
static const NoCoinState instance;
};
class HasCoinState : public State {
public:
void insertCoin(VendingMachine&) const override { std::cout << " Đã có tiền rồi, không nhận thêm\n"; }
void ejectCoin(VendingMachine& m) const override;
void pressButton(VendingMachine& m) const override;
const char* name() const override { return "Đã có tiền"; }
static const HasCoinState instance;
};
class SoldOutState : public State {
public:
void insertCoin(VendingMachine&) const override { std::cout << " Hết hàng! Trả lại tiền\n"; }
void ejectCoin(VendingMachine&) const override { std::cout << " Không có tiền để trả\n"; }
void pressButton(VendingMachine&) const override { std::cout << " Hết hàng\n"; }
const char* name() const override { return "Hết hàng"; }
static const SoldOutState instance;
};
const NoCoinState NoCoinState::instance;
const HasCoinState HasCoinState::instance;
const SoldOutState SoldOutState::instance;
// ===== Context =====
class VendingMachine {
public:
explicit VendingMachine(int stock)
: state_(stock > 0 ? static_cast<const State*>(&NoCoinState::instance) : &SoldOutState::instance),
stock_(stock) {}
void insertCoin() { log("Bỏ tiền"); state_->insertCoin(*this); }
void ejectCoin() { log("Trả tiền"); state_->ejectCoin(*this); }
void pressButton() { log("Bấm nút"); state_->pressButton(*this); }
void refill(int n) {
stock_ += n;
std::cout << "🔧 Nạp thêm " << n << " chai\n";
setState(NoCoinState::instance);
}
void setState(const State& s) {
std::cout << " ↳ chuyển trạng thái: " << state_->name() << " → " << s.name() << "\n";
state_ = &s;
}
int getStock() const { return stock_; }
void releaseProduct() { --stock_; std::cout << " 🥤 Rơi ra 1 chai nước (còn " << stock_ << ")\n"; }
private:
void log(const char* action) const { std::cout << "[" << state_->name() << "] " << action << "\n"; }
const State* state_;
int stock_;
};
// ===== Concrete States (định nghĩa – cần VendingMachine đầy đủ) =====
void NoCoinState::insertCoin(VendingMachine& m) const {
std::cout << " Đã nhận tiền\n";
m.setState(HasCoinState::instance);
}
void HasCoinState::ejectCoin(VendingMachine& m) const {
std::cout << " Trả lại tiền\n";
m.setState(NoCoinState::instance);
}
void HasCoinState::pressButton(VendingMachine& m) const {
m.releaseProduct();
if (m.getStock() > 0) m.setState(NoCoinState::instance);
else m.setState(SoldOutState::instance);
}
int main() {
VendingMachine vm(2);
vm.pressButton();
vm.insertCoin();
vm.insertCoin();
vm.pressButton();
vm.insertCoin();
vm.ejectCoin();
vm.insertCoin();
vm.pressButton();
vm.insertCoin();
vm.refill(5);
vm.insertCoin();
}
g++ -std=c++17 21-state.cpp -o app && ./app[Chờ tiền] Bấm nút
Vui lòng bỏ tiền trước
[Chờ tiền] Bỏ tiền
Đã nhận tiền
↳ chuyển trạng thái: Chờ tiền → Đã có tiền
[Đã có tiền] Bỏ tiền
Đã có tiền rồi, không nhận thêm
[Đã có tiền] Bấm nút
🥤 Rơi ra 1 chai nước (còn 1)
↳ chuyển trạng thái: Đã có tiền → Chờ tiền
[Chờ tiền] Bỏ tiền
Đã nhận tiền
↳ chuyển trạng thái: Chờ tiền → Đã có tiền
[Đã có tiền] Trả tiền
Trả lại tiền
↳ chuyển trạng thái: Đã có tiền → Chờ tiền
[Chờ tiền] Bỏ tiền
Đã nhận tiền
↳ chuyển trạng thái: Chờ tiền → Đã có tiền
[Đã có tiền] Bấm nút
🥤 Rơi ra 1 chai nước (còn 0)
↳ chuyển trạng thái: Đã có tiền → Hết hàng
[Hết hàng] Bỏ tiền
Hết hàng! Trả lại tiền
🔧 Nạp thêm 5 chai
↳ chuyển trạng thái: Hết hàng → Chờ tiền
[Chờ tiền] Bỏ tiền
Đã nhận tiền
↳ chuyển trạng thái: Chờ tiền → Đã có tiềnGiải thích code C++17
- Các trạng thái không có dữ liệu riêng nên mỗi lớp chỉ có một thể hiện
static const— chuyển trạng thái chỉ là đổi con trỏ, không cấp phát. - Khai báo trước (
class VendingMachine;), định nghĩa các hàm cần Context ở cuối file — kỹ thuật tách khai báo/định nghĩa để giải quyết phụ thuộc vòng. - Hàm trạng thái nhận
VendingMachine&để có thể gọisetState().
"""State – Python 3."""
class State:
name = "?"
def insert_coin(self, m): ...
def eject_coin(self, m): ...
def press_button(self, m): ...
class NoCoinState(State):
name = "Chờ tiền"
def insert_coin(self, m): print(" Đã nhận tiền"); m.set_state(HAS_COIN)
def eject_coin(self, m): print(" Bạn chưa bỏ tiền")
def press_button(self, m): print(" Vui lòng bỏ tiền trước")
class HasCoinState(State):
name = "Đã có tiền"
def insert_coin(self, m): print(" Đã có tiền rồi, không nhận thêm")
def eject_coin(self, m): print(" Trả lại tiền"); m.set_state(NO_COIN)
def press_button(self, m):
m.release_product()
m.set_state(NO_COIN if m.stock > 0 else SOLD_OUT)
class SoldOutState(State):
name = "Hết hàng"
def insert_coin(self, m): print(" Hết hàng! Trả lại tiền")
def eject_coin(self, m): print(" Không có tiền để trả")
def press_button(self, m): print(" Hết hàng")
# Các trạng thái không có dữ liệu riêng -> dùng chung một thể hiện
NO_COIN, HAS_COIN, SOLD_OUT = NoCoinState(), HasCoinState(), SoldOutState()
class VendingMachine: # Context
def __init__(self, stock):
self.stock = stock
self._state = NO_COIN if stock > 0 else SOLD_OUT
def _log(self, a): print(f"[{self._state.name}] {a}")
def insert_coin(self): self._log("Bỏ tiền"); self._state.insert_coin(self)
def eject_coin(self): self._log("Trả tiền"); self._state.eject_coin(self)
def press_button(self): self._log("Bấm nút"); self._state.press_button(self)
def refill(self, n):
self.stock += n
print(f"🔧 Nạp thêm {n} chai")
self.set_state(NO_COIN)
def set_state(self, s):
print(f" ↳ chuyển trạng thái: {self._state.name} → {s.name}")
self._state = s
def release_product(self):
self.stock -= 1
print(f" 🥤 Rơi ra 1 chai nước (còn {self.stock})")
if __name__ == "__main__":
vm = VendingMachine(2)
vm.press_button(); vm.insert_coin(); vm.insert_coin(); vm.press_button()
vm.insert_coin(); vm.eject_coin()
vm.insert_coin(); vm.press_button(); vm.insert_coin()
vm.refill(5); vm.insert_coin()
python 21-state.py[Chờ tiền] Bấm nút
Vui lòng bỏ tiền trước
[Chờ tiền] Bỏ tiền
Đã nhận tiền
↳ chuyển trạng thái: Chờ tiền → Đã có tiền
[Đã có tiền] Bỏ tiền
Đã có tiền rồi, không nhận thêm
[Đã có tiền] Bấm nút
🥤 Rơi ra 1 chai nước (còn 1)
↳ chuyển trạng thái: Đã có tiền → Chờ tiền
[Chờ tiền] Bỏ tiền
Đã nhận tiền
↳ chuyển trạng thái: Chờ tiền → Đã có tiền
[Đã có tiền] Trả tiền
Trả lại tiền
↳ chuyển trạng thái: Đã có tiền → Chờ tiền
[Chờ tiền] Bỏ tiền
Đã nhận tiền
↳ chuyển trạng thái: Chờ tiền → Đã có tiền
[Đã có tiền] Bấm nút
🥤 Rơi ra 1 chai nước (còn 0)
↳ chuyển trạng thái: Đã có tiền → Hết hàng
[Hết hàng] Bỏ tiền
Hết hàng! Trả lại tiền
🔧 Nạp thêm 5 chai
↳ chuyển trạng thái: Hết hàng → Chờ tiền
[Chờ tiền] Bỏ tiền
Đã nhận tiền
↳ chuyển trạng thái: Chờ tiền → Đã có tiềnGiải thích code Python 3
- Ba thể hiện dùng chung
NO_COIN,HAS_COIN,SOLD_OUTở mức module. - Lớp cơ sở
Statecó phương thức mặc định...nên lớp con chỉ ghi đè cái cần.
public class StateDemo {
public static void main(String[] args) {
VendingMachine vm = new VendingMachine(2); // còn 2 chai nước
vm.pressButton(); // chưa bỏ tiền
vm.insertCoin();
vm.insertCoin(); // bỏ tiền 2 lần
vm.pressButton(); // nhận nước
vm.insertCoin();
vm.ejectCoin(); // đổi ý, lấy lại tiền
vm.insertCoin();
vm.pressButton(); // chai cuối cùng -> hết hàng
vm.insertCoin(); // máy hết hàng, từ chối
vm.refill(5);
vm.insertCoin();
}
}
/** State – mỗi hành động của người dùng là một phương thức. */
interface State {
void insertCoin(VendingMachine m);
void ejectCoin(VendingMachine m);
void pressButton(VendingMachine m);
String name();
}
/** Context – uỷ quyền mọi hành vi cho đối tượng trạng thái hiện tại. */
class VendingMachine {
private State state;
private int stock;
VendingMachine(int stock) {
this.stock = stock;
this.state = stock > 0 ? new NoCoinState() : new SoldOutState();
}
void insertCoin() { log("Bỏ tiền"); state.insertCoin(this); }
void ejectCoin() { log("Trả tiền"); state.ejectCoin(this); }
void pressButton() { log("Bấm nút"); state.pressButton(this); }
void refill(int n) { stock += n; System.out.println("🔧 Nạp thêm " + n + " chai"); setState(new NoCoinState()); }
void setState(State s) { System.out.println(" ↳ chuyển trạng thái: " + state.name() + " → " + s.name()); state = s; }
int getStock() { return stock; }
void releaseProduct() { stock--; System.out.println(" 🥤 Rơi ra 1 chai nước (còn " + stock + ")"); }
private void log(String a) { System.out.println("[" + state.name() + "] " + a); }
}
/** ConcreteStates – không có if/else theo trạng thái trong Context nữa! */
class NoCoinState implements State {
public void insertCoin(VendingMachine m) { System.out.println(" Đã nhận tiền"); m.setState(new HasCoinState()); }
public void ejectCoin(VendingMachine m) { System.out.println(" Bạn chưa bỏ tiền"); }
public void pressButton(VendingMachine m) { System.out.println(" Vui lòng bỏ tiền trước"); }
public String name() { return "Chờ tiền"; }
}
class HasCoinState implements State {
public void insertCoin(VendingMachine m) { System.out.println(" Đã có tiền rồi, không nhận thêm"); }
public void ejectCoin(VendingMachine m) { System.out.println(" Trả lại tiền"); m.setState(new NoCoinState()); }
public void pressButton(VendingMachine m) {
m.releaseProduct();
m.setState(m.getStock() > 0 ? new NoCoinState() : new SoldOutState());
}
public String name() { return "Đã có tiền"; }
}
class SoldOutState implements State {
public void insertCoin(VendingMachine m) { System.out.println(" Hết hàng! Trả lại tiền"); }
public void ejectCoin(VendingMachine m) { System.out.println(" Không có tiền để trả"); }
public void pressButton(VendingMachine m) { System.out.println(" Hết hàng"); }
public String name() { return "Hết hàng"; }
}
java 21-state.java[Chờ tiền] Bấm nút
Vui lòng bỏ tiền trước
[Chờ tiền] Bỏ tiền
Đã nhận tiền
↳ chuyển trạng thái: Chờ tiền → Đã có tiền
[Đã có tiền] Bỏ tiền
Đã có tiền rồi, không nhận thêm
[Đã có tiền] Bấm nút
🥤 Rơi ra 1 chai nước (còn 1)
↳ chuyển trạng thái: Đã có tiền → Chờ tiền
[Chờ tiền] Bỏ tiền
Đã nhận tiền
↳ chuyển trạng thái: Chờ tiền → Đã có tiền
[Đã có tiền] Trả tiền
Trả lại tiền
↳ chuyển trạng thái: Đã có tiền → Chờ tiền
[Chờ tiền] Bỏ tiền
Đã nhận tiền
↳ chuyển trạng thái: Chờ tiền → Đã có tiền
[Đã có tiền] Bấm nút
🥤 Rơi ra 1 chai nước (còn 0)
↳ chuyển trạng thái: Đã có tiền → Hết hàng
[Hết hàng] Bỏ tiền
Hết hàng! Trả lại tiền
🔧 Nạp thêm 5 chai
↳ chuyển trạng thái: Hết hàng → Chờ tiền
[Chờ tiền] Bỏ tiền
Đã nhận tiền
↳ chuyển trạng thái: Chờ tiền → Đã có tiềnGiải thích code Java 17+
- Lớp
VendingMachinekhông có một câuif/switchnào theo trạng thái — mọi thứ được uỷ quyền. - Mỗi lớp State gom đủ 3 hành vi của trạng thái đó tại một chỗ → dễ đọc, dễ kiểm thử.
HasCoinState.pressButton()tự quyết định trạng thái tiếp theo dựa vào số hàng còn lại.- Log in rõ mọi lần chuyển trạng thái, khớp với sơ đồ trạng thái phía trên.
6Áp dụng trong lập trình nhúng
- Máy trạng thái hữu hạn (FSM) là xương sống của firmware: đèn giao thông, robot dò line (FOLLOW → AVOID → STOP), máy giặt, giao thức truyền thông.
- Ba cách cài FSM trong C:
switchtrênenum(đơn giản), bảng chuyển trạng thái (mảng con trỏ hàm), và mẫu State hướng đối tượng (dễ mở rộng nhất). - Kết hợp với
millis()để mỗi trạng thái tự kiểm tra hết thời gian mà không dùngdelay()chặn chương trình.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Single Responsibility: mỗi trạng thái một lớp.
- Open/Closed: thêm trạng thái mới không sửa trạng thái cũ hay Context.
- Loại bỏ các khối điều kiện cồng kềnh.
✘ Nhược điểm
- Thừa thãi nếu chỉ có vài trạng thái và ít khi thay đổi.
- Số lớp tăng theo số trạng thái.
8Khi nào nên dùng
- Hành vi của đối tượng phụ thuộc mạnh vào trạng thái và có nhiều trạng thái.
- Code có nhiều
switch/iflặp lại theo cùng một biến trạng thái. - Máy trạng thái: đèn giao thông, đơn hàng, nhân vật game, robot (dò line → tránh vật cản → dừng).
9Gặp ở đâu trong thực tế
C++ / nhúng
- Boost.MSM, Boost.SML (FSM bằng template)
- Trạng thái kết nối WiFi/BLE trong ESP-IDF
- Marlin: trạng thái máy in
Python
- Thư viện
transitions asynciotrạng thái của Future/Task- Trạng thái kết nối của
http.client
Java
javax.faces.lifecycle.LifeCycle- Trạng thái đơn hàng trong thương mại điện tử
- Spring State Machine
- Bộ điều khiển robot dùng máy trạng thái hữu hạn (FSM)
10Liên hệ với các mẫu khác
Strategy có cấu trúc giống State. Các đối tượng State có thể là Singleton/Flyweight nếu không có field riêng. Bridge cũng dựa trên composition nhưng nhằm tách hai chiều phát triển.
11Bài tập thực hành
♟️ Strategy — Chiến lược
Mục đích: Định nghĩa một họ thuật toán, đóng gói từng thuật toán thành lớp riêng và làm cho chúng có thể hoán đổi cho nhau lúc chạy.
1Vấn đề cần giải quyết
Giỏ hàng hỗ trợ nhiều phương thức thanh toán: tiền mặt, thẻ, ví MoMo… mỗi cách tính phí và xử lý khác nhau.
Viết tất cả trong ShoppingCart.checkout() bằng if (method == "card") … else if …: lớp ngày càng phình to; thêm ZaloPay phải sửa lớp giỏ hàng (có nguy cơ làm hỏng phần đang chạy); khó kiểm thử từng phương thức riêng.
2Giải pháp
Tách mỗi thuật toán ra một lớp cài đặt interface chung Strategy (PaymentStrategy). Lớp Context (ShoppingCart) chỉ giữ/nhận một strategy và gọi nó, không biết chi tiết bên trong.
Client chọn strategy phù hợp và đưa cho context — có thể đổi bất cứ lúc nào.
① interface + hàm ảo — đổi lúc chạy;
② lambda /
std::function — gọn khi strategy chỉ có một hàm;③ template (policy-based design) — chọn lúc biên dịch, không tốn chi phí gọi hàm ảo, trình biên dịch có thể inline → rất được ưa chuộng trong firmware.
Trong Python, strategy thường chỉ là một hàm truyền vào.
3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Strategy (PaymentStrategy) | Interface chung cho mọi thuật toán. |
| ConcreteStrategy (CashPayment…) | Cài đặt một thuật toán cụ thể. |
| Context (ShoppingCart) | Dùng strategy qua interface; có thể nhận strategy qua tham số hoặc setter. |
| Client | Chọn strategy và đưa cho context. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Strategy – C++17. Có 3 cách cài đặt trong C++:
// (1) interface + virtual (lúc chạy) (2) std::function / lambda (lúc chạy)
// (3) template (lúc BIÊN DỊCH – "policy-based design", không tốn chi phí gọi hàm ảo; rất được ưa chuộng trong nhúng)
#include <cstdio>
#include <functional>
#include <string>
// ===== (1) Strategy dạng interface =====
class PaymentStrategy {
public:
virtual ~PaymentStrategy() = default;
virtual const char* name() const = 0;
virtual int fee(int amount) const = 0;
virtual void pay(int amount) const = 0;
};
class CashPayment : public PaymentStrategy {
public:
const char* name() const override { return "Tiền mặt"; }
int fee(int) const override { return 0; }
void pay(int a) const override { std::printf(" 💵 Thanh toán tiền mặt khi nhận hàng: %d đ\n", a); }
};
class CardPayment : public PaymentStrategy {
public:
explicit CardPayment(std::string n) : number_(std::move(n)) {}
const char* name() const override { return "Thẻ"; }
int fee(int a) const override { return a * 2 / 100; }
void pay(int a) const override { std::printf(" 💳 Trừ %d đ từ thẻ ****%s\n", a, number_.substr(number_.size() - 4).c_str()); }
private:
std::string number_;
};
class MomoPayment : public PaymentStrategy {
public:
explicit MomoPayment(std::string p) : phone_(std::move(p)) {}
const char* name() const override { return "MoMo"; }
int fee(int) const override { return 1000; }
void pay(int a) const override { std::printf(" 📱 Gửi yêu cầu %d đ tới ví MoMo %s\n", a, phone_.c_str()); }
private:
std::string phone_;
};
// ===== Context =====
class ShoppingCart {
public:
void addItem(const char*, int price) { subtotal_ += price; }
void checkout(const PaymentStrategy& s) const {
int f = s.fee(subtotal_);
std::printf("Thanh toán bằng %s (phí %d đ)\n", s.name(), f);
s.pay(subtotal_ + f);
}
// (2) strategy dạng lambda
int total(const std::function<int(int)>& discount) const { return discount(subtotal_); }
// (3) strategy dạng template – chọn lúc biên dịch
template <typename DiscountPolicy>
int totalWith() const { return DiscountPolicy::apply(subtotal_); }
private:
int subtotal_ = 0;
};
struct StudentDiscount { static constexpr int apply(int p) { return p * 90 / 100; } };
struct NoDiscount { static constexpr int apply(int p) { return p; } };
int main() {
ShoppingCart cart;
cart.addItem("Arduino Uno R3", 180000);
cart.addItem("Bộ cảm biến 37 món", 320000);
cart.checkout(CashPayment());
cart.checkout(CardPayment("4111-2222-3333-4444"));
cart.checkout(MomoPayment("0359 581 461"));
std::printf("-- Chiến lược giảm giá dạng lambda --\n");
auto blackFriday = [](int p) { return p > 100000 ? p - 100000 : 0; };
std::printf("Black Friday: %d\n", cart.total(blackFriday));
std::printf("-- Chiến lược dạng template (biên dịch) --\n");
std::printf("Giá gốc: %d | HS-SV: %d\n", cart.totalWith<NoDiscount>(), cart.totalWith<StudentDiscount>());
}
g++ -std=c++17 22-strategy.cpp -o app && ./appThanh toán bằng Tiền mặt (phí 0 đ) 💵 Thanh toán tiền mặt khi nhận hàng: 500000 đ Thanh toán bằng Thẻ (phí 10000 đ) 💳 Trừ 510000 đ từ thẻ ****4444 Thanh toán bằng MoMo (phí 1000 đ) 📱 Gửi yêu cầu 501000 đ tới ví MoMo 0359 581 461 -- Chiến lược giảm giá dạng lambda -- Black Friday: 400000 -- Chiến lược dạng template (biên dịch) -- Giá gốc: 500000 | HS-SV: 450000
Giải thích code C++17
- Ví dụ thể hiện cả ba cách: interface + hàm ảo (
checkout), lambda/std::function(total), và template policy (totalWith<StudentDiscount>()). - Policy là struct có hàm
static constexpr— trình biên dịch có thể tính sẵn và inline, không tốn chi phí lúc chạy. - Strategy được truyền bằng
const&— tạo tạm thời trên stack.
"""Strategy – Python 3. Hàm là đối tượng hạng nhất, nên strategy thường chỉ là một hàm."""
from abc import ABC, abstractmethod
class PaymentStrategy(ABC):
name = "?"
@abstractmethod
def fee(self, amount) -> int: ...
@abstractmethod
def pay(self, amount): ...
class CashPayment(PaymentStrategy):
name = "Tiền mặt"
def fee(self, amount): return 0
def pay(self, amount): print(f" 💵 Thanh toán tiền mặt khi nhận hàng: {amount:,} đ")
class CardPayment(PaymentStrategy):
name = "Thẻ"
def __init__(self, number): self.number = number
def fee(self, amount): return amount * 2 // 100
def pay(self, amount): print(f" 💳 Trừ {amount:,} đ từ thẻ ****{self.number[-4:]}")
class MomoPayment(PaymentStrategy):
name = "MoMo"
def __init__(self, phone): self.phone = phone
def fee(self, amount): return 1_000
def pay(self, amount): print(f" 📱 Gửi yêu cầu {amount:,} đ tới ví MoMo {self.phone}")
class ShoppingCart:
def __init__(self): self.subtotal = 0
def add_item(self, name, price): self.subtotal += price
def checkout(self, strategy: PaymentStrategy):
f = strategy.fee(self.subtotal)
print(f"Thanh toán bằng {strategy.name:<9} (phí {f:>6,} đ)")
strategy.pay(self.subtotal + f)
def total(self, discount=lambda p: p): # strategy là hàm, mặc định không giảm
return discount(self.subtotal)
if __name__ == "__main__":
cart = ShoppingCart()
cart.add_item("Arduino Uno R3", 180_000)
cart.add_item("Bộ cảm biến 37 món", 320_000)
for s in (CashPayment(), CardPayment("4111-2222-3333-4444"), MomoPayment("0359 581 461")):
cart.checkout(s)
print("-- Chiến lược giảm giá dạng hàm --")
print(f"Giá gốc: {cart.total():,} | HS-SV: {cart.total(lambda p: p * 90 // 100):,} | "
f"Black Friday: {cart.total(lambda p: max(0, p - 100_000)):,}")
python 22-strategy.pyThanh toán bằng Tiền mặt (phí 0 đ) 💵 Thanh toán tiền mặt khi nhận hàng: 500,000 đ Thanh toán bằng Thẻ (phí 10,000 đ) 💳 Trừ 510,000 đ từ thẻ ****4444 Thanh toán bằng MoMo (phí 1,000 đ) 📱 Gửi yêu cầu 501,000 đ tới ví MoMo 0359 581 461 -- Chiến lược giảm giá dạng hàm -- Giá gốc: 500,000 | HS-SV: 450,000 | Black Friday: 400,000
Giải thích code Python 3
- Strategy thường chỉ là một hàm:
cart.total(lambda p: p * 90 // 100). - Tham số mặc định
discount=lambda p: plà strategy “không làm gì”.
import java.util.function.IntUnaryOperator;
public class StrategyDemo {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.addItem("Arduino Uno R3", 180_000);
cart.addItem("Bộ cảm biến 37 món", 320_000);
// Đổi thuật toán thanh toán lúc chạy (runtime) mà không sửa ShoppingCart
cart.checkout(new CashPayment());
cart.checkout(new CardPayment("4111-2222-3333-4444"));
cart.checkout(new MomoPayment("0359 581 461"));
// Với Java 8+, strategy đơn giản có thể là lambda (functional interface)
System.out.println("-- Chiến lược giảm giá dạng lambda --");
IntUnaryOperator noDiscount = p -> p;
IntUnaryOperator studentDiscount = p -> p * 90 / 100;
IntUnaryOperator blackFriday = p -> Math.max(0, p - 100_000);
System.out.printf("Giá gốc: %,d | HS-SV: %,d | Black Friday: %,d%n",
cart.total(noDiscount), cart.total(studentDiscount), cart.total(blackFriday));
}
}
/** Strategy */
interface PaymentStrategy {
String name();
int fee(int amount); // mỗi phương thức có cách tính phí riêng
void pay(int amount);
}
/** ConcreteStrategies */
class CashPayment implements PaymentStrategy {
public String name() { return "Tiền mặt"; }
public int fee(int amount) { return 0; }
public void pay(int amount) { System.out.printf(" 💵 Thanh toán tiền mặt khi nhận hàng: %,d đ%n", amount); }
}
class CardPayment implements PaymentStrategy {
private final String cardNumber;
CardPayment(String n) { cardNumber = n; }
public String name() { return "Thẻ"; }
public int fee(int amount) { return amount * 2 / 100; } // phí 2%
public void pay(int amount) {
System.out.printf(" 💳 Trừ %,d đ từ thẻ ****%s%n", amount, cardNumber.substring(cardNumber.length() - 4));
}
}
class MomoPayment implements PaymentStrategy {
private final String phone;
MomoPayment(String p) { phone = p; }
public String name() { return "MoMo"; }
public int fee(int amount) { return 1_000; } // phí cố định
public void pay(int amount) { System.out.printf(" 📱 Gửi yêu cầu %,d đ tới ví MoMo %s%n", amount, phone); }
}
/** Context */
class ShoppingCart {
private int subtotal = 0;
void addItem(String name, int price) { subtotal += price; }
void checkout(PaymentStrategy strategy) {
int total = subtotal + strategy.fee(subtotal);
System.out.printf("Thanh toán bằng %-9s (phí %,6d đ)%n", strategy.name(), strategy.fee(subtotal));
strategy.pay(total);
}
int total(IntUnaryOperator discount) { return discount.applyAsInt(subtotal); }
}
java 22-strategy.javaThanh toán bằng Tiền mặt (phí 0 đ) 💵 Thanh toán tiền mặt khi nhận hàng: 500,000 đ Thanh toán bằng Thẻ (phí 10,000 đ) 💳 Trừ 510,000 đ từ thẻ ****4444 Thanh toán bằng MoMo (phí 1,000 đ) 📱 Gửi yêu cầu 501,000 đ tới ví MoMo 0359 581 461 -- Chiến lược giảm giá dạng lambda -- Giá gốc: 500,000 | HS-SV: 450,000 | Black Friday: 400,000
Giải thích code Java 17+
- Cùng một giỏ hàng, gọi
checkout()với ba strategy khác nhau → ba cách tính phí và thanh toán. ShoppingCartkhông cóifnào theo phương thức thanh toán.- Phần giảm giá minh hoạ cách viết strategy bằng lambda với functional interface
IntUnaryOperator.
6Áp dụng trong lập trình nhúng
- Thuật toán điều khiển động cơ hoán đổi được: Bang-bang, P, PID — chọn qua cấu hình hoặc lệnh Bluetooth.
- Bộ lọc tín hiệu cảm biến: trung bình trượt, median, Kalman — cùng interface
filter(sample). - Chiến lược tiết kiệm năng lượng: luôn bật, ngủ nhẹ, ngủ sâu theo mức pin.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Hoán đổi thuật toán lúc chạy.
- Tách chi tiết thuật toán khỏi code sử dụng.
- Thay kế thừa bằng composition.
- Open/Closed: thêm strategy mới không sửa context.
✘ Nhược điểm
- Client phải biết sự khác nhau giữa các strategy để chọn.
- Thừa nếu chỉ có 2 thuật toán ít thay đổi — lambda có thể đủ.
8Khi nào nên dùng
- Có nhiều biến thể của một thuật toán và cần đổi lúc chạy.
- Nhiều lớp chỉ khác nhau ở cách thực hiện một hành vi.
- Thay thế khối điều kiện lớn chọn thuật toán.
9Gặp ở đâu trong thực tế
C++ / nhúng
std::sort(begin, end, comparator)- Tham số
Allocator,Hash,Comparecủa container STL (policy) - Thư viện PID_v1 với các chế độ
Python
sorted(data, key=...)max(..., key=...)- scikit-learn: tham số
solver=
Java
java.util.Comparatortruyền vàoCollections.sort()/List.sort()javax.servlet.http.HttpServlet(service → doGet/doPost)- Chiến lược nén, mã hoá, định tuyến; thuật toán tìm đường A*/Dijkstra trong game
10Liên hệ với các mẫu khác
State có cấu trúc giống nhưng các trạng thái biết và chuyển cho nhau. Template Method dùng kế thừa để thay một phần thuật toán (tĩnh, lúc biên dịch); Strategy dùng composition để thay cả thuật toán (động, lúc chạy). Decorator thay vỏ, Strategy thay ruột.
11Bài tập thực hành
📐 Template Method — Phương thức khuôn mẫu
Mục đích: Định nghĩa bộ khung (skeleton) của một thuật toán trong lớp cha, cho phép lớp con định nghĩa lại một số bước mà không thay đổi cấu trúc tổng thể của thuật toán.
1Vấn đề cần giải quyết
Ứng dụng xuất báo cáo điểm ra CSV, HTML, PDF… Mọi định dạng đều theo các bước: tải dữ liệu → lọc → ghi tiêu đề → ghi từng dòng → ghi cuối trang.
Viết ba lớp độc lập thì phần tải dữ liệu, trình tự gọi các bước bị lặp lại ở cả ba. Sửa cách tải dữ liệu phải sửa ba nơi; ai đó có thể “quên” bước lọc ở một định dạng.
2Giải pháp
Chia thuật toán thành các bước, đặt trong lớp trừu tượng. Một phương thức template (generate(), khai báo final) gọi các bước theo đúng trình tự. Các bước gồm:
- Bước chung: cài đặt sẵn ở lớp cha (
loadData()). - Bước trừu tượng (primitive operations): lớp con bắt buộc cài đặt (
writeHeader(),writeRow()…). - Hook: có cài đặt mặc định (thường rỗng), lớp con có thể ghi đè để chen vào (
filter(),shouldLog()).
3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| AbstractClass (ReportGenerator) | Định nghĩa template method và các bước trừu tượng/hook. |
| ConcreteClass (CsvReport, HtmlReport) | Cài đặt các bước trừu tượng, tuỳ chọn ghi đè hook. |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Template Method – C++17. Ví dụ quen thuộc nhất với người làm nhúng:
// khung chương trình Arduino gọi setup() một lần rồi loop() mãi mãi – bạn chỉ "điền các bước".
#include <iostream>
#include <string>
#include <vector>
struct Row { std::string name; double score; };
// ===== AbstractClass =====
class ReportGenerator {
public:
virtual ~ReportGenerator() = default;
// TEMPLATE METHOD – không virtual, lớp con không thể đổi trình tự
void generate() {
std::vector<Row> rows = loadData();
rows = filter(rows); // hook
writeHeader();
for (const Row& r : rows) writeRow(r);
writeFooter(rows.size());
if (shouldLog()) std::cout << " (đã ghi log xuất báo cáo)\n"; // hook
}
protected:
// Hook – có mặc định, CÓ THỂ ghi đè
virtual std::vector<Row> filter(const std::vector<Row>& rows) { return rows; }
virtual bool shouldLog() const { return false; }
// Primitive operations – BẮT BUỘC ghi đè (thuần ảo)
virtual void writeHeader() = 0;
virtual void writeRow(const Row& r) = 0;
virtual void writeFooter(size_t count) = 0;
private:
std::vector<Row> loadData() const { return { {"An", 9.0}, {"Bình", 6.5}, {"Chi", 4.0} }; }
};
class CsvReport : public ReportGenerator {
protected:
void writeHeader() override { std::cout << "ho_ten,diem\n"; }
void writeRow(const Row& r) override { std::cout << r.name << "," << r.score << "\n"; }
void writeFooter(size_t n) override { std::cout << "# " << n << " dòng\n"; }
};
class HtmlReport : public ReportGenerator {
protected:
std::vector<Row> filter(const std::vector<Row>& rows) override {
std::vector<Row> passed;
for (const Row& r : rows) if (r.score >= 5) passed.push_back(r);
return passed;
}
bool shouldLog() const override { return true; }
void writeHeader() override { std::cout << "<table><tr><th>Họ tên</th><th>Điểm</th></tr>\n"; }
void writeRow(const Row& r) override { std::cout << " <tr><td>" << r.name << "</td><td>" << r.score << "</td></tr>\n"; }
void writeFooter(size_t n) override { std::cout << "</table> <!-- " << n << " học sinh đạt -->\n"; }
};
int main() {
CsvReport csv;
HtmlReport html;
ReportGenerator* generators[] = { &csv, &html };
for (ReportGenerator* g : generators) {
g->generate();
std::cout << "\n";
}
}
g++ -std=c++17 23-template-method.cpp -o app && ./appho_ten,diem An,9 Bình,6.5 Chi,4 # 3 dòng <table><tr><th>Họ tên</th><th>Điểm</th></tr> <tr><td>An</td><td>9</td></tr> <tr><td>Bình</td><td>6.5</td></tr> </table> <!-- 2 học sinh đạt --> (đã ghi log xuất báo cáo)
Giải thích code C++17
generate()không phải hàm ảo nên lớp con không ghi đè được trình tự (tương đươngfinal).- Các bước bắt buộc là hàm thuần ảo
= 0; hook là hàm ảo có cài đặt mặc định. - Đặt các bước ở
protectedđể client chỉ thấygenerate()(idiom Non-Virtual Interface — NVI).
"""Template Method – Python 3."""
from abc import ABC, abstractmethod
class ReportGenerator(ABC):
def generate(self): # TEMPLATE METHOD (Python không có final – quy ước không ghi đè)
rows = self._load_data()
rows = self.filter(rows) # hook
self.write_header()
for r in rows:
self.write_row(r)
self.write_footer(len(rows))
if self.should_log(): # hook
print(" (đã ghi log xuất báo cáo)")
def _load_data(self):
return [("An", 9.0), ("Bình", 6.5), ("Chi", 4.0)]
# Hooks
def filter(self, rows): return rows
def should_log(self): return False
# Primitive operations
@abstractmethod
def write_header(self): ...
@abstractmethod
def write_row(self, r): ...
@abstractmethod
def write_footer(self, n): ...
class CsvReport(ReportGenerator):
def write_header(self): print("ho_ten,diem")
def write_row(self, r): print(f"{r[0]},{r[1]}")
def write_footer(self, n): print(f"# {n} dòng")
class HtmlReport(ReportGenerator):
def filter(self, rows): return [r for r in rows if r[1] >= 5]
def should_log(self): return True
def write_header(self): print("<table><tr><th>Họ tên</th><th>Điểm</th></tr>")
def write_row(self, r): print(f" <tr><td>{r[0]}</td><td>{r[1]}</td></tr>")
def write_footer(self, n): print(f"</table> <!-- {n} học sinh đạt -->")
if __name__ == "__main__":
for g in (CsvReport(), HtmlReport()):
g.generate()
print()
python 23-template-method.pyho_ten,diem An,9.0 Bình,6.5 Chi,4.0 # 3 dòng <table><tr><th>Họ tên</th><th>Điểm</th></tr> <tr><td>An</td><td>9.0</td></tr> <tr><td>Bình</td><td>6.5</td></tr> </table> <!-- 2 học sinh đạt --> (đã ghi log xuất báo cáo)
Giải thích code Python 3
- Python không có
finalthật cho phương thức; đặt tên rõ ràng và tài liệu hoá rằnggenerate()không nên ghi đè. @abstractmethodbuộc lớp con cài các bước bắt buộc.
import java.util.List;
public class TemplateMethodDemo {
public static void main(String[] args) {
ReportGenerator[] generators = { new CsvReport(), new HtmlReport() };
for (ReportGenerator g : generators) {
g.generate();
System.out.println();
}
}
}
/** AbstractClass – định nghĩa "khung" thuật toán, các bước cụ thể để lớp con lo. */
abstract class ReportGenerator {
/** TEMPLATE METHOD – final để lớp con không phá vỡ trình tự các bước. */
public final void generate() {
List<String[]> rows = loadData(); // bước chung
rows = filter(rows); // hook (tuỳ chọn ghi đè)
writeHeader(); // bước trừu tượng
for (String[] r : rows) writeRow(r); // bước trừu tượng
writeFooter(rows.size()); // bước trừu tượng
if (shouldLog()) System.out.println(" (đã ghi log xuất báo cáo)"); // hook
}
/** Bước chung, cài đặt sẵn. */
private List<String[]> loadData() {
return List.of(
new String[]{"An", "9.0"},
new String[]{"Bình", "6.5"},
new String[]{"Chi", "4.0"});
}
/** Hook – có cài đặt mặc định, lớp con CÓ THỂ ghi đè. */
protected List<String[]> filter(List<String[]> rows) { return rows; }
protected boolean shouldLog() { return false; }
/** Primitive operations – lớp con BẮT BUỘC cài đặt. */
protected abstract void writeHeader();
protected abstract void writeRow(String[] row);
protected abstract void writeFooter(int count);
}
class CsvReport extends ReportGenerator {
protected void writeHeader() { System.out.println("ho_ten,diem"); }
protected void writeRow(String[] r) { System.out.println(r[0] + "," + r[1]); }
protected void writeFooter(int count) { System.out.println("# " + count + " dòng"); }
}
class HtmlReport extends ReportGenerator {
/** Ghi đè hook: chỉ lấy học sinh đạt (>= 5). */
@Override protected List<String[]> filter(List<String[]> rows) {
return rows.stream().filter(r -> Double.parseDouble(r[1]) >= 5).toList();
}
@Override protected boolean shouldLog() { return true; }
protected void writeHeader() { System.out.println("<table><tr><th>Họ tên</th><th>Điểm</th></tr>"); }
protected void writeRow(String[] r) { System.out.println(" <tr><td>" + r[0] + "</td><td>" + r[1] + "</td></tr>"); }
protected void writeFooter(int count) { System.out.println("</table> <!-- " + count + " học sinh đạt -->"); }
}
java 23-template-method.javaho_ten,diem An,9.0 Bình,6.5 Chi,4.0 # 3 dòng <table><tr><th>Họ tên</th><th>Điểm</th></tr> <tr><td>An</td><td>9.0</td></tr> <tr><td>Bình</td><td>6.5</td></tr> </table> <!-- 2 học sinh đạt --> (đã ghi log xuất báo cáo)
Giải thích code Java 17+
generate()làfinal— lớp con không thể đổi trình tự các bước.CsvReportchỉ cài đặt 3 bước bắt buộc, dùng hook mặc định (không lọc, không log).HtmlReportghi đè hookfilter()để chỉ lấy học sinh đạt vàshouldLog()để bật log.- Dữ liệu chỉ được tải ở một chỗ duy nhất (
loadData()) — không còn trùng lặp.
6Áp dụng trong lập trình nhúng
- Khung Arduino chính là Template Method: lõi Arduino viết sẵn
main()gọiinit(),setup(), rồi lặploop()— bạn chỉ điền các bước. - Lớp trừu tượng
SensorTaskvớirun()=wake() → measure() → filter() → publish() → sleep(); mỗi cảm biến chỉ càimeasure(). - Quy trình hiệu chuẩn: các bước giống nhau, chỉ khác cách đọc giá trị chuẩn.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Loại bỏ code trùng lặp — phần chung nằm ở lớp cha.
- Kiểm soát điểm mở rộng: lớp con chỉ ghi đè những gì được phép.
- Dễ thêm biến thể mới.
✘ Nhược điểm
- Bị giới hạn bởi bộ khung đã định nghĩa.
- Có thể vi phạm Liskov nếu lớp con làm sai “hợp đồng” của bước.
- Càng nhiều bước càng khó bảo trì; dựa trên kế thừa nên kém linh hoạt hơn composition.
8Khi nào nên dùng
- Nhiều lớp có thuật toán giống nhau, chỉ khác vài bước.
- Muốn kiểm soát lớp con chỉ mở rộng ở những điểm nhất định.
- Xây dựng framework: framework định luồng, người dùng điền các bước.
9Gặp ở đâu trong thực tế
C++ / nhúng
- Arduino core
main.cppgọisetup()/loop() - Google Test:
SetUp()→ test →TearDown() - Qt:
QThread::run()
Python
unittest.TestCase:setUp/tearDownthreading.Thread.run()socketserver.BaseRequestHandler.handle()
Java
java.io.InputStream.read(byte[], int, int)gọiread()trừu tượngjava.util.AbstractList,AbstractMapHttpServlet.service()gọidoGet()/doPost()- JUnit:
@BeforeEach→ test →@AfterEach; vòngsetup()/loop()của Arduino
10Liên hệ với các mẫu khác
Factory Method là một dạng đặc biệt của Template Method (một bước là “tạo đối tượng”). Strategy thay toàn bộ thuật toán bằng composition lúc chạy; Template Method thay một phần bằng kế thừa lúc biên dịch.
11Bài tập thực hành
🧳 Visitor — Người viếng thăm
Mục đích: Cho phép thêm thao tác mới lên các đối tượng của một cấu trúc mà không cần sửa các lớp của những đối tượng đó — bằng cách tách thao tác ra một đối tượng “khách viếng thăm”.
1Vấn đề cần giải quyết
Phần mềm vẽ có các lớp hình: Circle, Rectangle, Triangle. Liên tục có yêu cầu thao tác mới: tính diện tích, xuất JSON, xuất XML, vẽ ra SVG…
Nếu mỗi lần lại thêm một phương thức vào mọi lớp hình → các lớp hình phình to với những trách nhiệm chẳng liên quan (xuất XML là việc của hình tròn sao?), và mỗi thay đổi phải sửa nhiều lớp đã ổn định.
2Giải pháp
Đưa mỗi thao tác vào một lớp Visitor riêng, có một phương thức visit() cho từng loại phần tử. Mỗi phần tử chỉ cần một phương thức duy nhất: accept(visitor) gọi visitor.visit(this).
Double dispatch: lời gọi shape.accept(v) chọn đúng lớp phần tử (dispatch lần 1, đa hình), sau đó v.visit(this) chọn đúng overload theo kiểu tĩnh của this (dispatch lần 2). Nhờ vậy không cần instanceof.
std::variant + std::visit — không cần hàm ảo, không cấp phát động; Python 3.10 match/case.)3Sơ đồ lớp UML
snake_case. Xem bảng ký hiệu UML4Các thành phần tham gia
| Thành phần | Vai trò |
|---|---|
| Visitor (ShapeVisitor) | Khai báo visit() cho từng ConcreteElement. |
| ConcreteVisitor (AreaVisitor…) | Cài đặt một thao tác cho mọi loại phần tử; có thể tích luỹ trạng thái. |
| Element (Shape) | Khai báo accept(visitor). |
| ConcreteElement (Circle…) | Cài đặt accept bằng cách gọi visitor.visit(this). |
| ObjectStructure | Tập hợp phần tử có thể duyệt (danh sách drawing). |
5Code ví dụ chi tiết
Cùng một kịch bản, cùng tên lớp, viết bằng 3 ngôn ngữ — chọn tab để so sánh. C++ là ngôn ngữ chính của khóa (gần với lập trình nhúng nhất).
// Visitor – C++17. Bản cổ điển dùng double dispatch; cuối file có bản hiện đại
// dùng std::variant + std::visit (C++17) – không cần hàm ảo hay cấp phát động.
#include <cmath>
#include <cstdio>
#include <memory>
#include <string>
#include <variant>
#include <vector>
constexpr double PI = 3.14159265358979323846; // M_PI không thuộc chuẩn C++
struct Circle; struct Rectangle; struct Triangle;
// ===== Visitor =====
class ShapeVisitor {
public:
virtual ~ShapeVisitor() = default;
virtual void visit(const Circle& c) = 0;
virtual void visit(const Rectangle& r) = 0;
virtual void visit(const Triangle& t) = 0;
};
// ===== Element =====
struct Shape {
virtual ~Shape() = default;
virtual void accept(ShapeVisitor& v) const = 0;
};
// ===== Concrete Elements – double dispatch =====
struct Circle : Shape {
double r;
explicit Circle(double r) : r(r) {}
void accept(ShapeVisitor& v) const override { v.visit(*this); }
};
struct Rectangle : Shape {
double w, h;
Rectangle(double w, double h) : w(w), h(h) {}
void accept(ShapeVisitor& v) const override { v.visit(*this); }
};
struct Triangle : Shape {
double base, height;
Triangle(double b, double h) : base(b), height(h) {}
void accept(ShapeVisitor& v) const override { v.visit(*this); }
};
// ===== Concrete Visitors =====
class AreaVisitor : public ShapeVisitor {
public:
void visit(const Circle& c) override { add("Hình tròn", PI * c.r * c.r); }
void visit(const Rectangle& r) override { add("Chữ nhật", r.w * r.h); }
void visit(const Triangle& t) override { add("Tam giác", t.base * t.height / 2); }
double total() const { return total_; }
private:
void add(const char* n, double a) { std::printf(" %6.2f <- %s\n", a, n); total_ += a; }
double total_ = 0;
};
class JsonExportVisitor : public ShapeVisitor {
public:
void visit(const Circle& c) override { items_.push_back("{\"type\":\"circle\",\"r\":" + num(c.r) + "}"); }
void visit(const Rectangle& r) override { items_.push_back("{\"type\":\"rect\",\"w\":" + num(r.w) + ",\"h\":" + num(r.h) + "}"); }
void visit(const Triangle& t) override { items_.push_back("{\"type\":\"triangle\",\"base\":" + num(t.base) + ",\"height\":" + num(t.height) + "}"); }
std::string result() const {
std::string s = "[\n";
for (size_t i = 0; i < items_.size(); ++i) s += " " + items_[i] + (i + 1 < items_.size() ? ",\n" : "\n");
return s + "]";
}
private:
static std::string num(double d) { char b[32]; std::snprintf(b, sizeof b, "%g", d); return b; }
std::vector<std::string> items_;
};
// ===== Bản hiện đại: std::variant + std::visit =====
struct CircleV { double r; };
struct RectV { double w, h; };
using ShapeV = std::variant<CircleV, RectV>;
template <class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template <class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
int main() {
std::vector<std::unique_ptr<Shape>> drawing;
drawing.push_back(std::make_unique<Circle>(2));
drawing.push_back(std::make_unique<Rectangle>(3, 4));
drawing.push_back(std::make_unique<Triangle>(6, 5));
AreaVisitor area;
for (const auto& s : drawing) s->accept(area);
std::printf("Tổng diện tích: %.2f\n", area.total());
std::printf("-- Xuất JSON --\n");
JsonExportVisitor json;
for (const auto& s : drawing) s->accept(json);
std::printf("%s\n", json.result().c_str());
std::printf("-- std::variant + std::visit --\n");
ShapeV shapes[] = { CircleV{2}, RectV{3, 4} }; // không heap, không hàm ảo
for (const ShapeV& s : shapes)
std::visit(overloaded{
[](const CircleV& c) { std::printf(" tròn %.2f\n", PI * c.r * c.r); },
[](const RectV& r) { std::printf(" chữ nhật %.2f\n", r.w * r.h); },
}, s);
}
g++ -std=c++17 24-visitor.cpp -o app && ./app 12.57 <- Hình tròn
12.00 <- Chữ nhật
15.00 <- Tam giác
Tổng diện tích: 39.57
-- Xuất JSON --
[
{"type":"circle","r":2},
{"type":"rect","w":3,"h":4},
{"type":"triangle","base":6,"height":5}
]
-- std::variant + std::visit --
tròn 12.57
chữ nhật 12.00Giải thích code C++17
- Double dispatch:
s->accept(v)chọn lớp phần tử qua hàm ảo, rồiv.visit(*this)chọn đúng overload theo kiểu tĩnh của*this. - Cuối file là cách hiện đại:
std::variant+std::visitvới mẫuoverloaded— không hàm ảo, không heap, và trình biên dịch báo lỗi nếu thiếu trường hợp. - Hằng
PItự định nghĩa vìM_PIkhông thuộc chuẩn C++.
"""Visitor – Python 3. Python không có nạp chồng hàm theo kiểu, nên dùng tên visit_<lớp>
và tra bằng getattr. Cuối file: cách hiện đại với match/case (Python 3.10+)."""
import json
import math
from dataclasses import dataclass, asdict
class Shape:
def accept(self, visitor):
# double dispatch: chọn phương thức theo tên lớp của chính phần tử
return getattr(visitor, "visit_" + type(self).__name__.lower())(self)
@dataclass
class Circle(Shape):
r: float
@dataclass
class Rectangle(Shape):
w: float
h: float
@dataclass
class Triangle(Shape):
base: float
height: float
class AreaVisitor:
def __init__(self): self.total = 0.0
def _add(self, label, a):
print(f" {a:6.2f} <- {label}")
self.total += a
def visit_circle(self, c): self._add("Hình tròn", math.pi * c.r ** 2)
def visit_rectangle(self, r): self._add("Chữ nhật", r.w * r.h)
def visit_triangle(self, t): self._add("Tam giác", t.base * t.height / 2)
class JsonExportVisitor:
def __init__(self): self.items = []
def visit_circle(self, c): self.items.append({"type": "circle", **asdict(c)})
def visit_rectangle(self, r): self.items.append({"type": "rect", **asdict(r)})
def visit_triangle(self, t): self.items.append({"type": "triangle", **asdict(t)})
def area(shape) -> float: # Python 3.10+: structural pattern matching
match shape:
case Circle(r=r): return math.pi * r * r
case Rectangle(w=w, h=h): return w * h
case Triangle(base=b, height=h): return b * h / 2
if __name__ == "__main__":
drawing = [Circle(2), Rectangle(3, 4), Triangle(6, 5)]
av = AreaVisitor()
for s in drawing:
s.accept(av)
print(f"Tổng diện tích: {av.total:.2f}")
print("-- Xuất JSON --")
jv = JsonExportVisitor()
for s in drawing:
s.accept(jv)
print(json.dumps(jv.items, indent=2))
print("-- match/case --")
print(f"Tổng diện tích: {sum(area(s) for s in drawing):.2f}")
python 24-visitor.py 12.57 <- Hình tròn
12.00 <- Chữ nhật
15.00 <- Tam giác
Tổng diện tích: 39.57
-- Xuất JSON --
[
{
"type": "circle",
"r": 2
},
{
"type": "rect",
"w": 3,
"h": 4
},
{
"type": "triangle",
"base": 6,
"height": 5
}
]
-- match/case --
Tổng diện tích: 39.57Giải thích code Python 3
- Python không nạp chồng theo kiểu nên dùng quy ước tên
visit_circle,visit_rectanglevàgetattrđể dispatch (giống cách moduleast.NodeVisitorlàm). - Cách hiện đại:
match/case(Python 3.10+) với mẫu cấu trúcCircle(r=r).
import java.util.List;
public class VisitorDemo {
public static void main(String[] args) {
// Cấu trúc đối tượng: bản vẽ gồm nhiều hình
List<Shape> drawing = List.of(
new Circle(2),
new Rectangle(3, 4),
new Triangle(6, 5));
// Thêm "phép toán mới" bằng cách tạo Visitor mới – KHÔNG sửa các lớp Shape
AreaVisitor area = new AreaVisitor();
for (Shape s : drawing) s.accept(area);
System.out.printf("Tổng diện tích: %.2f%n", area.getTotal());
System.out.println("-- Xuất JSON --");
JsonExportVisitor json = new JsonExportVisitor();
for (Shape s : drawing) s.accept(json);
System.out.println(json.result());
}
}
/** Element – chỉ có duy nhất phương thức accept(visitor). */
interface Shape {
void accept(ShapeVisitor v);
}
/** Visitor – mỗi loại Element có một phương thức visit riêng. */
interface ShapeVisitor {
void visit(Circle c);
void visit(Rectangle r);
void visit(Triangle t);
}
/** ConcreteElements – double dispatch: gọi v.visit(this) để Java chọn đúng overload. */
record Circle(double r) implements Shape {
public void accept(ShapeVisitor v) { v.visit(this); }
}
record Rectangle(double w, double h) implements Shape {
public void accept(ShapeVisitor v) { v.visit(this); }
}
record Triangle(double base, double height) implements Shape {
public void accept(ShapeVisitor v) { v.visit(this); }
}
/** ConcreteVisitor 1 – tính diện tích (có trạng thái tích luỹ). */
class AreaVisitor implements ShapeVisitor {
private double total = 0;
public void visit(Circle c) {
double a = Math.PI * c.r() * c.r();
System.out.printf(" Hình tròn r=%.0f -> %.2f%n", c.r(), a);
total += a;
}
public void visit(Rectangle r) {
double a = r.w() * r.h();
System.out.printf(" Chữ nhật %.0fx%.0f -> %.2f%n", r.w(), r.h(), a);
total += a;
}
public void visit(Triangle t) {
double a = t.base() * t.height() / 2;
System.out.printf(" Tam giác đáy %.0f cao %.0f -> %.2f%n", t.base(), t.height(), a);
total += a;
}
double getTotal() { return total; }
}
/** ConcreteVisitor 2 – xuất JSON. */
class JsonExportVisitor implements ShapeVisitor {
private final StringBuilder sb = new StringBuilder("[\n");
public void visit(Circle c) { sb.append(" {\"type\":\"circle\",\"r\":").append(c.r()).append("},\n"); }
public void visit(Rectangle r) { sb.append(" {\"type\":\"rect\",\"w\":").append(r.w()).append(",\"h\":").append(r.h()).append("},\n"); }
public void visit(Triangle t) { sb.append(" {\"type\":\"triangle\",\"base\":").append(t.base()).append(",\"height\":").append(t.height()).append("},\n"); }
String result() { return sb.substring(0, sb.length() - 2) + "\n]"; }
}
java 24-visitor.java Hình tròn r=2 -> 12.57
Chữ nhật 3x4 -> 12.00
Tam giác đáy 6 cao 5 -> 15.00
Tổng diện tích: 39.57
-- Xuất JSON --
[
{"type":"circle","r":2.0},
{"type":"rect","w":3.0,"h":4.0},
{"type":"triangle","base":6.0,"height":5.0}
]Giải thích code Java 17+
- Các
recordhình học chỉ cóaccept()— không có code tính diện tích hay xuất JSON. AreaVisitortích luỹ tổng diện tích qua nhiều lần visit (visitor có trạng thái).- Thêm thao tác
SvgExportVisitor: tạo 1 lớp mới, không sửa một dòng nào của Circle/Rectangle/Triangle.
6Áp dụng trong lập trình nhúng
- Trên cấu trúc cấu hình phức tạp (cây menu, danh sách thiết bị) cần nhiều thao tác: xuất JSON gửi app, lưu EEPROM, in ra Serial — mỗi thao tác một visitor.
std::variantrất hợp với firmware: biểu diễn các loại gói tin/sự kiện khác nhau trong một hàng đợi kích thước cố định, xử lý bằngstd::visit.
7Ưu điểm và nhược điểm
✔ Ưu điểm
- Open/Closed với thao tác: thêm thao tác mới không sửa lớp phần tử.
- Single Responsibility: gom các phiên bản của một thao tác vào một lớp.
- Visitor có thể tích luỹ thông tin khi duyệt cấu trúc phức tạp.
✘ Nhược điểm
- Thêm loại phần tử mới phải sửa mọi Visitor.
- Visitor có thể cần truy cập dữ liệu private của phần tử → phải công khai bớt.
- Khó hiểu với người mới (double dispatch).
8Khi nào nên dùng
- Cần thực hiện nhiều thao tác không liên quan trên một cấu trúc đối tượng phức tạp (cây Composite, AST).
- Cấu trúc phần tử ổn định, thao tác thường xuyên thay đổi.
- Trình biên dịch: kiểm tra kiểu, tối ưu, sinh mã trên cùng một AST.
9Gặp ở đâu trong thực tế
C++ / nhúng
std::visit(C++17)- Clang AST:
RecursiveASTVisitor - Boost.Variant
Python
ast.NodeVisitorfunctools.singledispatchos.walk+ hàm xử lý
Java
java.nio.file.FileVisitor/Files.walkFileTree()javax.lang.model.element.ElementVisitor(annotation processing)- ASM, JavaParser: duyệt cây bytecode/mã nguồn
10Liên hệ với các mẫu khác
Visitor thường dùng trên cây Composite và AST của Interpreter. Iterator duyệt cấu trúc, Visitor xử lý từng phần tử. Visitor là phiên bản mạnh hơn của Command — thực thi thao tác trên nhiều loại đối tượng khác nhau.