35. Không kế thừa từ những lớp không được thiết kế làm lớp cơ sở

Estimated reading: 8 minutes 240 views

Tóm tắt

Không phải ai cũng muốn có con:
Các lớp được thiết kế để sử dụng độc lập tuân theo một bản thiết kế khác so với lớp cơ sở (Mục 32). Việc dùng một lớp độc lập làm lớp cơ sở là một lỗi thiết kế nghiêm trọng và nên tránh. Để thêm hành vi, hãy ưu tiên thêm hàm không phải thành viên thay vì hàm thành viên (Mục 44). Để thêm trạng thái, hãy ưu tiên thành phần (composition) thay vì kế thừa (Mục 34). Tránh kế thừa từ các lớp cơ sở cụ thể (concrete base classes).

Thảo luận

Sử dụng kế thừa khi không cần thiết thể hiện một niềm tin sai chỗ vào sức mạnh của lập trình hướng đối tượng. Trong C++, khi định nghĩa một lớp cơ sở, bạn cần thực hiện những việc đặc thù (Mục 3250, và 54). Khi thiết kế các lớp độc lập, lại cần những điều rất khác, thậm chí ngược lại. Việc kế thừa từ một lớp độc lập sẽ mở ra hàng loạt vấn đề, mà hiếm khi trình biên dịch cảnh báo bạn.

Người mới học đôi khi kế thừa từ lớp giá trị (value class), chẳng hạn một lớp chuỗi (string chuẩn hoặc loại khác), để “thêm nhiều chức năng hơn.” Tuy nhiên, việc định nghĩa các hàm tự do (không phải thành viên) vượt trội hơn nhiều so với việc tạo ra super_string, vì các lý do sau:

  • Hàm không phải thành viên hoạt động tốt trong code hiện tại vốn đã thao tác với string. Ngược lại, nếu bạn cung cấp một super_string, bạn buộc phải thay đổi cả hệ thống: đổi kiểu (type), đổi chữ ký hàm (khai báo hàm) sang super_string.

  • Các hàm giao diện nhận string giờ phải:
    a) tránh xa chức năng bổ sung của super_string (vô ích),
    b) copy tham số sang super_string (lãng phí), hoặc
    c) ép kiểu string& sang super_string& (rườm rà, có khi còn không hợp pháp)

  • Các hàm thành viên của super_string không có thêm quyền truy cập vào nội bộ của string hơn so với các hàm không phải thành viên, vì string vốn dĩ không có thành viên protected (nó không được thiết kế để kế thừa).

  • Nếu super_string vô tình che khuất (hide) một số hàm của string (nhớ rằng việc định nghĩa lại hàm không ảo trong lớp dẫn xuất không phải override, mà chỉ là che khuất), điều này có thể gây ra sự nhầm lẫn lan rộng trong code vốn ban đầu thao tác với string, nhưng sau đó lại tự động bị chuyển sang từ super_string.

Vì vậy, hãy ưu tiên thêm chức năng thông qua các hàm không phải thành viên (Mục 44). Để tránh các vấn đề tra cứu tên (name lookup), hãy chắc chắn đặt chúng trong cùng namespace với kiểu (type) mà bạn định mở rộng (Mục 57).

Một số người không thích hàm không phải thành viên vì cú pháp gọi là Fun(str) thay vì str.Fun(), nhưng đây chỉ là vấn đề thói quen cú pháp. (Và còn có câu nói nổi tiếng được cho là của Alan Perlis: “Quá nhiều đường cú pháp sẽ gây ung thư dấu chấm phẩy.”)

Điều gì xảy ra nếu super_string muốn kế thừa từ string để thêm trạng thái, chẳng hạn encoding hoặc bộ nhớ đệm (cached) số từ? Kế thừa công khai (public inheritance) vẫn không được khuyến khích, vì:

  • string không được bảo vệ khỏi slicing (Mục 54). Nghĩa là, khi một super_string bị copy vào một string, phần trạng thái bổ sung sẽ bị cắt bỏ lặng lẽ.

  • Ngoài ra, kế thừa từ một lớp có destructor công khai nhưng không ảo sẽ dễ dẫn đến hành vi không xác định (undefined behavior) khi bạn delete một con trỏ string vốn thực sự trỏ tới một đối tượng super_string (Mục 50). Undefined behavior này có thể tạm thời “có vẻ” được compiler và bộ quản lý bộ nhớ chấp nhận, nhưng thực tế sẽ dẫn đến lỗi ngầm, rò rỉ bộ nhớ, hỏng heap, và ác mộng khi porting.

Ví dụ

Thành phần thay vì kế thừa công khai hoặc riêng tư. Điều gì xảy ra nếu bạn cần một localized_string mà “gần giống như string, nhưng có thêm một vài trạng thái và hàm, và một vài tinh chỉnh đối với các hàm string hiện có”, và nhiều phần cài đặt của các hàm sẽ không thay đổi? Khi đó, hãy cài đặt nó dựa trên string một cách an toàn bằng cách sử dụng thành phần thay vì kế thừa (điều này ngăn chặn hiện tượng cắt lát và xóa không đa hình), và thêm các hàm chuyển tiếp để làm cho các hàm không thay đổi có thể truy cập được:

				
					class localized_string {
public:
    // ... cung cấp các hàm chuyển tiếp đến các hàm thành viên của string mà ta muốn giữ nguyên
    // (ví dụ: định nghĩa insert gọi impl_.insert) ...
    void clear(); // che khuất / định nghĩa lại clear()
    bool is_in_klingon() const; // thêm chức năng mới
private:
    std::string impl_;
    // thêm trạng thái mở rộng ...
};
				
			

Phải thừa nhận rằng việc viết các hàm chuyển tiếp cho những hàm thành viên mà bạn muốn giữ lại là khá tẻ nhạt, nhưng một cách cài đặt như vậy tốt hơn nhiều và an toàn hơn nhiều so với việc sử dụng kế thừa công khai hoặc không công khai.

Tham khảo

[DewhurstO3] §70, §93 [Meyers97] §33 • [StroustrupOO] §24.2-3, §25.2

 Link

Leave a Comment

Chia sẻ:

35. Không kế thừa từ những lớp không được thiết kế làm lớp cơ sở

Or copy link

CONTENTS