[C++ OOP Thực Chiến] Bài 5: Kế thừa (Inheritance) - Đừng copy-paste code nữa, hãy dùng não!
Chào anh em! Chúng ta lại gặp nhau trong series OOP Thực chiến.
Ở [Bài 4], chúng ta đã thiết kế một cái DigitalWallet (Ví điện tử) cực kỳ an toàn với tính Đóng gói (Encapsulation). Hệ thống đang chạy rất ngon thì sếp giao task mới: "Em làm thêm cho anh cái Ví VIP nhé, tính năng y hệt Ví thường nhưng khi nạp tiền thì được hoàn lại (cashback) 5% giá trị!"
Cách giải quyết của một "thợ code" lười biếng: Tạo một class VipWallet, copy y nguyên 100 dòng code từ DigitalWallet sang, rồi sửa lại mỗi cái hàm deposit.
Hậu quả? Hôm sau sếp bảo đổi logic hàm rút tiền withdraw, bạn sẽ phải đi sửa ở cả 2 nơi. Trăm cái ví thì sửa 100 nơi. Chúc mừng, bạn vừa tạo ra một mớ code rác (Technical Debt)!
Để là một Kỹ sư phần mềm thực thụ, bạn phải biết tuân thủ nguyên tắc DRY (Don't Repeat Yourself). Và vũ khí ở đây chính là Kế thừa (Inheritance).
1. Kế thừa (Inheritance) là gì?
Hiểu đơn giản, Kế thừa cho phép một Class mới (Lớp con - Derived Class) "hưởng sái" toàn bộ tài sản (Thuộc tính và Phương thức) của một Class đã có sẵn (Lớp cha - Base Class).
Lớp con có thể dùng lại code của Lớp cha mà không cần viết lại, đồng thời có thể Mở rộng (Extend) thêm những tính năng của riêng nó. Mối quan hệ này trong thiết kế hệ thống được gọi là quan hệ "IS-A" (Ví VIP là một loại Ví điện tử).
2. Bí mật mang tên "Protected"
Ở bài Đóng gói, mình đã nói "Mọi thuộc tính đều phải là private". Nhưng private thì ích kỷ quá, lớp con kế thừa xong cũng không được phép đụng vào dữ liệu của lớp cha luôn!
Vậy nên C++ đẻ ra một Access Modifier thứ 3: protected (Được bảo vệ).
- Đối với người ngoài (Hàm main, class khác):
protectedhoạt động y hệtprivate(Cấm đụng vào). - Đối với Lớp con (Kế thừa):
protectedmở cửa cho lớp con được phép truy cập và sửa đổi. Nó giống như tài sản gia truyền, người ngoài không được xài nhưng con cháu trong nhà thì được.
3. Code Demo: Nâng cấp hệ thống Ví điện tử
Hãy xem cách chúng ta định nghĩa BaseWallet và kế thừa nó ra VipWallet gọn gàng như thế nào:
#include <iostream>
#include <string>
using namespace std;
// 1. LỚP CHA (Base Class): Cung cấp các tính năng dùng chung
class BaseWallet {
protected:
// Dùng protected thay vì private để lớp con (VipWallet) có thể thao tác
string accountId;
double balance;
public:
BaseWallet(string id) {
accountId = id;
balance = 0.0;
}
// Các hàm này Lớp con sẽ được hưởng xài miễn phí, KHÔNG CẦN VIẾT LẠI
double getBalance() const { return balance; }
void withdraw(double amount) {
if (amount > 0 && balance >= amount) {
balance -= amount;
cout << "[RUT TIEN] " << accountId << " rut $" << amount
<< ". So du: $" << balance << "\n";
}
}
// Hàm nạp tiền cơ bản của ví thường
void deposit(double amount) {
if (amount > 0) {
balance += amount;
cout << "[NAP TIEN] " << accountId << " nap $" << amount
<< ". So du: $" << balance << "\n";
}
}
};
// 2. LỚP CON (Derived Class): Kế thừa BaseWallet bằng từ khóa ": public"
class VipWallet : public BaseWallet {
public:
// Gọi constructor của Lớp cha để khởi tạo dữ liệu
VipWallet(string id) : BaseWallet(id) {}
// Ghi đè (Override) lại logic nạp tiền cho riêng Ví VIP
void deposit(double amount) {
if (amount > 0) {
double cashback = amount * 0.05; // Hoàn tiền 5%
balance += (amount + cashback); // Truy cập được balance vì nó là protected
cout << "[VIP NAP TIEN] " << accountId << " nap $" << amount
<< " (Thuong $" << cashback << "). So du: $" << balance << "\n";
}
}
// Mở rộng thêm tính năng mới mà ví thường không có
void showVipPrivilege() {
cout << "[VIP INFO] Ban duoc mien phi phi duy tri hang thang!\n";
}
};
int main() {
cout << "--- TEST VI THUONG ---\n";
BaseWallet normalWallet("USER_NOOB");
normalWallet.deposit(100); // Gọi deposit của cha
normalWallet.withdraw(20); // Gọi withdraw của cha
cout << "\n--- TEST VI VIP ---\n";
VipWallet myVipWallet("USER_PRO");
myVipWallet.deposit(100); // Gọi deposit ĐÃ ĐƯỢC GHI ĐÈ của con (có cashback)
myVipWallet.withdraw(20); // KHÔNG CẦN VIẾT HÀM NÀY, tái sử dụng withdraw của cha!
myVipWallet.showVipPrivilege(); // Tính năng riêng của VIP
return 0;
}
Nhận xét: Thay vì copy hàng trăm dòng code, VipWallet thực chất chỉ cần vài dòng để khởi tạo, tùy chỉnh lại hàm deposit và thêm tính năng riêng. Code cực kỳ "sạch" và xịn xò. Nếu sau này cần sửa logic hàm withdraw, bạn chỉ sửa ĐÚNG 1 LẦN ở BaseWallet, tất cả các ví con sẽ tự động được cập nhật!
Tạm kết & Gợi mở
Tuyệt vời! Bạn đã biết cách dùng Kế thừa để tái sử dụng code, tiết kiệm hàng chục giờ gõ phím vô nghĩa.
Tuy nhiên, trong code demo ở trên, hàm deposit của VipWallet đang "che khuất" (shadowing) hàm deposit của lớp cha. Chuyện gì sẽ xảy ra nếu hệ thống của bạn có 1000 cái ví (gồm cả Ví Thường lẫn Ví VIP) được lưu chung vào một danh sách mảng, và bạn muốn dùng một vòng lặp chạy qua tất cả để gọi lệnh napTien()?
Làm sao máy tính biết lúc nào thì nên gọi hàm nạp của Ví Thường, lúc nào thì gọi hàm nạp có cashback của Ví VIP ngay tại thời điểm đoạn code đang chạy (Runtime)?
Đây chính là giới hạn cuối cùng mà chúng ta phải vượt qua để thành thạo hoàn toàn OOP. Câu trả lời nằm ở "phép thuật" mang tên Đa hình. Hẹn gặp lại anh em ở Bài 6: Đa hình (Polymorphism) là gì? - Khái niệm định hình đẳng cấp Senior. Nhớ Follow và Upvote series nhé!
All rights reserved