Phân Biệt Abstract Class và Interface Trong C++: Hướng Dẫn Chi Tiết

Trong lập trình hướng đối tượng (OOP), abstract class và interface là hai khái niệm quan trọng. Tuy nhiên, sự khác biệt giữa chúng trong C++ có thể không rõ ràng như trong các ngôn ngữ khác. Bài viết này sẽ đi sâu vào từng khái niệm, cung cấp ví dụ cụ thể và hướng dẫn bạn cách lựa chọn giữa abstract class và interface trong các tình huống khác nhau.

Abstract Class Là Gì?

Abstract class là một lớp không hoàn chỉnh, nghĩa là nó chứa ít nhất một phương thức ảo (virtual function) mà không có định nghĩa (body). Mục đích của abstract class là để cung cấp một bản thiết kế chung cho các lớp con (subclass). Các lớp con này bắt buộc phải định nghĩa (override) các phương thức ảo của lớp abstract.

Abstract class cho phép chúng ta ẩn đi các chi tiết triển khai cụ thể và chỉ hiển thị giao diện (interface) của đối tượng. Điều này giúp đạt được tính đa hình (polymorphism) lúc runtime, nghĩa là một đối tượng có thể có nhiều hình thái khác nhau tùy thuộc vào lớp con mà nó thuộc về.

Ví dụ, chúng ta có thể có một abstract class Shape (Hình) với một phương thức ảo area() (tính diện tích). Các lớp con như Rectangle (Hình chữ nhật), Circle (Hình tròn) sẽ override phương thức area() để tính diện tích theo công thức riêng của từng hình.

#include <iostream>

class Shape {
public:
    // Phương thức ảo thuần túy (pure virtual function)
    virtual double area() = 0;

    // Phương thức ảo (virtual function) với định nghĩa mặc định
    virtual void display() {
        std::cout << "Đây là một hình dạng." << std::endl;
    }
};

class Rectangle : public Shape {
private:
    double width;
    double height;

public:
    Rectangle(double w, double h) : width(w), height(h) {}

    // Override phương thức area()
    double area() override {
        return width * height;
    }

    // Override phương thức display()
    void display() override {
        std::cout << "Đây là một hình chữ nhật." << std::endl;
    }
};

int main() {
    // Không thể tạo đối tượng trực tiếp từ abstract class
    // Shape s; // Lỗi: không thể tạo đối tượng của abstract class

    Rectangle r(5, 10);
    std::cout << "Diện tích hình chữ nhật: " << r.area() << std::endl;
    r.display();

    Shape* shapePtr = &r; // Con trỏ Shape trỏ đến đối tượng Rectangle
    std::cout << "Diện tích (qua con trỏ Shape): " << shapePtr->area() << std::endl;
    shapePtr->display(); // Gọi phương thức display() của Rectangle

    return 0;
}

Trong ví dụ trên, Shape là một abstract class vì nó có phương thức ảo thuần túy area() = 0;. Chúng ta không thể tạo đối tượng trực tiếp từ Shape, mà chỉ có thể tạo đối tượng từ các lớp con như Rectangle, Circle,… Lớp Rectangle kế thừa từ Shape và override phương thức area() để cung cấp cách tính diện tích cụ thể cho hình chữ nhật.

Ví dụ code abstract class trong C++ minh họa tính kế thừa và đa hình.

Kết quả sau khi chạy chương trình abstract class, thể hiện tính đa hình lúc runtime.

Lưu ý quan trọng:

  • Trong C++, một lớp được coi là abstract nếu nó chứa ít nhất một phương thức ảo thuần túy (pure virtual function), được khai báo bằng = 0.
  • Không thể tạo đối tượng trực tiếp từ abstract class.
  • Các lớp con kế thừa từ abstract class phải override tất cả các phương thức ảo thuần túy của lớp cha, nếu không chúng cũng sẽ trở thành abstract class.

Interface Là Gì?

Trong C++, interface thường được triển khai bằng cách sử dụng một class mà tất cả các phương thức đều là phương thức ảo thuần túy (pure virtual functions) và có một virtual destructor. Interface định nghĩa một tập hợp các phương thức mà một lớp phải triển khai.

Interface tập trung vào việc định nghĩa những gì một lớp có thể làm, chứ không phải cách lớp đó thực hiện. Nó giống như một bản hợp đồng: bất kỳ lớp nào “ký” vào bản hợp đồng này (tức là implement interface) đều phải cung cấp triển khai cho tất cả các phương thức được định nghĩa trong interface.

#include <iostream>

class ISpeakable {
public:
    // Phương thức ảo thuần túy (pure virtual function)
    virtual void speak() = 0;

    // Phương thức hủy ảo (virtual destructor) - quan trọng cho tính đa hình
    virtual ~ISpeakable() {}
};

class Dog : public ISpeakable {
public:
    // Override phương thức speak()
    void speak() override {
        std::cout << "Woof!" << std::endl;
    }
};

class Cat : public ISpeakable {
public:
    // Override phương thức speak()
    void speak() override {
        std::cout << "Meow!" << std::endl;
    }
};

int main() {
    Dog dog;
    Cat cat;

    dog.speak(); // Output: Woof!
    cat.speak(); // Output: Meow!

    ISpeakable* animal1 = &dog;
    ISpeakable* animal2 = &cat;

    animal1->speak(); // Output: Woof!
    animal2->speak(); // Output: Meow!

    return 0;
}

Trong ví dụ trên, ISpeakable là một interface. Cả DogCat đều implement interface ISpeakable và cung cấp triển khai riêng cho phương thức speak().

Ví dụ interface trong C++ với phương thức thuần ảo tinnhDienTich().

Kết quả khi triển khai interface trong C++, thể hiện khả năng định nghĩa hành vi cho các lớp.

Lưu ý quan trọng:

  • Trong C++, một interface thường là một class mà tất cả các phương thức đều là phương thức ảo thuần túy.
  • Interface không có thuộc tính (member variables).
  • Interface có thể kế thừa từ các interface khác.
  • Virtual destructor là cần thiết để đảm bảo giải phóng bộ nhớ đúng cách khi sử dụng con trỏ đến interface.

Khi Nào Nên Sử Dụng Abstract Class và Interface?

Việc lựa chọn giữa abstract class và interface phụ thuộc vào ngữ cảnh cụ thể của bài toán. Dưới đây là một số hướng dẫn chung:

  • Sử dụng abstract class khi:

    • Bạn muốn cung cấp một bản thiết kế chung cho các lớp con, bao gồm cả hành vi (phương thức) và trạng thái (thuộc tính).
    • Bạn muốn một số phương thức có triển khai mặc định.
    • Các lớp con có mối quan hệ “IS-A” (ví dụ: “Xe máy LÀ một loại Xe”).
  • Sử dụng interface khi:

    • Bạn chỉ muốn định nghĩa một tập hợp các hành vi mà một lớp phải hỗ trợ.
    • Bạn không muốn cung cấp bất kỳ triển khai mặc định nào.
    • Các lớp có thể có mối quan hệ “LIKE-A” (ví dụ: “Xe buýt CÓ THỂ chở khách”). Một class có thể implement nhiều interface.
    • Bạn muốn đạt được tính đa kế thừa (multiple inheritance) về mặt giao diện (C++ không hỗ trợ đa kế thừa trực tiếp từ các class, nhưng hỗ trợ đa kế thừa từ các interface).

Ví dụ thực tế:

Giả sử bạn đang xây dựng một hệ thống quản lý các loại phương tiện giao thông.

  • Bạn có thể sử dụng abstract class Vehicle (Phương tiện) để định nghĩa các thuộc tính chung như engine (động cơ), color (màu sắc) và các phương thức chung như startEngine() (khởi động động cơ), stopEngine() (tắt động cơ). Các lớp con như Car (Ô tô), Motorcycle (Xe máy), Truck (Xe tải) sẽ kế thừa từ Vehicle và cung cấp triển khai cụ thể cho các phương thức này.
  • Bạn có thể sử dụng interface IDrivable (Có thể lái được) để định nghĩa phương thức drive() (lái xe). Cả Car, Motorcycle, Truck đều implement interface IDrivable. Ngoài ra, bạn có thể có một interface ICarryPassengers (Chở khách) mà chỉ có CarBus implement.

So sánh abstract class và interface thông qua ví dụ trực quan về xe cộ.

Lời Kết

Hiểu rõ sự khác biệt giữa abstract class và interface là rất quan trọng để viết mã C++ hướng đối tượng hiệu quả. Bằng cách nắm vững các khái niệm này, bạn có thể thiết kế các hệ thống linh hoạt, dễ bảo trì và có khả năng mở rộng cao. Hãy luôn cân nhắc ngữ cảnh cụ thể của bài toán để đưa ra lựa chọn phù hợp nhất giữa abstract class và interface.