34. Thành phần tốt hơn Kế thừa

Estimated reading: 8 minutes 245 views

Tóm tắt

Hãy tránh “thuế” của kế thừa: Kế thừa là mối quan hệ ràng buộc chặt chẽ đứng thứ hai trong C++, chỉ sau friendship. Ràng buộc chặt chẽ là điều không mong muốn và nên tránh nếu có thể. Vì vậy, hãy ưu tiên thành phần hơn là kế thừa, trừ khi bạn biết chắc rằng kế thừa thực sự mang lại lợi ích cho thiết kế của mình.

Thảo luận

Kế thừa thường bị lạm dụng, ngay cả bởi những lập trình viên dày dạn kinh nghiệm. Một quy tắc vững chắc trong kỹ thuật phần mềm là giảm thiểu sự ràng buộc: Nếu một mối quan hệ có thể được biểu đạt theo nhiều cách, hãy dùng mối quan hệ yếu nhất nhưng vẫn thực tiễn.

Bởi vì kế thừa là một trong những mối quan hệ chặt chẽ nhất mà ta có thể biểu diễn trong C++ (chỉ đứng sau friendship), nó chỉ thực sự phù hợp khi không có lựa chọn yếu hơn. Nếu bạn có thể diễn tả quan hệ lớp chỉ bằng thành phần, hãy ưu tiên thành phần. 

Ở đây, “thành phần” đơn giản nghĩa là nhúng một biến thành viên (member variable) của một kiểu này vào trong một kiểu khác. Bằng cách đó, bạn có thể nắm giữ và sử dụng đối tượng theo những cách cho phép bạn kiểm soát mức độ ràng buộc.

Ưu điểm quan trọng của thành phần so với kế thừa:

  • Tính linh hoạt cao hơn mà không ảnh hưởng đến mã gọi (client code):
    Một biến thành viên private nằm trong sự kiểm soát của bạn. Bạn có thể thay đổi cách nắm giữ nó — từ giữ trực tiếp (by value), sang giữ bằng (smart) pointer, hoặc Pimpl (Mục 43) — mà không phá vỡ mã phía người dùng; bạn chỉ cần sửa lại phần cài đặt của các hàm thành viên trong lớp. Nếu bạn quyết định cần một chức năng khác, bạn có thể thay đổi kiểu dữ liệu thành viên hoặc cách lưu trữ nó mà vẫn giữ nguyên giao diện công khai của lớp.
    Trái lại, nếu ngay từ đầu bạn dùng kế thừa công khai (public inheritance), khả năng cao là mã của client đã phụ thuộc vào đó; tức là bạn đã cam kết lớp của mình phải duy trì quyết định kế thừa, và sẽ khó thay đổi về sau (Mục 37).

  • Giảm phụ thuộc biên dịch, rút ngắn thời gian biên dịch:
    Giữ đối tượng bằng pointer (ưu tiên smart pointer), thay vì thành viên trực tiếp hoặc kế thừa, có thể giảm bớt phụ thuộc trong header, vì khai báo pointer tới một đối tượng không cần định nghĩa đầy đủ của lớp đó. Ngược lại, kế thừa luôn yêu cầu định nghĩa đầy đủ của lớp cơ sở phải hiện diện.
    Một kỹ thuật phổ biến là gom tất cả các thành viên private đằng sau một con trỏ mờ (opaque pointer), gọi là Pimpl (Mục 43).

  • Ít sự “kỳ quặc” hơn:
    Kế thừa từ một kiểu có thể khiến quá trình tìm tên (name lookup) kéo theo cả hàm hoặc template được định nghĩa trong cùng namespace với kiểu đó. Đây là điều rất tinh vi và khó gỡ rối (debug) (Mục 58).

  • Phạm vi áp dụng rộng hơn:
    Một số lớp vốn không được thiết kế để làm lớp cơ sở (Mục 35). Tuy nhiên, hầu hết các lớp đều có thể đóng vai trò làm thành viên.

  • Độ an toàn và khả năng chống lỗi cao hơn:
    Ràng buộc chặt chẽ của kế thừa khiến việc viết code an toàn với lỗi trở nên khó hơn ([Sutter02] §23).

  • Ít phức tạp và ít mong manh hơn:
    Kế thừa phơi bày bạn trước nhiều rắc rối bổ sung, chẳng hạn như che khuất tên (name hiding) và các biến chứng khác phát sinh khi lớp cơ sở được thay đổi sau này.

Tất nhiên, những điều trên không phải là các lập luận chống lại kế thừa hoàn toàn. Kế thừa mang lại rất nhiều sức mạnh, bao gồm khả năng thay thế (substitutability) và/hoặc khả năng ghi đè hàm ảo (xem Mục 36–39, cùng phần Ngoại lệ bên dưới). Nhưng hãy nhớ: đừng trả giá cho những gì bạn không cần; trừ khi bạn cần sức mạnh của kế thừa, còn thì đừng chịu đựng các nhược điểm của nó.

Ngoại lệ

Hãy dùng kế thừa công khai (public inheritance) để mô hình hóa tính thay thế (substitutability) (Mục 37).

Ngay cả khi bạn không cần cung cấp quan hệ thay thế cho tất cả các caller, bạn vẫn cần kế thừa không công khai (nonpublic inheritance) nếu bạn cần một trong các trường hợp sau (theo mức độ từ thường gặp đến cực hiếm):

  • Nếu bạn cần ghi đè (override) một hàm ảo.

  • Nếu bạn cần truy cập một thành viên protected.

  • Nếu bạn cần tạo đối tượng được sử dụng trước hoặc hủy nó sau lớp cơ sở.

  • Nếu bạn cần quan tâm đến lớp cơ sở ảo (virtual base class).

  • Nếu bạn chắc chắn được lợi từ tối ưu hóa lớp cơ sở rỗng (empty base class optimization), bao gồm việc điều đó thực sự quan trọng và trình biên dịch mục tiêu của bạn thực sự hỗ trợ tối ưu hóa đó (xem Mục 8).

  • Nếu bạn cần đa hình có kiểm soát (controlled polymorphism), tức là bạn cần quan hệ thay thế nhưng chỉ để lộ nó cho một số mã được chọn (thông qua friendship).

Tham khảo

[CargM92] pp. 49-65, 101-105 • [Cline99] §5.9-10, 8.11-12, 37.04 • [DewhurstO3] §95 • [Lakos96] §1.7, §6.3.1 • [McConnell93] §5 • [Meyers97] §40 • [StroustrupOO] §24.2-3 • [SutterOO] §22-24, §26-30 • [SutterO2] §23

Link

Leave a Comment

Chia sẻ:

34. Thành phần tốt hơn Kế thừa

Or copy link

CONTENTS