Interface Contracts Tập 1: Bản chất của Hợp đồng (Design by Contract).
Trong bài học này, chúng ta sẽ đi sâu vào bộ ba khái niệm cốt lõi: Tiền điều kiện (Pre-conditions), Hậu quyết (Post-conditions), và Bất biến (Invariants).
Để dễ hình dung nhất, chúng ta sẽ lấy một bài toán kinh điển: Xử lý rút tiền trong tài khoản ngân hàng (Bank Account). Nếu code không có "hợp đồng", tiền có thể bị rút âm hoặc hệ thống xử lý sai logic. Tư duy Contract sẽ khóa chặt các lỗi này.
Dưới đây là ví dụ thực tế được viết bằng TypeScript.
1. Định nghĩa Hợp đồng (Interface)
Đầu tiên, chúng ta định nghĩa "Cái gì" (What) cần làm, kèm theo các cam kết bằng document để Client biết họ đang ký vào cái gì.
// IBankAccount.ts
export interface IBankAccount {
/**
* Rút tiền khỏi tài khoản.
*
* [Pre-condition]: amount phải > 0.
* [Pre-condition]: amount không được vượt quá số dư hiện tại (balance).
*
* [Post-condition]: Trả về mã giao dịch (Transaction ID) hợp lệ.
* [Post-condition]: Số dư tài khoản sẽ bị trừ đi đúng bằng 'amount'.
*/
withdraw(amount: number): string;
getBalance(): number;
}
2. Thực thi Hợp đồng (Implementation)
Bây giờ, chúng ta viết class thực thi. Class này có trách nhiệm ép buộc (enforce) tất cả các điều khoản của bản hợp đồng trên.
// BankAccount.ts
import { IBankAccount } from './IBankAccount';
export class BankAccount implements IBankAccount {
private balance: number;
constructor(initialBalance: number) {
if (initialBalance < 0) {
throw new Error("Pre-condition failed: Số dư ban đầu không được âm.");
}
this.balance = initialBalance;
this.checkInvariants();
}
public getBalance(): number {
return this.balance;
}
public withdraw(amount: number): string {
// ==========================================
// 1. KIỂM TRA PRE-CONDITIONS (Client phải tuân thủ)
// ==========================================
if (amount <= 0) {
throw new Error("Pre-condition failed: Số tiền rút phải lớn hơn 0.");
}
if (amount > this.balance) {
throw new Error(`Pre-condition failed: Số dư không đủ. Cần ${amount}, hiện có ${this.balance}.`);
}
// Lưu lại trạng thái cũ để kiểm tra Post-condition
const balanceBeforeTx = this.balance;
// Xử lý logic nghiệp vụ
this.balance -= amount;
const transactionId = `TXN-${Date.now()}`;
// ==========================================
// 2. KIỂM TRA POST-CONDITIONS (Provider cam kết)
// ==========================================
if (this.balance !== balanceBeforeTx - amount) {
throw new Error("Post-condition failed: Trừ tiền sai logic.");
}
if (!transactionId || transactionId.trim() === "") {
throw new Error("Post-condition failed: Lỗi sinh mã giao dịch.");
}
// ==========================================
// 3. KIỂM TRA INVARIANTS (Sự thật bất biến)
// ==========================================
this.checkInvariants();
return transactionId;
}
/**
* Hàm này kiểm tra các "Bất biến" - những trạng thái mà
* một tài khoản ngân hàng LUÔN PHẢI GIỮ DÙ CÓ CHUYỆN GÌ XẢY RA.
*/
private checkInvariants(): void {
if (this.balance < 0) {
throw new Error("Invariant failed: Số dư tài khoản tuyệt đối không được rơi vào số âm!");
}
}
}
3. Phân tích tư duy Contract qua đoạn code trên
Pre-conditions (Phòng tuyến đầu tiên)
- Ý nghĩa: Trách nhiệm thuộc về người gọi hàm (Client).
- Hành động: Chặn đứng các tham số rác ngay từ cửa. Nếu Client truyền
amount = -50hoặc lớn hơn số dư, hàm sẽ ném ra lỗi ngay lập tức. Client không tuân thủ hợp đồng thì Provider không có nghĩa vụ phải phục vụ.
Post-conditions (Trách nhiệm của Provider)
- Ý nghĩa: Nếu Client đã làm đúng, Provider phải giao đúng hàng.
- Hành động: Dù logic xử lý bên trong phức tạp đến đâu (kết nối database, gọi API bên thứ 3...), hàm phải đảm bảo kết quả cuối cùng:
Số dư mới = Số dư cũ - Số tiền rút. Nếu điều này sai, lỗi hoàn toàn thuộc về lập trình viên viết hàmwithdraw, giúp khoanh vùng bug cực nhanh.
Invariants (Cốt lõi của hệ thống)
- Ý nghĩa: Các quy luật vật lý của Domain.
- Hành động: Trong mọi khoảnh khắc (sau khi khởi tạo và sau khi public method chạy xong), số dư
balancephải>= 0. Giả sử sau này có một dev mới vào team viết thêm hàmapplyMonthlyFee(), họ có thể vô tình làm tài khoản bị âm. Nhờ hàmcheckInvariants()được gọi ở cuối mỗi thao tác, hệ thống sẽ báo lỗi ngay trước khi dữ liệu sai được lưu xuống database.
Tổng kết
Lập trình viên thông thường thường gộp chung validation, logic và xử lý lỗi thành một mớ hỗn độn (Spaghetti code).
Người có tư duy Contract sẽ phân tách rất rõ: "Lỗi này do dữ liệu đầu vào (Pre)?" hay "Lỗi này do tôi xử lý sai (Post)?" và viết code tự bảo vệ (Defensive Programming) ở mức độ cao nhất.
All rights reserved