[C++ OOP Thực Chiến] Bài 6: Đa hình (Polymorphism) - "Phép thuật" định hình đẳng cấp Senior
Chào anh em! Chúc mừng anh em đã đi đến mảnh ghép cuối cùng và cũng là mảnh ghép uy lực nhất của 4 tính chất OOP: Đa hình (Polymorphism).
Hãy nhớ lại câu hỏi ở [Bài 5]: Giả sử hệ thống của bạn có một mảng chứa 1000 cái ví điện tử, bao gồm lộn xộn cả Ví Thường (BaseWallet) lẫn Ví VIP (VipWallet). Cuối tháng, sếp yêu cầu chạy một vòng lặp nạp 100$ tiền thưởng cho toàn bộ user.
Làm sao máy tính biết được cái ví nào đang nằm trong mảng để gọi đúng hàm deposit() (Ví thường thì nạp nguyên 100$, Ví VIP thì phải nạp 100$ + 5% hoàn tiền)? Đa hình sinh ra để làm chuyện đó!
1. Đa hình (Polymorphism) là gì?
Từ "Polymorphism" xuất phát từ tiếng Hy Lạp, nghĩa là "Nhiều hình thái".
Trong lập trình, Đa hình hiểu đơn giản là: Cùng một lời gọi hàm, nhưng các Object khác nhau sẽ có cách phản hồi (thực thi) khác nhau. Giống như khi sếp hô to: "Làm việc đi!".
- Anh Dev sẽ mở VS Code lên gõ phím.
- Chị Tester sẽ mở tool lên test bug.
- Cô HR sẽ đi lọc CV.
Cùng một mệnh lệnh "Làm việc", nhưng mỗi người (mỗi class) tự biết cách làm công việc của riêng mình.
2. Vũ khí tối thượng: Từ khóa virtual và Con trỏ Lớp Cha
Trong C++, nếu bạn lưu các Object con (VipWallet) vào một danh sách của Lớp cha (BaseWallet), mặc định trình biên dịch sẽ rất "ngu". Nó chỉ nhìn vào cái vỏ bên ngoài (kiểu dữ liệu của danh sách) và gọi thẳng hàm của Lớp Cha (gọi là Early Binding - Liên kết sớm lúc compile).
Để kích hoạt Đa hình, bạn phải làm 2 việc:
1.Thêm từ khóa virtual vào trước hàm của Lớp cha. Lời nhắn nhủ ở đây là: "Ê C++, hàm này có thể bị ghi đè đấy. Lúc chạy hãy kiểm tra vùng nhớ THẬT sự xem nó là đối tượng gì rồi hẵng gọi hàm nhé!" (Đây gọi là Late Binding - Liên kết trễ lúc runtime)
2. Sử dụng Con trỏ (Pointer *) hoặc Tham chiếu (Reference &) của Lớp cha để quản lý Lớp con.
3. Code Demo: Giải quyết bài toán 1000 cái ví
Hãy xem cách chúng ta vận hành mảng 1000 cái ví cực kỳ thanh lịch bằng std::vector (đã ôn ở Bài 1) kết hợp với Đa hình:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// 1. LỚP CHA
class BaseWallet {
protected:
string accountId;
double balance;
public:
BaseWallet(string id) : accountId(id), balance(0) {}
// TỪ KHÓA VIRTUAL: Kích hoạt phép thuật Đa hình
virtual void deposit(double amount) {
balance += amount;
cout << "[Base] " << accountId << " nap $" << amount
<< " | So du: $" << balance << "\n";
}
// LƯU Ý SỐ 1 KHI PHỎNG VẤN: Đã dùng virtual function thì Destructor cũng PHẢI là virtual!
virtual ~BaseWallet() {
// cout << "Huy BaseWallet\n";
}
};
// 2. LỚP CON
class VipWallet : public BaseWallet {
public:
VipWallet(string id) : BaseWallet(id) {}
// GHI ĐÈ (Override) hàm của Lớp cha
void deposit(double amount) override { // 'override' giúp C++ check lỗi gõ sai tên hàm
double cashback = amount * 0.05;
balance += (amount + cashback);
cout << "[VIP] " << accountId << " nap $" << amount
<< " (+hoan $" << cashback << ") | So du: $" << balance << "\n";
}
};
int main() {
// Tạo một vector chứa CON TRỞ Lớp Cha.
// Nhờ vậy, nó có thể trỏ tới cả đối tượng Cha lẫn đối tượng Con!
vector<BaseWallet*> userWallets;
// Push lẫn lộn các loại ví vào hệ thống
userWallets.push_back(new BaseWallet("USER_THUONG_01"));
userWallets.push_back(new VipWallet("USER_VIP_01"));
userWallets.push_back(new VipWallet("USER_VIP_02"));
userWallets.push_back(new BaseWallet("USER_THUONG_02"));
cout << "--- HE THONG BAT DAU THUONG TIEN ---\n";
// Chỉ với 1 VÒNG LẶP DUY NHẤT, gọi 1 HÀM DUY NHẤT
for (BaseWallet* wallet : userWallets) {
// PHÉP THUẬT RUNTIME: C++ tự biết wallet nào là VIP để gọi đúng hàm!
wallet->deposit(100);
}
// Dọn dẹp Memory (Tránh Memory Leak)
for (BaseWallet* wallet : userWallets) {
delete wallet;
}
userWallets.clear();
return 0;
}
Kết quả khi chạy:
Dù lệnh gọi trông giống hệt nhau là wallet->deposit(100);, nhưng ví USER_THUONG sẽ được cộng 100$, còn ví USER_VIP sẽ được cộng 105$. Hệ thống của bạn giờ đây linh hoạt đến mức, sau này có thêm SuperVipWallet, bạn chỉ việc tạo Class mới, code ở hàm main (vòng lặp) không cần thay đổi một chữ nào!
Tạm kết & Gợi mở
Vậy là chúng ta đã chinh phục xong 4 trụ cột của OOP: Trừu tượng - Đóng gói - Kế thừa - Đa hình.
Tuy nhiên, từ đầu series đến giờ, để tiện cho việc giảng giải, mình luôn gom tất cả các class (BaseWallet, VipWallet...) và hàm main() vào chung một file main.cpp. Nếu bạn mang phong cách code "bún xào" này vào các dự án thực tế với hàng trăm nghìn dòng code, file của bạn sẽ nặng hàng MB, lúc compile thì chậm như rùa bò, và team của bạn sẽ không thể chia nhau ra làm việc được (vì đụng git conflict liên tục).
Đã đến lúc chúng ta tạm gác lại lý thuyết để học cách làm việc như một kỹ sư chuyên nghiệp. Hẹn gặp lại các bạn ở Bài 7: Khai báo Class trong C++ - Đừng nhét tất cả vào một file, hãy chia Header (.h) và Source (.cpp)!
Nhớ Upvote và Share series để ủng hộ mình ra bài mới nhé!
All Rights Reserved