Kế thừa hay composition? Cách chọn khi thiết kế lớp

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Khi học OOP, kế thừa thường được giới thiệu như siêu năng lực: viết một lần, dùng lại nhiều nơi. Nhưng kinh nghiệm cho thấy lạm dụng nó sinh ra những cây phả hệ rối rắm. Có một lựa chọn khác gọn gàng hơn trong nhiều tình huống: composition.

Kế thừa: quan hệ "là một" (is-a)

Kế thừa dùng khi lớp con thực sự là một dạng đặc biệt của lớp cha: Chó là một Động vật, Hình tròn là một Hình học.

class Hinh:
    def dien_tich(self):
        raise NotImplementedError

class HinhTron(Hinh):
    def __init__(self, r):
        self.r = r
    def dien_tich(self):
        return 3.14159 * self.r ** 2

class HinhChuNhat(Hinh):
    def __init__(self, dai, rong):
        self.dai, self.rong = dai, rong
    def dien_tich(self):
        return self.dai * self.rong

Ở đây kế thừa hợp lý: các lớp con chia sẻ cùng giao diện, và code khác có thể dùng chúng thay thế cho nhau.

Composition: quan hệ "có một" (has-a)

Composition (kết hợp) nghĩa là một lớp chứa object của lớp khác như một thành phần. Xe hơi có một động cơ, không phải là một động cơ.

class DongCo:
    def khoi_dong(self):
        return "Brừm!"

class XeHoi:
    def __init__(self):
        self.dong_co = DongCo()

    def chay(self):
        return self.dong_co.khoi_dong() + " Xe đang chạy"

print(XeHoi().chay())

Xe ủy quyền công việc cho động cơ. Muốn đổi sang động cơ điện, bạn chỉ thay thành phần mà không phải sửa cây kế thừa.

Vấn đề của kế thừa lạm dụng

  • Gắn kết chặt: sửa lớp cha có thể làm hỏng mọi lớp con mà bạn không ngờ tới.
  • Cây kế thừa sâu: đọc code phải nhảy qua nhiều lớp mới hiểu hành vi.
  • Quan hệ gượng ép: ví dụ cho lớp Ngăn xếp kế thừa lớp Danh sách chỉ để tái sử dụng code, khiến Ngăn xếp có cả những thao tác không nên có.
  • Nổ tổ hợp: ví dụ tạo lớp cho mọi kết hợp Chim bay được, Chim biết bơi, Chim vừa bay vừa bơi.

Đây là lý do một nguyên tắc thiết kế nổi tiếng khuyên: ưu tiên composition hơn kế thừa (favor composition over inheritance).

Giải quyết bài toán chim bằng composition

class BayDuoc:
    def di_chuyen(self):
        return "bay"

class BoiDuoc:
    def di_chuyen(self):
        return "bơi"

class Vit:
    def __init__(self):
        self.ky_nang = [BayDuoc(), BoiDuoc()]

    def mo_ta(self):
        return ", ".join(k.di_chuyen() for k in self.ky_nang)

print(Vit().mo_ta())   # bay, bơi

Khả năng được tách thành những mảnh riêng và lắp ghép tùy ý. Thêm kỹ năng mới chỉ là thêm một mảnh.

Cách quyết định

Câu hỏi tự đặtNghiêng về
B có thực sự là một dạng của A ở mọi nơi dùng A?Kế thừa
Chỉ muốn dùng lại vài chức năng của A?Composition
Hành vi cần đổi lúc chạy chương trình?Composition
Có nhiều chiều biến thể độc lập?Composition

Kế thừa vẫn có chỗ đứng

Đừng hiểu nhầm rằng kế thừa là xấu. Khung ứng dụng và thư viện giao diện dùng nó rất nhiều, và nó rất tự nhiên với các họ đối tượng rõ ràng. Nguyên tắc thực dụng: giữ cây kế thừa nông (một hai tầng), lớp cha ổn định, và luôn kiểm tra xem lớp con có thể thay thế lớp cha mà không gây bất ngờ không.

Ví dụ trong game: nhân vật và kỹ năng

Một game có chiến binh, pháp sư, cung thủ; mỗi loại có thể có kỹ năng khác nhau, thậm chí đổi kỹ năng trong lúc chơi. Nếu dùng kế thừa, bạn sẽ có cây lớp phình to. Với composition, kỹ năng là các mảnh riêng:

class DanhThuong:
    def dung(self):
        return "chém 10 sát thương"

class PhepLua:
    def dung(self):
        return "lửa 25 sát thương"

class NhanVat:
    def __init__(self, ten, ky_nang):
        self.ten = ten
        self.ky_nang = ky_nang

    def tan_cong(self):
        return f"{self.ten} {self.ky_nang.dung()}"

hiep_si = NhanVat("Hiệp sĩ", DanhThuong())
print(hiep_si.tan_cong())
hiep_si.ky_nang = PhepLua()      # đổi kỹ năng khi đang chạy
print(hiep_si.tan_cong())

Đây là một dạng của mẫu thiết kế Strategy. Chỉ cần đổi thành phần là hành vi đổi theo, không cần tạo lớp con mới. Nhiều game engine, như hệ thống thành phần của Unity, được thiết kế theo tinh thần này.

Lỗi thường gặp

  • Kế thừa chỉ để dùng lại một hai hàm tiện ích.
  • Lớp con bỏ qua hoặc vô hiệu hóa phần lớn hành vi của lớp cha.
  • Composition nhưng thành phần phụ thuộc chặt vào nhau, mất lợi ích linh hoạt.

Kết luận

Hỏi "là một" hay "có một" là cách nhanh để chọn giữa kế thừa và composition. Khi phân vân, hãy chọn composition vì nó linh hoạt và ít ràng buộc hơn. Hãy thử viết lại một lớp kế thừa gượng ép trong dự án của bạn bằng composition, bạn sẽ thấy sự khác biệt.

Muốn học bài bản và có người hướng dẫn? Hãy xem các khóa học trên codebem.com để đi từ nền tảng đến dự án thực tế.

author_photo
Nguyễn Đình Cường

0 comment