[C++ OOP Thực Chiến] Bài 3: Trừu tượng (Abstraction) là gì? - Nghệ thuật thiết kế API "chống ngu"
Chào các bạn, chúng ta lại gặp nhau trong series OOP Thực chiến!
Ở Bài 2, chúng ta đã nhắc đến việc che giấu những chi tiết phức tạp của hệ thống để người dùng (hoặc các developer khác trong team) dễ dàng sử dụng. Hôm nay, chúng ta sẽ đi sâu vào khái niệm đầu tiên trong 4 tính chất cốt lõi của OOP: Trừu tượng (Abstraction).
Nhiều người đi làm vài năm vẫn nhầm lẫn giữa Trừu tượng (Abstraction) và Đóng gói (Encapsulation). Bài này mình sẽ giúp các bạn dứt điểm sự nhầm lẫn đó.
1. Trừu tượng (Abstraction) dưới góc nhìn Backend Engineer
Hãy tưởng tượng bạn đang đi xe máy. Để xe chạy, bạn chỉ cần nổ máy, vặn ga và đạp số. Bạn không cần biết bên trong lốc máy có bao nhiêu xilanh, bugi đánh lửa lúc nào, hay xăng được phun vào buồng đốt ra sao.
Những chi tiết máy móc phức tạp đã được che giấu đi (ẩn giấu độ phức tạp), và nhà sản xuất chỉ cung cấp cho bạn một cái Giao diện (Interface) cực kỳ đơn giản: Nút đề, tay ga, cần số. Đó chính là Trừu tượng!
Trong lập trình cũng vậy. Khi bạn thiết kế một Class hay một Module, Trừu tượng có nghĩa là bạn chỉ "phơi bày" ra những tính năng cốt lõi (What it does), và giấu đi toàn bộ logic phức tạp bên trong (How it does it). Nó giống y hệt cách chúng ta gọi một API: Bạn chỉ cần truyền Request và nhận Response, không cần quan tâm server của họ code bằng ngôn ngữ gì hay chạy database nào.
2. Abstraction trong C++: Abstract Class và Interface
Trong C++, Trừu tượng thường được thể hiện rõ nhất qua Abstract Class (Lớp trừu tượng) bằng cách sử dụng Hàm ảo thuần túy (Pure Virtual Function).
Giả sử bạn đang làm một hệ thống Backend xử lý thanh toán (Payment). Khách hàng có thể thanh toán bằng MoMo, VNPay, hoặc ZaloPay. Nếu không dùng tính trừu tượng, bạn sẽ phải viết hàng đống lệnh if-else lằng nhằng ở mọi nơi.
Hãy xem cách Abstraction giải cứu hệ thống của bạn:
#include <iostream>
#include <string>
using namespace std;
// 1. GIAO DIỆN TRỪU TƯỢNG (Interface)
// Bản thân nó không có code logic, chỉ định nghĩa "Các tính năng cần có"
class PaymentGateway {
public:
// Pure Virtual Function (= 0) ép các class con phải tự triển khai logic
virtual void processPayment(double amount) = 0;
virtual ~PaymentGateway() {} // Virtual destructor cho an toàn bộ nhớ
};
// 2. CÁC IMPLEMENTATION (Triển khai chi tiết - ẩn giấu độ phức tạp)
class MomoPayment : public PaymentGateway {
private:
void connectToMomoAPI() { cout << "[Log] Dang ket noi server MoMo...\n"; }
void checkWalletBalance() { cout << "[Log] Kiem tra so du vi MoMo...\n"; }
public:
void processPayment(double amount) override {
connectToMomoAPI();
checkWalletBalance();
cout << "=> [SUCCESS] Thanh toan $" << amount << " qua MoMo thanh cong!\n\n";
}
};
class VNPayPayment : public PaymentGateway {
private:
void encryptDataWithHash() { cout << "[Log] Dang ma hoa du lieu chuan VNPay...\n"; }
public:
void processPayment(double amount) override {
encryptDataWithHash();
cout << "=> [SUCCESS] Thanh toan $" << amount << " qua VNPay thanh cong!\n\n";
}
};
// 3. NGƯỜI SỬ DỤNG (Client Code) - Hoàn toàn trừu tượng!
void checkoutProcess(PaymentGateway* gateway, double amount) {
cout << "--- Bat dau quy trinh thanh toan ---\n";
// Client CHỈ GỌI hàm processPayment, KHÔNG CẦN BIẾT bên trong nó làm gì
gateway->processPayment(amount);
}
int main() {
// Khởi tạo các cổng thanh toán
PaymentGateway* myMomo = new MomoPayment();
PaymentGateway* myVNPay = new VNPayPayment();
// Thanh toán đơn hàng 100$ bằng MoMo
checkoutProcess(myMomo, 100.5);
// Thanh toán đơn hàng 250$ bằng VNPay
checkoutProcess(myVNPay, 250.0);
// Dọn dẹp RAM (Luôn nhớ khi dùng raw pointer nhé)
delete myMomo;
delete myVNPay;
return 0;
}
Phân tích sự bá đạo của code trên:
Hàm checkoutProcess hoàn toàn mù tịt về cách MoMo hay VNPay hoạt động. Nó chỉ nhận vào một cái Interface là PaymentGateway và tin tưởng rằng hàm processPayment sẽ làm đúng nhiệm vụ. Sau này sếp yêu cầu tích hợp thêm ZaloPay, bạn chỉ cần tạo Class ZaloPayment, hệ thống cốt lõi checkoutProcess không cần sửa một dòng code nào!
Tạm kết & Gợi mở
Trừu tượng (Abstraction) giải quyết bài toán về Thiết kế (Design Level) - Giúp giảm bớt sự phức tạp của hệ thống bằng cách giấu đi phần thực thi (implementation), chỉ cung cấp giao diện (interface).
Nhưng hãy quay lại ví dụ chiếc vé xem phim ở bài trước. Giả sử hệ thống của bạn đã thiết kế giao diện rất đẹp, rất trừu tượng. Nhưng một ông dev mới vào nghề nào đó lỡ tay truy cập thẳng vào biến lưu trữ dữ liệu và viết: ticket.price = -500; (Vé âm tiền?). Hệ thống lại toang lần 2!
Trừu tượng hóa đã che giấu sự phức tạp, nhưng để bảo vệ dữ liệu khỏi những thao tác sai trái từ bên ngoài, chúng ta cần một lớp khiên bảo vệ vững chắc hơn.
Đó là lúc chúng ta phải triệu hồi tính chất thứ hai của OOP. Hẹn gặp lại anh em ở Bài 4: Đóng gói (Encapsulation) là gì? - Đặt "lính gác" cho dữ liệu của bạn. Đừng quên thả Upvote để ủng hộ series nhé!
All rights reserved