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.
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ộtstd::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ộtstd::string - Một hàm hủy ảo
- Một phương thức ảo thuần túy
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 khaiPowerable:- 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()đặtisOnthành true và in ra:<name>: Light turned onpowerOff()đặtisOnthành false và in ra:<name>: Light turned offgetPowerStatus()trả về"ON"hoặc"OFF"tùy theo trạng thái
SmartTV: triển khai cảPowerablevà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()đặtisOnthành true và in ra:<brand> TV: Powered onpowerOff()đặtisOnthành false, xóa mạng đang kết nối và in ra:<brand> TV: Powered offgetPowerStatus()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: DisconnectedgetConnectionInfo()trả về"Connected to <network>"nếu đã kết nối, hoặc"Not connected"nếu mạng để trống
- Một thành viên riêng tư
main.cpp: Đọc ba đầu vào (mỗi đầu vào trên một dòng riêng):- Tên đèn
- Thương hiệu TV
- Tên mạng
Tạo một
Lampvà mộtSmartTV. 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ảngPowerable*, sau đó lặp qua mảng và gọipowerOn()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ọiconnect()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
Powerablevà 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: OFFHã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;
}
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
1Nền tảng về OOP
Tệp bên ngoàiBiên dịch và tạo bản dựng C++Tệp header và tệp nguồnNamespace và phạm viGiới thiệu về OOP trong C++Lớp và đối tượngCon trỏ thisPhương thức (hàm thành viên)Thuộc tính (dữ liệu thành viên)Cơ bản về hàm khởi tạo và hàm hủyÔn tập: máy tính đơn giản4Thuộc tính của lớp
Thành viên đối tượng và thành viên tĩnhHàm truy xuất và hàm thiết lậpHàm thành viên hằngTừ khóa mutablePhương thức và biến tĩnhHàm và lớp bạnÔn tập: Trình quản lý tài khoản ngân hàng7Kế thừa
Kế thừa cơ bảnMức truy cập trong kế thừaThứ tự gọi hàm khởi tạo và hàm hủyGhi đè phương thứcHàm ảo và VTableĐa kế thừaKế thừa ảoÔn tập: hệ phân cấp nhân viên2Quản lý bộ nhớ
Bộ nhớ ngăn xếp và heapCon trỏ và tham chiếuBộ nhớ động (new/delete)Con trỏ thông minh trong C++RAII trong C++Ôn tập: Trình quản lý mảng động5Tính đóng gói
Phạm vi truy cập trong C++Tìm hiểu sâu về phạm vi truy cậpẨn thông tinStruct và lớpLớp lồng nhau và lớp bên trongÔn tập: hệ thống hồ sơ sinh viên8Đa hình
Đa hình lúc biên dịch và khi chạyNạp chồng hàmÔn lại hàm ảoHàm ảo thuần túyLớp trừu tượngThiết kế interface trong C++Ép kiểu động và RTTIÔn tập: Máy tính hình học3Hàm khởi tạo và hàm hủy
Hàm khởi tạo mặc địnhHàm khởi tạo có tham sốHàm khởi tạo sao chépHàm khởi tạo di chuyểnDanh sách khởi tạoHàm khởi tạo ủy quyềnTìm hiểu sâu về hàm hủyQuy tắc 3 / 5 / 0Ôn tập: lớp chuỗi6Nạp chồng toán tử
Giới thiệu về nạp chồng toán tửNạp chồng toán tử số họcNạp chồng toán tử so sánhToán tử luồngNạp chồng toán tử gánNạp chồng toán tử [] và ()Toán tử chuyển đổi kiểuÔn tập: lớp ma trận9Mẫu
Mẫu hàmMẫu lớpChuyên biệt hóa mẫuMẫu biến tham sốCơ bản về SFINAE và đặc điểm kiểuÔn tập: vùng chứa tổng quátTự luyện tập: Trình biên dịch C++ online