Transaction Script: Mẫu đơn giản cho logic kinh doanh
Khi xây dựng các ứng dụng doanh nghiệp, một trong những thách thức lớn nhất là cách tổ chức logic kinh doanh. Có nhiều cách để làm điều này, và Martin Fowler, trong cuốn sách nổi tiếng Patterns of Enterprise Application Architecture (2003), đã đề xuất một danh mục các mẫu để giải quyết những vấn đề này.
Trong bài viết này, chúng ta sẽ khám phá mẫu Transaction Script, một trong những mẫu đơn giản và trực tiếp nhất trong danh mục, kèm theo một ví dụ thực tiễn bằng Python.
Transaction Script là gì?
Mẫu Transaction Script tổ chức logic kinh doanh thành các thủ tục riêng biệt, trong đó mỗi thủ tục xử lý một giao dịch hoặc yêu cầu duy nhất.
Nói cách khác:
- Nếu một khách hàng muốn tạo một đặt chỗ → có một script cho việc đó.
- Nếu một khách hàng muốn hủy một đặt chỗ → có một script khác dành riêng cho việc đó.
Mỗi script bao gồm:
- Kiểm tra đầu vào.
- Quy tắc kinh doanh.
- Các thao tác đọc/ghi cơ sở dữ liệu.
- Cam kết/hoàn tác giao dịch.
Khi nào nên sử dụng?
Mẫu Transaction Script lý tưởng khi:
- Ứng dụng tương đối nhỏ.
- Các quy tắc kinh doanh đơn giản và dễ mô tả từng bước.
- Bạn muốn tốc độ trong phát triển.
- Chưa đủ lý do để đầu tư vào các kiến trúc phức tạp hơn như Domain Model.
Trường hợp sử dụng điển hình:
- Các nguyên mẫu nhanh.
- Các ứng dụng CRUD đơn giản.
- Các dịch vụ xử lý các yêu cầu rõ ràng, tuyến tính.
Khi nào nên tránh?
Tránh khi:
- Miền (domain) phức tạp và logic bắt đầu bị lặp lại giữa các script.
- Có nhiều quy tắc chung giữa các thao tác.
- Bạn cần tái sử dụng logic hoặc hành vi đối tượng phong phú.
Trong những trường hợp đó, nên chuyển sang các mẫu như Domain Model hoặc Service Layer.
Ví dụ thực tiễn: Hệ thống đặt chỗ đơn giản
Giả sử chúng ta đang xây dựng một API nhỏ cho phép:
- Tạo một đặt chỗ.
- Liệt kê các đặt chỗ đang hoạt động.
- Hủy một đặt chỗ.
Chúng ta sẽ sử dụng Python + Flask + SQLite để minh họa mẫu này.
1. Khởi tạo cơ sở dữ liệu
Tạo file db_init.py:
python
import sqlite3
def init_db(path='reservas.db'):
conn = sqlite3.connect(path)
c = conn.cursor()
c.execute('''
CREATE TABLE IF NOT EXISTS reservations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_name TEXT NOT NULL,
resource TEXT NOT NULL,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
''')
conn.commit()
conn.close()
if __name__ == '__main__':
init_db()
print("Cơ sở dữ liệu đã được khởi tạo tại reservas.db")
2. Các script giao dịch
Tạo file transactions.py:
python
import sqlite3
from contextlib import contextmanager
DB_PATH = 'reservas.db'
@contextmanager
def get_conn():
conn = sqlite3.connect(DB_PATH)
try:
yield conn
conn.commit()
except:
conn.rollback()
raise
finally:
conn.close()
def select_active_reservations():
with get_conn() as conn:
c = conn.cursor()
c.execute("SELECT id, customer_name, resource, start_time, end_time, status, created_at FROM reservations WHERE status = 'active'")
return c.fetchall()
def create_reservation_tx(customer_name, resource, start_time, end_time):
if start_time >= end_time:
raise ValueError("start_time phải trước end_time")
with get_conn() as conn:
c = conn.cursor()
c.execute('''
SELECT COUNT(*) FROM reservations
WHERE resource = ? AND status = 'active'
AND NOT (end_time <= ? OR start_time >= ?)
''', (resource, start_time, end_time))
(overlap_count,) = c.fetchone()
if overlap_count > 0:
raise ValueError("Đã có một đặt chỗ trùng thời gian")
c.execute('''
INSERT INTO reservations (customer_name, resource, start_time, end_time)
VALUES (?, ?, ?, ?)
''', (customer_name, resource, start_time, end_time))
return c.lastrowid
def cancel_reservation_tx(reservation_id):
with get_conn() as conn:
c = conn.cursor()
c.execute('SELECT status FROM reservations WHERE id = ?', (reservation_id,))
row = c.fetchone()
if not row:
raise ValueError("Không tìm thấy đặt chỗ")
if row[0] != 'active':
raise ValueError("Đặt chỗ không còn hoạt động")
c.execute('UPDATE reservations SET status = ? WHERE id = ?', ('cancelled', reservation_id))
return True
3. API Flask
Tạo file app.py:
python
from flask import Flask, request, jsonify
from transactions import create_reservation_tx, cancel_reservation_tx, select_active_reservations
from db_init import init_db
app = Flask(__name__)
init_db()
@app.route('/reservations', methods=['POST'])
def create_reservation():
payload = request.get_json()
try:
res_id = create_reservation_tx(
customer_name=payload['customer_name'],
resource=payload['resource'],
start_time=payload['start_time'],
end_time=payload['end_time']
)
return jsonify({"id": res_id}), 201
except Exception as e:
return jsonify({"error": str(e)}), 400
@app.route('/reservations', methods=['GET'])
def list_reservations():
rows = select_active_reservations()
reservations = [
{
"id": r[0],
"customer_name": r[1],
"resource": r[2],
"start_time": r[3],
"end_time": r[4],
"status": r[5],
"created_at": r[6]
}
for r in rows
]
return jsonify(reservations), 200
@app.route('/reservations/<int:res_id>/cancel', methods=['POST'])
def cancel_reservation(res_id):
try:
cancel_reservation_tx(res_id)
return jsonify({"status": "cancelled"}), 200
except Exception as e:
return jsonify({"error": str(e)}), 400
if __name__ == '__main__':
app.run(debug=True)
4. Kiểm tra nhanh với curl
Tạo một đặt chỗ:
bash
curl -X POST http://127.0.0.1:5000/reservations \
-H "Content-Type: application/json" \
-d '{"customer_name":"Ana Perez","resource":"room-A","start_time":"2025-09-20T10:00","end_time":"2025-09-20T11:00"}'
Liệt kê các đặt chỗ:
bash
curl http://127.0.0.1:5000/reservations
Hủy:
bash
curl -X POST http://127.0.0.1:5000/reservations/1/cancel
Ưu điểm
- Đơn giản và trực tiếp.
- Dễ đọc cho bất kỳ nhà phát triển nào.
- Tuyệt vời cho các nguyên mẫu hoặc dự án nhỏ.
Nhược điểm
- Có xu hướng lặp lại logic khi dự án phát triển.
- Không mở rộng tốt cho các miền phức tạp.
- Ít tái sử dụng khi các quy tắc gia tăng.
Kết luận
Mẫu Transaction Script là một điểm khởi đầu tuyệt vời để thiết kế các ứng dụng doanh nghiệp. Nếu hệ thống của bạn nhỏ, mẫu này mang lại sự rõ ràng và tốc độ. Tuy nhiên, khi độ phức tạp tăng lên, bạn sẽ cần phải tiến hóa hướng tới các mẫu khác như Domain Model hoặc Service Layer.
Các thực hành tốt nhất
- Kiểm tra đầu vào: Luôn kiểm tra dữ liệu đầu vào để tránh lỗi và đảm bảo tính toàn vẹn của dữ liệu.
- Tách biệt logic: Cố gắng tách biệt logic giữa các script để dễ dàng bảo trì và mở rộng sau này.
Những cạm bẫy phổ biến
- Lặp lại logic: Đừng để logic bị lặp lại trong các script khác nhau, điều này có thể gây khó khăn trong việc duy trì mã nguồn.
- Thiếu kiểm tra: Không quên thực hiện kiểm tra cho các trường hợp biên, điều này sẽ giúp phát hiện lỗi sớm hơn.
Mẹo hiệu suất
- Sử dụng kết nối hiệu quả: Đảm bảo rằng các kết nối cơ sở dữ liệu được quản lý hiệu quả để tránh rò rỉ tài nguyên.
- Tối ưu hóa truy vấn: Thực hiện tối ưu hóa các truy vấn SQL để cải thiện hiệu suất truy cập dữ liệu.
Phần hỏi đáp
Transaction Script có thể sử dụng cho loại ứng dụng nào?
Mẫu này phù hợp cho các ứng dụng nhỏ, nơi logic kinh doanh không quá phức tạp.
Có nên sử dụng Transaction Script cho các ứng dụng lớn không?
Không nên, vì nó có thể dẫn đến việc lặp lại logic và khó khăn trong bảo trì.
Làm thế nào để chuyển đổi từ Transaction Script sang Domain Model?
Bạn nên bắt đầu bằng cách xác định các đối tượng miền và tách biệt logic của chúng ra khỏi các script giao dịch.
So sánh với các mẫu khác
| Mẫu | Ưu điểm | Nhược điểm |
|---|---|---|
| Transaction Script | Dễ hiểu, nhanh chóng phát triển | Không mở rộng tốt, lặp lại logic |
| Domain Model | Tính tái sử dụng cao, phù hợp với miền phức tạp | Cần thời gian phát triển dài hơn |
| Service Layer | Tách biệt rõ ràng giữa các lớp | Có thể phức tạp hơn trong cấu trúc |