Menu
CoddyTech

Thiết kế interface trong C++

Thuộc phần Lập trình hướng đối tượng trong hành trình C++ của Coddy. Bài học 61 trên 104.

Một giao diện (interface) trong C++ là một lớp trừu tượng (abstract class), trong đó mọi hàm thành viên (function), ngoại trừ hàm hủy (virtual destructor), đều là hàm ảo thuần túy (pure virtual). Khác với lớp trừu tượng có thể chứa một phần mã triển khai, giao diện chỉ định nghĩa những thao tác cần có: chúng xác định một hợp đồng mà không quy định hành vi nào.—

class Drawable {
public:
    virtual void draw() = 0;
    virtual void resize(double factor) = 0;
    virtual ~Drawable() = default;
};

Giao diện Drawable này đảm bảo mọi lớp triển khai giao diện đều có các phương thức draw() và resize(), nhưng không quy định chúng hoạt động như thế nào. Sự tách biệt này rất hữu ích: code có thể phụ thuộc vào giao diện mà không cần biết gì về các kiểu cụ thể.

class Circle : public Drawable {
    double radius;
public:
    Circle(double r) : radius(r) {}
    void draw() override { std::cout << "Drawing circle" << std::endl; }
    void resize(double factor) override { radius *= factor; }
};

class Button : public Drawable {
    std::string label;
public:
    Button(std::string l) : label(l) {}
    void draw() override { std::cout << "Drawing button: " << label << std::endl; }
    void resize(double factor) override { /* thay đổi kích thước nút */ }
};

Một lớp có thể triển khai nhiều giao diện, giúp bạn thiết kế linh hoạt để các đối tượng đảm nhận những vai trò khác nhau:

class Clickable {
public:
    virtual void onClick() = 0;
    virtual ~Clickable() = default;
};

class IconButton : public Drawable, public Clickable {
public:
    void draw() override { std::cout << "Drawing icon" << std::endl; }
    void resize(double factor) override { }
    void onClick() override { std::cout << "Clicked!" << std::endl; }
};

Interface giúp giảm sự phụ thuộc chặt chẽ: code dựa vào các khái niệm trừu tượng thay vì cách triển khai cụ thể, nhờ đó bạn dễ mở rộng và kiểm thử hơn.

challenge icon

Thử thách

Dễ

Hãy xây dựng một hệ thống quản lý thiết bị để minh họa cách các giao diện xác định những quy ước cho nhiều loại thiết bị khác nhau. Bạn sẽ tạo hai giao diện riêng biệt mà các thiết bị có thể triển khai, sau đó xây dựng các lớp thiết bị cụ thể đáp ứng một hoặc cả hai quy ước này.

Bạn sẽ sắp xếp code trong ba tệp:

  • Interfaces.h: Định nghĩa hai lớp trừu tượng thuần túy (giao diện) đại diện cho các khả năng khác nhau của thiết bị:

    Powerable: mọi thiết bị có thể bật và tắt:

    • Một phương thức ảo thuần túy powerOn()
    • Một phương thức ảo thuần túy powerOff()
    • Một phương thức ảo thuần túy getPowerStatus() trả về một std::string
    • Một hàm hủy ảo

    Connectable: mọi thiết bị có thể kết nối với mạng:

    • Một phương thức ảo thuần túy connect(const std::string& network)
    • Một phương thức ảo thuần túy disconnect()
    • Một phương thức ảo thuần túy getConnectionInfo() trả về một std::string
    • Một hàm hủy ảo
  • Devices.h: Triển khai các lớp thiết bị cụ thể sử dụng những giao diện này:

    Lamp: chỉ triển khai Powerable:

    • Một thành viên riêng tư bool isOn (ban đầu là false)
    • Một thành viên riêng tư std::string name
    • Một hàm khởi tạo nhận tên đèn
    • powerOn() đặt isOn thành true và in ra: <name>: Light turned on
    • powerOff() đặt isOn thành false và in ra: <name>: Light turned off
    • getPowerStatus() trả về "ON" hoặc "OFF" tùy theo trạng thái

    SmartTV: triển khai cả Powerable và Connectable:

    • Các thành viên riêng tư: bool isOn (ban đầu là false), std::string brand, std::string currentNetwork (ban đầu để trống)
    • Một hàm khởi tạo nhận thương hiệu TV
    • powerOn() đặt isOn thành true và in ra: <brand> TV: Powered on
    • powerOff() đặt isOn thành false, xóa mạng đang kết nối và in ra: <brand> TV: Powered off
    • getPowerStatus() trả về "ON" hoặc "OFF"
    • connect() lưu tên mạng và in ra: <brand> TV: Connected to <network>
    • disconnect() xóa mạng đang kết nối và in ra: <brand> TV: Disconnected
    • getConnectionInfo() trả về "Connected to <network>" nếu đã kết nối, hoặc "Not connected" nếu mạng để trống
  • main.cpp: Đọc ba đầu vào (mỗi đầu vào trên một dòng riêng):
    1. Tên đèn
    2. Thương hiệu TV
    3. Tên mạng

    Tạo một Lamp và một SmartTV. Minh họa cách dùng cùng một giao diện với nhiều thiết bị khác nhau:

    Đầu tiên, thao tác với cả hai thiết bị thông qua giao diện Powerable. Lưu con trỏ đến cả hai thiết bị trong một mảng Powerable*, sau đó lặp qua mảng và gọi powerOn() cho từng thiết bị, rồi in trạng thái theo định dạng: Status: <powerStatus>

    In một dòng trống, sau đó thao tác với SmartTV thông qua giao diện Connectable. Tạo một con trỏ Connectable* trỏ đến SmartTV, gọi connect() với tên mạng và in ra: Connection: <connectionInfo>

    In thêm một dòng trống, sau đó tắt cả hai thiết bị thông qua mảng Powerable và hiển thị trạng thái cuối cùng của chúng.

Ví dụ, với đầu vào Desk Lamp, Samsung và HomeWiFi:

Desk Lamp: Light turned on
Status: ON
Samsung TV: Powered on
Status: ON

Samsung TV: Connected to HomeWiFi
Connection: Connected to HomeWiFi

Desk Lamp: Light turned off
Status: OFF
Samsung TV: Powered off
Status: OFF

Hãy chú ý rằng có thể xem SmartTV là Powerable hoặc Connectable, tùy vào con trỏ giao diện mà bạn sử dụng. Tính linh hoạt này là lợi ích của việc triển khai nhiều giao diện. Code của bạn có thể làm việc với bất kỳ thiết bị nào đáp ứng quy ước cần thiết, mà không cần biết kiểu cụ thể của thiết bị đó.

Tự làm thử

#include <iostream>
#include <string>
#include "Devices.h"

using namespace std;

int main() {
    // Đọc dữ liệu đầu vào
    string lampName;
    string tvBrand;
    string networkName;
    
    getline(cin, lampName);
    getline(cin, tvBrand);
    getline(cin, networkName);
    
    // TODO: Tạo một đối tượng (object) Lamp và một đối tượng SmartTV
    
    // TODO: Tạo một mảng (array) con trỏ Powerable* chứa cả hai thiết bị
    
    // TODO: Duyệt mảng và gọi powerOn() trên từng thiết bị
    // Sau mỗi lần gọi powerOn(), in ra: Status: <powerStatus>
    
    // TODO: In ra một dòng trống
    
    // TODO: Tạo một con trỏ Connectable* trỏ đến SmartTV
    // Gọi connect() với tên mạng
    // In ra: Connection: <connectionInfo>
    
    // TODO: In ra một dòng trống
    
    // TODO: Duyệt mảng Powerable và gọi powerOff() trên từng thiết bị
    // Sau mỗi lần gọi powerOff(), in ra: Status: <powerStatus>
    
    return 0;
}
quiz iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Trình biên dịch C++ online