Bài 36.4: Ứng dụng kế thừa trong bài toán thực tế trong C++

Bài 36.4: Ứng dụng kế thừa trong bài toán thực tế trong C++
Chào mừng bạn đến với bài viết tiếp theo trong chuỗi seri về C++! Hôm nay, chúng ta sẽ khám phá một trong những trụ cột quan trọng nhất của Lập trình hướng đối tượng (OOP) trong C++: kế thừa (Inheritance). Không chỉ dừng lại ở lý thuyết, chúng ta sẽ cùng nhau tìm hiểu cách ứng dụng thực tế kỹ thuật mạnh mẽ này để giải quyết các bài toán trong thế giới thực, giúp code của bạn mạch lạc hơn, dễ bảo trì hơn, và có khả năng mở rộng cao hơn.
Tại sao Kế Thừa Quan Trọng Trong Thế Giới Thực?
Hãy nghĩ về thế giới xung quanh chúng ta. Mọi vật thể, khái niệm thường được tổ chức theo hệ thống phân cấp. Một chiếc ô tô là một loại phương tiện. Một con chó là một loại động vật. Một nhân viên trả lương hàng tháng là một loại nhân viên. Mối quan hệ "là một loại" (is-a relationship) này chính là bản chất của kế thừa.
Trong lập trình, kế thừa cho phép bạn tạo ra một lớp mới (gọi là lớp con hay lớp dẫn xuất - derived class) dựa trên một lớp đã có (gọi là lớp cơ sở - base class). Lớp con thừa hưởng tất cả các thuộc tính (thành viên dữ liệu) và hành vi (phương thức) của lớp cơ sở, đồng thời có thể thêm vào các thuộc tính/phương thức riêng hoặc thay đổi (ghi đè - override) các hành vi được kế thừa.
Lợi ích khi áp dụng kế thừa trong các bài toán thực tế là vô cùng to lớn:
- Tái sử dụng Code: Các đặc điểm và hành vi chung được định nghĩa ở lớp cơ sở, tránh việc lặp đi lặp lại code ở nhiều lớp khác nhau. Viết một lần, dùng nhiều nơi!
- Dễ Bảo trì: Khi cần sửa lỗi hoặc cập nhật một logic chung, bạn chỉ cần thay đổi ở lớp cơ sở, các lớp con sẽ tự động hưởng lợi.
- Dễ Mở rộng: Khi có một loại đối tượng mới xuất hiện với các đặc điểm gần giống loại đã có, bạn chỉ cần tạo một lớp con mới kế thừa từ lớp cơ sở chung, thêm vào các đặc điểm riêng biệt là xong.
- Mô hình hóa Thế giới Thực: Giúp cấu trúc chương trình phản ánh một cách tự nhiên các mối quan hệ phân cấp trong thực tế.
- Hỗ trợ Đa hình (Polymorphism): Đây là sức mạnh thực sự khi kết hợp với kế thừa. Nó cho phép bạn coi các đối tượng của các lớp con khác nhau như thể chúng là đối tượng của lớp cơ sở, và gọi các phương thức chung mà không cần biết chính xác đối tượng đó thuộc loại nào ở thời điểm biên dịch. Chương trình sẽ tự động gọi đúng phương thức của lớp con tương ứng khi chạy.
Bây giờ, chúng ta hãy cùng nhau đi vào các ví dụ minh họa cụ thể để thấy rõ kế thừa được ứng dụng như thế nào trong C++ nhé!
Ví dụ 1: Hệ thống Quản lý Hình học (Geometric Shapes)
Bài toán: Bạn cần viết một chương trình xử lý các hình học khác nhau như hình tròn, hình chữ nhật, tam giác... và tính diện tích của chúng. Các hình đều có chung khái niệm "diện tích", nhưng cách tính diện tích lại khác nhau.
Sử dụng kế thừa, chúng ta có thể tạo ra một lớp cơ sở trừu tượng là Shape
, định nghĩa phương thức tính diện tích chung là calculateArea()
. Các lớp cụ thể như Circle
và Rectangle
sẽ kế thừa từ Shape
và triển khai phương thức calculateArea()
theo công thức riêng của mình.
#include <iostream>
#include <vector>
#include <cmath>
#include <memory>
#define _USE_MATH_DEFINES // Can thiet cho M_PI tren mot so he thong
using namespace std;
// Lop co so Hinh
class Hinh {
public:
virtual double tinhDt() const = 0;
virtual ~Hinh() = default;
};
// Lop Tron ke thua Hinh
class Tron : public Hinh {
private:
double r;
public:
Tron(double r0) : r(r0) {}
double tinhDt() const override {
return M_PI * r * r;
}
};
// Lop ChuNhat ke thua Hinh
class CN : public Hinh {
private:
double w, h;
public:
CN(double w0, double h0) : w(w0), h(h0) {}
double tinhDt() const override {
return w * h;
}
};
int main() {
vector<unique_ptr<Hinh>> hinhs;
hinhs.push_back(make_unique<Tron>(5.0));
hinhs.push_back(make_unique<CN>(4.0, 6.0));
hinhs.push_back(make_unique<Tron>(2.5));
cout << "--- Tinh dien tich cac hinh ---" << endl;
for (const auto& h : hinhs) {
cout << "Dien tich: " << h->tinhDt() << endl;
}
cout << "------------------------------" << endl;
return 0;
}
Output:
--- Tinh dien tich cac hinh ---
Dien tich: 78.5398
Dien tich: 24.0000
Dien tich: 19.6350
------------------------------
Giải thích:
- Lớp
Shape
là lớp cơ sở, đóng vai trò là giao diện chung cho tất cả các loại hình. Phương thứccalculateArea()
được khai báo làvirtual = 0
, biếnShape
thành một lớp trừu tượng. Điều này có nghĩa là bạn không thể tạo trực tiếp một đối tượngShape
, mà chỉ có thể tạo các đối tượng của các lớp con cụ thể đã triển khaicalculateArea()
. - Các lớp
Circle
vàRectangle
kế thừa từShape
bằng cách sử dụngpublic Shape
. Điều này có nghĩa là tất cả các thành viênpublic
củaShape
vẫn làpublic
trongCircle
vàRectangle
, và các thành viênprotected
củaShape
vẫn làprotected
trong lớp con. - Mỗi lớp con bắt buộc phải ghi đè (override) phương thức
calculateArea()
để cung cấp logic tính diện tích riêng. Từ khóaoverride
là tùy chọn nhưng rất nên dùng vì nó giúp trình biên dịch kiểm tra xem bạn có thực sự ghi đè một phương thức ảo ở lớp cơ sở hay không. - Trong
main()
, chúng ta tạo mộtvector
lưu trữ các con trỏ thông minh (unique_ptr
) tớiShape
. Đây là điểm mấu chốt cho đa hình. Mặc dù vector chỉ "biết" đó là các con trỏShape
, khi chúng ta gọishape->calculateArea()
, C++ sẽ nhìn vào loại đối tượng thực tế được con trỏ đó trỏ tới (mộtCircle
hay mộtRectangle
) và thực thi đúng phương thứccalculateArea()
của lớp con đó. Điều này giúp code xử lý các hình trở nên rất linh hoạt và dễ mở rộng.
Ví dụ 2: Hệ thống Quản lý Phương tiện Giao thông (Vehicle Hierarchy)
Bài toán: Cần xây dựng một hệ thống quản lý các loại phương tiện giao thông như ô tô, xe máy, xe tải. Chúng đều có những hành động chung như khởi động/tắt máy, nhưng cũng có những hành động riêng biệt.
Kế thừa cho phép chúng ta tạo lớp cơ sở Vehicle
với các hành động chung, và các lớp con Car
, Motorcycle
, Truck
kế thừa Vehicle
, thêm vào các hành động đặc trưng.
#include <iostream>
#include <vector>
#include <string>
#include <memory>
using namespace std;
// Lop co so Xe
class Xe {
protected:
string ten;
public:
Xe(const string& t = "Vo danh") : ten(t) {}
virtual void khoiDong() const {
cout << ten << ": Khoi dong dong co chung." << endl;
}
virtual void tatMay() const {
cout << ten << ": Tat dong co chung." << endl;
}
virtual ~Xe() = default;
};
// Lop OTo ke thua Xe
class OTo : public Xe {
private:
int soCua;
public:
OTo(const string& t, int c) : Xe(t), soCua(c) {}
void khoiDong() const override {
cout << ten << ": Khoi dong xe hoi (" << soCua << " cua)." << endl;
}
void moCop() const {
cout << ten << ": Mo cop xe." << endl;
}
};
// Lop XeMay ke thua Xe
class XeMay : public Xe {
private:
bool coThung;
public:
XeMay(const string& t, bool thung) : Xe(t), coThung(thung) {}
void khoiDong() const override {
cout << ten << ": Khoi dong xe may (tieng po lon hon)." << endl;
}
void bayDau() const {
cout << ten << ": Bay dau xe!" << endl;
}
};
int main() {
vector<unique_ptr<Xe>> gara;
gara.push_back(make_unique<OTo>("Camry", 4));
gara.push_back(make_unique<XeMay>("CBR", false));
gara.push_back(make_unique<OTo>("Civic", 5));
cout << "--- Khoi dong tat ca xe ---" << endl;
for (const auto& x : gara) {
x->khoiDong();
}
cout << "---------------------------" << endl;
cout << "\n--- Thuc hien hanh dong rieng biet ---" << endl;
for (const auto& x : gara) {
OTo* oto = dynamic_cast<OTo*>(x.get());
if (oto) {
oto->moCop();
}
XeMay* xm = dynamic_cast<XeMay*>(x.get());
if (xm) {
xm->bayDau();
}
}
cout << "------------------------------------" << endl;
cout << "\n--- Tat dong co tat ca xe ---" << endl;
for (const auto& x : gara) {
x->tatMay();
}
cout << "-----------------------------" << endl;
return 0;
}
Output:
--- Khoi dong tat ca xe ---
Camry: Khoi dong xe hoi (4 cua).
CBR: Khoi dong xe may (tieng po lon hon).
Civic: Khoi dong xe hoi (5 cua).
---------------------------
--- Thuc hien hanh dong rieng biet ---
Camry: Mo cop xe.
CBR: Bay dau xe!
Civic: Mo cop xe.
------------------------------------
--- Tat dong co tat ca xe ---
Camry: Tat dong co chung.
CBR: Tat dong co chung.
Civic: Tat dong co chung.
-----------------------------
Giải thích:
- Lớp
Vehicle
là lớp cơ sở chứa các hành động chung (startEngine
,stopEngine
) và một thuộc tính chung (model
). Các phương thức này được đánh dấu làvirtual
để các lớp con có thể chọn ghi đè chúng nếu cần. - Các lớp
Car
vàMotorcycle
kế thừa từVehicle
và thêm vào các thuộc tính/phương thức riêng (numberOfDoors
,openTrunk
choCar
;hasSidecar
,wheelie
choMotorcycle
). - Chúng ta có thể tạo một danh sách (
vector
) chứa các con trỏ tớiVehicle
. Khi gọiv->startEngine()
, hành động đặc trưng của từng loại xe sẽ được thực hiện nhờ tính năng đa hình. - Đối với các hành động riêng biệt của từng loại xe (như
openTrunk
haywheelie
), chúng ta không thể gọi trực tiếp qua con trỏVehicle*
. Chúng ta cần biết chính xác đối tượng đó là loại gì để gọi phương thức riêng của nó. Kỹ thuậtdynamic_cast
được sử dụng để kiểm tra xem con trỏ lớp cơ sở thực sự trỏ tới đối tượng của lớp con mong muốn hay không. Nếu thành công,dynamic_cast
trả về con trỏ tới lớp con, nếu không sẽ trả vềnullptr
.
Ví dụ 3: Hệ thống Quản lý Nhân viên (Employee Management System)
Bài toán: Một công ty có nhiều loại nhân viên: nhân viên ăn lương cố định hàng tháng, nhân viên ăn lương theo giờ. Cần tính lương cho họ.
Đây là một ví dụ kinh điển cho thấy kế thừa và đa hình giúp quản lý sự đa dạng như thế nào. Lớp cơ sở Employee
sẽ chứa thông tin chung (ID, tên) và một phương thức tính lương calculatePay()
. Các lớp con SalariedEmployee
và HourlyEmployee
sẽ kế thừa Employee
và cung cấp cách tính lương riêng của họ.
#include <iostream>
#include <vector>
#include <string>
#include <memory>
#include <iomanip>
using namespace std;
// Lop co so NhanVien
class NV {
protected:
int ma;
string ten;
public:
NV(int m0, const string& t0) : ma(m0), ten(t0) {}
int layMa() const { return ma; }
string layTen() const { return ten; }
virtual double tinhLuong() const = 0;
virtual ~NV() = default;
};
// Lop NhanVienLuongCoDinh ke thua NV
class NVLuongCoDinh : public NV {
private:
double luongNam;
public:
NVLuongCoDinh(int m0, const string& t0, double ln)
: NV(m0, t0), luongNam(ln) {}
double tinhLuong() const override {
return luongNam / 12.0;
}
};
// Lop NhanVienTheoGio ke thua NV
class NVTheoGio : public NV {
private:
double giaGio;
int gioLam;
public:
NVTheoGio(int m0, const string& t0, double gg, int gl)
: NV(m0, t0), giaGio(gg), gioLam(gl) {}
double tinhLuong() const override {
return giaGio * gioLam;
}
void capNhatGioLam(int gl) {
if (gl >= 0) gioLam = gl;
else cerr << "Gio lam khong hop le!\n";
}
};
int main() {
vector<unique_ptr<NV>> nvien;
nvien.push_back(make_unique<NVLuongCoDinh>(101, "Nguyen A", 60000.0));
nvien.push_back(make_unique<NVTheoGio>(102, "Tran B", 15.0, 160));
nvien.push_back(make_unique<NVLuongCoDinh>(103, "Le C", 72000.0));
cout << fixed << setprecision(2);
cout << "--- Bang luong thang nay ---" << endl;
for (const auto& e : nvien) {
cout << "ID: " << e->layMa()
<< ", Ten: " << e->layTen()
<< ", Luong: " << e->tinhLuong() << " $" << endl;
}
cout << "---------------------------" << endl;
cout << "\n--- Cap nhat du lieu rieng biet (vd: gio lam) ---" << endl;
for (const auto& e : nvien) {
NVTheoGio* nvg = dynamic_cast<NVTheoGio*>(e.get());
if (nvg) {
cout << "Cap nhat gio lam cho " << nvg->layTen() << endl;
nvg->capNhatGioLam(185);
cout << "Luong thang sau cap nhat gio lam: " << nvg->tinhLuong() << " $" << endl;
}
}
cout << "-----------------------------------------------" << endl;
return 0;
}
Output:
--- Bang luong thang nay ---
ID: 101, Ten: Nguyen A, Luong: 5000.00 $
ID: 102, Ten: Tran B, Luong: 2400.00 $
ID: 103, Ten: Le C, Luong: 6000.00 $
---------------------------
--- Cap nhat du lieu rieng biet (vd: gio lam) ---
Cap nhat gio lam cho Tran B
Luong thang sau cap nhat gio lam: 2775.00 $
-----------------------------------------------
Giải thích:
Employee
là lớp cơ sở trừu tượng (vìcalculatePay
là thuần túy ảo), chứa các thông tin và hành vi chung cho mọi nhân viên.SalariedEmployee
vàHourlyEmployee
kế thừa từEmployee
, mỗi lớp bổ sung các thuộc tính đặc trưng (lương năm, mức lương theo giờ, số giờ làm).- Phương thức
calculatePay()
được ghi đè trong mỗi lớp con để phản ánh đúng công thức tính lương của từng loại. - Trong
main
, chúng ta có thể quản lý tất cả nhân viên trong một danh sách chung (vector<unique_ptr<Employee>>
). Khi cần tính lương, chỉ cần gọicalculatePay()
thông qua con trỏEmployee*
, và hệ thống đa hình của C++ sẽ tự động gọi đúng phiên bảncalculatePay()
của lớp con (SalariedEmployee::calculatePay
hoặcHourlyEmployee::calculatePay
). - Tương tự ví dụ 2, để truy cập hoặc thay đổi các thuộc tính/phương thức riêng của lớp con (ví dụ:
setHoursWorked
củaHourlyEmployee
), bạn sẽ cần sử dụngdynamic_cast
để xác định loại đối tượng thực tế.
Khi nào nên và không nên dùng Kế thừa?
- Nên dùng khi có mối quan hệ "là một loại" (is-a). Ví dụ:
Dog IS-A Animal
,Car IS-A Vehicle
. Kế thừa giúp mô hình hóa cấu trúc phân cấp tự nhiên này. - Nên dùng khi bạn muốn tái sử dụng code và mở rộng hệ thống bằng cách thêm các loại mới dựa trên loại đã có.
- Không nên dùng khi chỉ có mối quan hệ "có một" (has-a). Ví dụ: Một chiếc xe hơi có một động cơ (A Car HAS-A Engine). Mối quan hệ này nên được mô hình hóa bằng cách sử dụng thành phần (Composition), tức là một lớp chứa một đối tượng của lớp khác làm thành viên của nó. Lạm dụng kế thừa cho mối quan hệ "has-a" có thể dẫn đến thiết kế cứng nhắc và khó bảo trì (được gọi là "địa ngục kế thừa" - inheritance hierarchy from hell).
- Cân nhắc kỹ khi lớp con chỉ đơn thuần thêm chức năng nhỏ mà không có mối quan hệ "is-a" rõ ràng, hoặc khi việc kế thừa dẫn đến việc lớp con phải "thừa hưởng" rất nhiều thứ không cần thiết từ lớp cha (Fragile Base Class Problem).
Comments