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:

  1. 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!
  2. 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.
  3. 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.
  4. 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ế.
  5. 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ư CircleRectangle 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> // De luu tru danh sach cac hinh
#include <cmath>  // De su dung M_PI cho hinh tron
#include <memory> // De su dung unique_ptr cho quan ly bo nho

// Lớp cơ sở Shape - Đại diện cho khái niệm chung về hình học
// Đây là một lớp trừu tượng vì có phương thức thuần túy ảo
class Shape {
public:
    // Phương thức ảo thuần túy (pure virtual function)
    // Bắt buộc các lớp con phải cung cấp định nghĩa của riêng mình.
    // Một lớp có phương thức ảo thuần túy là lớp trừu tượng và không thể tạo đối tượng trực tiếp.
    virtual double calculateArea() const = 0;

    // Phương thức ảo để hủy đối tượng đúng cách khi sử dụng đa hình
    virtual ~Shape() = default; // Sử dụng default để trình biên dịch tạo destructor mặc định
};

// Lớp Circle kế thừa công khai (public) từ Shape
class Circle : public Shape {
private:
    double radius; // Thuoc tinh rieng cua hinh tron
public:
    // Constructor
    Circle(double r) : radius(r) {
        if (radius < 0) radius = 0; // Dam bao ban kinh khong am
    }

    // Ghi đè (override) phương thức calculateArea() từ lớp cơ sở
    double calculateArea() const override {
        return M_PI * radius * radius; // Cong thuc tinh dien tich hinh tron
    }

    // Co the co cac phuong thuc rieng khac cua Circle, vi du getRadius()
    double getRadius() const { return radius; }
};

// Lớp Rectangle kế thừa công khai (public) từ Shape
class Rectangle : public Shape {
private:
    double width;  // Thuoc tinh rieng cua hinh chu nhat
    double height; // Thuoc tinh rieng cua hinh chu nhat
public:
    // Constructor
    Rectangle(double w, double h) : width(w), height(h) {
        if (width < 0) width = 0;
        if (height < 0) height = 0;
    }

    // Ghi đè (override) phương thức calculateArea() từ lớp cơ sở
    double calculateArea() const override {
        return width * height; // Cong thuc tinh dien tich hinh chu nhat
    }

    // Co the co cac phuong thuc rieng khac cua Rectangle
    double getWidth() const { return width; }
    double getHeight() const { return height; }
};

// --- Có thể thêm các lớp hình khác như Triangle, Square, v.v. một cách dễ dàng ---
/*
class Triangle : public Shape {
private:
    double base;
    double height;
public:
    Triangle(double b, double h) : base(b), height(h) {}
    double calculateArea() const override {
        return 0.5 * base * height;
    }
};
*/

int main() {
    // Sử dụng vector để lưu trữ các đối tượng Shape khác nhau
    // Chúng ta lưu trữ con trỏ thông minh (unique_ptr) tới Shape
    // Điều này cho phép chúng ta sử dụng tính năng đa hình
    vector<unique_ptr<Shape>> shapes;

    // Thêm các đối tượng của các lớp con vào vector lưu con trỏ lớp cơ sở
    shapes.push_back(make_unique<Circle>(5.0));        // Một hình tròn bán kính 5
    shapes.push_back(make_unique<Rectangle>(4.0, 6.0)); // Một hình chữ nhật 4x6
    shapes.push_back(make_unique<Circle>(2.5));        // Thêm một hình tròn khác

    // Duyệt qua danh sách các hình và tính diện tích cho từng hình
    // Đây là lúc đa hình phát huy tác dụng!
    cout << "--- Tinh dien tich cac hinh ---" << endl;
    for (const auto& shape : shapes) {
        // Chúng ta gọi phương thức calculateArea() thông qua con trỏ kiểu Shape*.
        // Tuy nhiên, nhờ có từ khóa 'virtual' và tính năng đa hình,
        // C++ sẽ tự động gọi đúng phương thức calculateArea() của lớp con tương ứng
        // (Circle::calculateArea() hoặc Rectangle::calculateArea()).
        cout << "Dien tich: " << shape->calculateArea() << endl;
    }
    cout << "------------------------------" << endl;

    // Khi vector shapes kết thúc phạm vi, các unique_ptr sẽ tự động giải phóng
    // bộ nhớ, gọi đúng destructor của lớp con nhờ virtual destructor trong Shape.

    return 0;
}

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ức calculateArea() được khai báo là virtual = 0, biến Shape 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ượng Shape, mà chỉ có thể tạo các đối tượng của các lớp con cụ thể đã triển khai calculateArea().
  • Các lớp CircleRectangle kế thừa từ Shape bằng cách sử dụng public Shape. Điều này có nghĩa là tất cả các thành viên public của Shape vẫn là public trong CircleRectangle, và các thành viên protected của Shape 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óa override 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ột vector lưu trữ các con trỏ thông minh (unique_ptr) tới Shape. Đâ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ọi shape->calculateArea(), C++ sẽ nhìn vào loại đối tượng thực tế được con trỏ đó trỏ tới (một Circle hay một Rectangle) và thực thi đúng phương thức calculateArea() 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ạtdễ 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>

// Lớp cơ sở Vehicle
class Vehicle {
protected:
    string model = "Unknown";
public:
    Vehicle(const string& m = "Unknown") : model(m) {}

    // Các phương thức chung, có thể là virtual để lớp con ghi đè
    virtual void startEngine() const {
        cout << model << ": Khoi dong dong co chung." << endl;
    }

    virtual void stopEngine() const {
        cout << model << ": Tat dong co chung." << endl;
    }

    // Phương thức virtual destructor là cần thiết để giải phóng bộ nhớ đúng cách
    // khi xóa đối tượng lớp con qua con trỏ lớp cơ sở
    virtual ~Vehicle() = default;
};

// Lớp Car kế thừa từ Vehicle
class Car : public Vehicle {
private:
    int numberOfDoors = 4;
public:
    Car(const string& m, int doors) : Vehicle(m), numberOfDoors(doors) {}

    // Ghi đè phương thức startEngine() cho hành vi đặc trưng hơn
    void startEngine() const override {
        cout << model << ": Khoi dong dong co xe hoi (" << numberOfDoors << " cua)." << endl;
    }

    // Phương thức riêng của Car
    void openTrunk() const {
        cout << model << ": Mo cop xe." << endl;
    }
};

// Lớp Motorcycle kế thừa từ Vehicle
class Motorcycle : public Vehicle {
private:
    bool hasSidecar;
public:
    Motorcycle(const string& m, bool sidecar) : Vehicle(m), hasSidecar(sidecar) {}

    // Ghi đè phương thức startEngine()
     void startEngine() const override {
        cout << model << ": Khoi dong dong co xe may (nghe tieng po lon hon)." << endl;
    }

    // Phương thức riêng của Motorcycle
    void wheelie() const {
        cout << model << ": Bay dau xe!" << endl;
    }
};

// --- Có thể thêm các lớp khác như Truck, Bus, v.v. ---
/*
class Truck : public Vehicle {
private:
    double maxLoad;
public:
    Truck(const string& m, double load) : Vehicle(m), maxLoad(load) {}
    void startEngine() const override {
         cout << model << ": Khoi dong dong co xe tai (tieng to)." << endl;
    }
    void loadCargo(double amount) {
        cout << model << ": Dang chat " << amount << " tan hang." << endl;
    }
};
*/


int main() {
    vector<unique_ptr<Vehicle>> garage;

    // Thêm các loại xe khác nhau vào garage
    garage.push_back(make_unique<Car>("Toyota Camry", 4));
    garage.push_back(make_unique<Motorcycle>("Honda CBR", false));
    garage.push_back(make_unique<Car>("Honda Civic", 5));
    // garage.push_back(make_unique<Truck>("Volvo FH", 30.0)); // Neu co lop Truck

    // Thực hiện hành động chung trên tất cả các phương tiện trong garage
    // Đa hình đảm bảo gọi đúng phương thức startEngine() của từng loại xe
    cout << "--- Khoi dong tat ca xe ---" << endl;
    for (const auto& v : garage) {
        v->startEngine();
    }
    cout << "---------------------------" << endl;

    // Thực hiện hành động riêng biệt (cần kiểm tra loại đối tượng thực tế)
    // Cách phổ biến là sử dụng dynamic_cast
    cout << "\n--- Thuc hien hanh dong rieng biet ---" << endl;
    for (const auto& v : garage) {
        // dynamic_cast tra ve con tro hop le neu ep kieu thanh cong, nguoc lai tra ve nullptr
        Car* car = dynamic_cast<Car*>(v.get());
        if (car) {
            car->openTrunk(); // Chi xe hoi moi co the mo cop
        }

        Motorcycle* moto = dynamic_cast<Motorcycle*>(v.get());
         if (moto) {
            moto->wheelie(); // Chi xe may moi co the bay dau
        }

        // Chú ý: Sử dụng dynamic_cast thường xuyên có thể cho thấy
        // thiết kế của bạn có thể cần xem xét lại để tận dụng đa hình tốt hơn
        // thông qua các phương thức virtual trong lớp cơ sở.
        // Tuy nhiên, đôi khi nó là cần thiết.
    }
     cout << "------------------------------------" << endl;

    // Tắt máy tất cả các xe
    cout << "\n--- Tat dong co tat ca xe ---" << endl;
    for (const auto& v : garage) {
        v->stopEngine(); // Phuong thuc chung
    }
     cout << "-----------------------------" << endl;


    return 0;
}

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 CarMotorcycle kế thừa từ Vehicle và thêm vào các thuộc tính/phương thức riêng (numberOfDoors, openTrunk cho Car; hasSidecar, wheelie cho Motorcycle).
  • Chúng ta có thể tạo một danh sách (vector) chứa các con trỏ tới Vehicle. Khi gọi v->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 hay wheelie), 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ật dynamic_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 SalariedEmployeeHourlyEmployee 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> // De dinh dang dau phay dong

// Lớp cơ sở Employee
class Employee {
protected:
    int id;
    string name;
public:
    // Constructor
    Employee(int i, const string& n) : id(i), name(n) {}

    // Getters chung
    int getId() const { return id; }
    string getName() const { return name; }

    // Phương thức tính lương - Cần triển khai riêng cho từng loại nhân viên
    virtual double calculatePay() const = 0; // Phương thức ảo thuần túy

    // Virtual destructor là rất quan trọng
    virtual ~Employee() = default;
};

// Lớp SalariedEmployee kế thừa từ Employee
class SalariedEmployee : public Employee {
private:
    double annualSalary; // Thu nhap hang nam co dinh
public:
    // Constructor
    SalariedEmployee(int i, const string& n, double salary)
        : Employee(i, n), annualSalary(salary) {}

    // Triển khai calculatePay cho nhân viên ăn lương cố định (tính lương tháng)
    double calculatePay() const override {
        return annualSalary / 12.0; // Chia luong nam cho 12 thang
    }

    // Co the them getter/setter cho annualSalary neu can
    double getAnnualSalary() const { return annualSalary; }
};

// Lớp HourlyEmployee kế thừa từ Employee
class HourlyEmployee : public Employee {
private:
    double hourlyRate;  // Muc luong theo gio
    int hoursWorked;    // So gio da lam trong ky tinh luong
public:
    // Constructor
    HourlyEmployee(int i, const string& n, double rate, int hours)
        : Employee(i, n), hourlyRate(rate), hoursWorked(hours) {}

    // Triển khai calculatePay cho nhân viên theo giờ
    double calculatePay() const override {
        return hourlyRate * hoursWorked; // Luong = So gio * Luong moi gio
    }

    // Phuong thuc de cap nhat so gio lam
    void setHoursWorked(int hours) {
        if (hours >= 0) {
            hoursWorked = hours;
        } else {
            cerr << "So gio lam khong hop le!\n";
        }
    }

    double getHourlyRate() const { return hourlyRate; }
    int getHoursWorked() const { return hoursWorked; }
};

// --- Có thể thêm các loại nhân viên khác như Manager (có thêm bonus), CommissionEmployee, v.v. ---
/*
class Manager : public SalariedEmployee {
private:
    double monthlyBonus;
public:
    Manager(int i, const string& n, double salary, double bonus)
        : SalariedEmployee(i, n, salary), monthlyBonus(bonus) {}

    double calculatePay() const override {
        // Lương quản lý = Lương cố định + Bonus
        return SalariedEmployee::calculatePay() + monthlyBonus;
    }
};
*/

int main() {
    // Sử dụng vector lưu trữ con trỏ thông minh tới Employee
    vector<unique_ptr<Employee>> staff;

    // Thêm các loại nhân viên khác nhau vào danh sách
    staff.push_back(make_unique<SalariedEmployee>(101, "Nguyen Van A", 60000.0)); // Luong nam 60k
    staff.push_back(make_unique<HourlyEmployee>(102, "Tran Thi B", 15.0, 160));    // 15$/gio, 160 gio
    staff.push_back(make_unique<SalariedEmployee>(103, "Le Van C", 72000.0)); // Luong nam 72k
    // staff.push_back(make_unique<Manager>(104, "Pham Thi D", 80000.0, 500.0)); // Neu co lop Manager

    // Duyệt qua danh sách nhân viên và tính lương cho từng người
    // Nhờ đa hình, chúng ta chỉ cần gọi calculatePay() chung
    cout << fixed << setprecision(2); // Dinh dang hien thi so thap phan

    cout << "--- Bang luong thang nay ---" << endl;
    for (const auto& emp : staff) {
        // C++ tu dong goi phuong thuc calculatePay() dung voi loai doi tuong thuc te
        cout << "ID: " << emp->getId()
                  << ", Ten: " << emp->getName()
                  << ", Luong: " << emp->calculatePay() << " $" << endl;
    }
    cout << "---------------------------" << endl;

    // Ví dụ cập nhật thông tin riêng của từng loại (cần dynamic_cast hoặc cách khác)
     cout << "\n--- Cap nhat du lieu rieng biet (vd: gio lam) ---" << endl;
     for (const auto& emp : staff) {
        // Kiểm tra nếu đối tượng hiện tại là HourlyEmployee
        HourlyEmployee* hourlyEmp = dynamic_cast<HourlyEmployee*>(emp.get());
        if (hourlyEmp) {
            cout << "Cap nhat gio lam cho " << hourlyEmp->getName() << endl;
            hourlyEmp->setHoursWorked(185); // Cap nhat so gio lam
            cout << "Luong thang sau cap nhat gio lam: " << hourlyEmp->calculatePay() << " $" << endl;
        }
     }
     cout << "-----------------------------------------------" << endl;


    return 0;
}

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.
  • SalariedEmployeeHourlyEmployee 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ọi calculatePay() 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ản calculatePay() của lớp con (SalariedEmployee::calculatePay hoặc HourlyEmployee::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ủa HourlyEmployee), bạn sẽ cần sử dụng dynamic_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

There are no comments at the moment.