37. Kế thừa Public mang tính chất cung cấp sự thay thế. Hãy kế thừa không cho mục đích “tái sử dụng” mà cho “được tái sử dụng”.

Estimated reading: 9 minutes 230 views

Tóm tắt

  • Hiểu cái gì (Know what): Kế thừa công khai (Public inheritance) cho phép một con trỏ (pointer) hoặc tham chiếu (reference) tới lớp cơ sở (base class) thực sự tham chiếu đến một đối tượng của một lớp dẫn xuất (derived class) nào đó, mà không phá vỡ tính đúng đắn của mã nguồn và không cần thay đổi mã nguồn hiện có.

  • Hiểu tại sao (Know why): Đừng kế thừa công khai để tái sử dụng mã (mã tồn tại trong lớp cơ sở); hãy kế thừa công khai để được tái sử dụng (bởi mã nguồn hiện có vốn đang sử dụng các đối tượng cơ sở theo kiểu đa hình (poly-morphically)).

Thảo luận

Bất chấp hai thập kỷ kiến thức về thiết kế hướng đối tượng, mục đích và thực hành của kế thừa công khai vẫn thường xuyên bị hiểu lầm, và nhiều cách sử dụng kế thừa bị sai sót.

Kế thừa công khai luôn phải mô hình hóa quan hệ “là một” (“is-a”) hay “làm việc như là” (“works-like-a”) theo Nguyên lý thay thế Liskov (Liskov Substitution Principle – xem [Liskov88]): Tất cả các hợp đồng (contracts) của lớp cơ sở phải được đáp ứng, và do đó tất cả các ghi đè (overrides) của các hàm thành viên ảo (virtual member functions) phải không yêu cầu nhiều hơn và không hứa hẹn ít hơn so với phiên bản cơ sở của chúng nếu chúng muốn thực hiện thành công hợp đồng của lớp cơ sở. Mã nguồn sử dụng một con trỏ hoặc tham chiếu tới một Base phải hành xử đúng đắn ngay cả khi con trỏ hoặc tham chiếu đó thực sự trỏ tới một Derived.

Việc lạm dụng kế thừa phá hủy tính đúng đắn. Việc kế thừa được cài đặt không chính xác thường đi chệch hướng do không tuân thủ hợp đồng tường minh hoặc ngầm định mà lớp cơ sở thiết lập. Những hợp đồng như vậy có thể rất tinh tế, và khi chúng không thể được diễn đạt trực tiếp trong mã nguồn, lập trình viên phải cực kỳ cẩn thận. (Một số mẫu thiết kế (patterns) giúp khai báo ý định trong mã rõ ràng hơn; Mục 39).

Để chắt lọc một ví dụ thường được trích dẫn: Hãy xem xét hai lớp Square (Hình vuông) và Rectangle (Hình chữ nhật), mỗi lớp đều có các hàm ảo (virtual functions) để thiết lập chiều cao và chiều rộng của chúng. Khi đó Square không thể kế thừa chính xác từ Rectangle, bởi vì mã nguồn sử dụng một Rectangle có thể sửa đổi được sẽ giả định rằng SetWidth (Thiết lập chiều rộng) không làm thay đổi chiều cao (bất kể Rectangle có tài liệu hóa hợp đồng đó một cách tường minh hay không), trong khi Square::SetWidth không thể bảo toàn hợp đồng đó và bất biến (invariant) về tính vuông vắn của chính nó cùng một lúc. Nhưng Rectangle cũng không thể kế thừa chính xác từ Square, nếu các khách hàng (clients) của Square giả định ví dụ rằng diện tích của một Square là chiều rộng của nó bình phương lên, hoặc nếu họ dựa vào một số thuộc tính khác không đúng với Rectangle.

Mô tả “là một” (“is-a”) của kế thừa công khai bị hiểu lầm khi mọi người sử dụng nó để vẽ ra những phép ẩn dụ không liên quan trong thế giới thực: Một hình vuông “là một” hình chữ nhật (về mặt toán học) nhưng một Square không phải là một Rectangle (về mặt hành vi). Do đó, thay vì “is-a”, chúng tôi thích nói là “works-like-a” (làm việc như là) (hoặc, nếu bạn thích, “usable-as-a” – có thể sử dụng như là) để làm cho mô tả ít bị hiểu lầm hơn.

Kế thừa công khai thực sự là về việc tái sử dụng, nhưng không theo cách mà nhiều lập trình viên dường như vẫn nghĩ. Như đã chỉ ra, mục đích của kế thừa công khai là để thực thi khả năng thay thế (substitutability) (xem [Liskov88]). Mục đích của kế thừa công khai không phải là để lớp dẫn xuất tái sử dụng mã của lớp cơ sở nhằm thực thi chính nó dựa trên mã của lớp cơ sở. Mối quan hệ “được thực thi dựa trên” (is-implemented-in-terms-of) như vậy hoàn toàn có thể hợp lý, nhưng nên được mô hình hóa bằng kết hợp (composition) — hoặc, chỉ trong các trường hợp đặc biệt, bằng kế thừa không công khai (nonpublic inheritance) (Mục 34).

Nói cách khác: Khi đa hình động (dynamic polymorphism) là đúng và phù hợp, kết hợp (composition) là ích kỷ; kế thừa (inheritance) là hào phóng (“Composition is selfish; Inheritance is generous”).

Một lớp dẫn xuất mới là một trường hợp đặc biệt mới của một sự trừu tượng hóa tổng quát hiện có. Mã nguồn đa hình (động) hiện có sử dụng một Base& hoặc Base* bằng cách gọi các hàm ảo của Base sẽ có thể sử dụng liền mạch các đối tượng của MyNewDerivedType (Lớp dẫn xuất mới của tôi) kế thừa từ Base. Kiểu dẫn xuất mới thêm chức năng mới vào mã nguồn hiện có, mã này không cần phải thay đổi nhưng có thể tăng chức năng một cách liền mạch khi các đối tượng dẫn xuất mới được nạp vào (plugged in).

Các yêu cầu mới nên được đáp ứng một cách tự nhiên bởi mã mới; các yêu cầu mới không nên gây ra việc làm lại (rework) trên mã hiện có. (Mục 36)

Trước khi có hướng đối tượng, việc mã mới gọi mã hiện có luôn dễ dàng. Kế thừa công khai đặc biệt giúp cho mã hiện có gọi mã mới một cách liền mạch và an toàn dễ dàng hơn. (Các khuôn mẫu (templates) cũng vậy, chúng cung cấp đa hình tĩnh (static polymorphism) có thể hòa hợp tốt với đa hình động; Mục 64)

Các ngoại lệ

Các lớp chính sách (Policy classes) và mixins thêm hành vi bằng kế thừa công khai, nhưng đây không phải là lạm dụng kế thừa công khai để mô hình hóa quan hệ “được thực thi dựa trên” (is-implemented-in-terms-of).

Tham khảo

[Cargill92] pp. 19-20 • [Cline99] §5.13, §7.01-8.15 • [DewhurstO3] §92 • [Liskov88] • [Meyers97] §35 • [StroustrupOO] §12.4.1, §23.4.3.1, §24.3.4 • [SutterOO] §22-24

Leave a Comment

Chia sẻ:

37. Kế thừa Public mang tính chất cung cấp sự thay thế. Hãy kế thừa không cho mục đích “tái sử dụng” mà cho “được tái sử dụng”.

Or copy link

CONTENTS