Topic 31. Inheritance Tax — Đừng trả 'thuế' kế thừa nữa (Pragmatic Programmer #31)

Phong

Dạo này mình đang đọc lại cuốn "The Pragmatic Programmer" (20th Anniversary Edition) của Andy Hunt và Dave Thomas — một trong những cuốn sách kinh điển mà bất kỳ lập trình viên nào cũng nên đọc ít nhất một lần trong đời. Mình quyết định viết blog cảm nhận từng topic như một series, vừa để ghi nhớ vừa để chia sẻ với mấy bạn.

Ảnh: Nemuel Sereti — Pexels

Topic 31. Inheritance Tax — "Cái thuế" kế thừa

Topic này nằm trong Chapter 5 — "Bend, or Break", nói về cách viết code linh hoạt, dễ thay đổi. Và cái đầu tiên mà tụi mình cần "bẻ gãy" chính là thói quen lạm dụng inheritance (kế thừa).

Mở đầu topic, tác giả trích dẫn một câu nói cực kỳ ấn tượng của Joe Armstrong (cha đẻ Erlang):

"You wanted a banana but what you got was a gorilla holding the banana and the entire jungle."

Ý ảnh muốn nói: mày chỉ muốn một trái chuối thôi, nhưng tao đưa mày cả con khỉ đang ôm trái chuối lẫn luôn cả khu rừng. Nghe quen không? Đó chính là cảm giác khi bạn subclass một class chỉ để dùng lại vài method, nhưng cuối cùng kéo theo cả đống method khác, dependency khác, behavior khác mà bạn không hề cần.

Lịch sử inheritance: Nó xuất hiện lần đầu trong Simula 67 (1969) như một giải pháp để xử lý nhiều kiểu event trên cùng một danh sách. Sau đó Smalltalk kế thừa ý tưởng và Alan Kay mô tả nó như một cách "differential programming" — cái này giống cái kia, chỉ khác vài chỗ. Hai trường phái này phát triển song song: Simula (C++, Java) dùng inheritance để kết hợp type, còn Smalltalk (Ruby, JavaScript) dùng để tổ chức behavior.

Vấn đề là — như tác giả chỉ ra — lập trình viên OOP ngày nay dùng inheritance chỉ vì hai lý do: không thích gõ nhiều (lười) hoặc thích type (sính phân loại). Cả hai đều có vấn đề.

Ảnh: luis gomes — Pexels

Vấn đề 1: Dùng inheritance để share code

Ví dụ kinh điển: bạn có class Vehicle với method move_at, rồi Car extends Vehicle. Code ở top-level gọi my_car.move_at(30) — mọi thứ đẹp đẽ. Nhưng một ngày đẹp trời, ông dev bảo trì class Vehicle quyết định đổi tên move_at thành set_velocity, và biến @speed thành @velocity.

Thế là Car vỡ. Code top-level vỡ. Tất cả đều vỡ dù họ chẳng động gì đến code của mình.

Đó là coupling. Inheritance là hình thức coupling mạnh nhất trong OOP. Nó không chỉ gắn child với parent, mà còn gắn child với grandparent, great-grandparent... Và code dùng child cũng bị gắn hết vào tất cả bọn họ.

Vấn đề 2: Dùng inheritance để xây dựng type hierarchy

Có một kiểu dev rất thích vẽ sơ đồ class hierarchy kiểu: Vehicle → Car, Bicycle → SportCar, MountainBike... Rồi họ hào hứng thêm layer này layer kia để biểu diễn từng nuance nhỏ nhất giữa các class.

Kết quả? Một bức tường giấy dán kín phòng với những "wall-covering monstrosities". Và khi change xảy ra, nó ripple lên xuống qua nhiều layer — làm app cực kỳ brittle (dễ vỡ).

Chưa kể multiple inheritance — nếu bạn muốn model một chiếc Car vừa là Vehicle, vừa là Asset, vừa là InsuredItem, LoanCollateral… thì sao? Đa kế thừa bị C++ làm cho mang tiếng xấu từ thập niên 90, và nhiều ngôn ngữ hiện đại bỏ hẳn tính năng này. Cho nên bạn chẳng thể model chính xác domain của mình được.

Ảnh: Alberlan Barros — Pexels

Ba giải pháp thay thế inheritance

Tác giả đưa ra 3 alternatives khiến bạn "không bao giờ cần dùng inheritance nữa":

1. Interfaces & Protocols (Tip 52: Prefer Interfaces To Express Polymorphism)

Interface khai báo contract — class nào implement interface đó thì phải có đủ methods. Bạn có thể dùng interface như type, và bất kỳ class nào implement interface đó đều compatible. CarPhone đều implement Locatable → bạn có thể bỏ cả hai vào List<Locatable> và gọi getLocation() mà không cần biết chúng là gì. Polymorphism mà không cần inheritance.

2. Delegation (Tip 53: Delegate to Services — Has-A Trumps Is-A)

Thay vì class Account extends PersistenceBaseClass (kéo theo 20 methods của framework persistence), bạn làm: Account có một @repo = Persister.for(self) và tự viết method save delegate cho repo. Giờ bạn kiểm soát hoàn toàn API của mình, không bị ràng buộc bởi framework.

3. Mixins & Traits (Tip 54: Use Mixins to Share Functionality)

Thay vì tạo class cha chứa validation, bạn viết mixin CommonFinders rồi with vào AccountRecord, OrderRecord. Đặc biệt, bạn có thể tạo các derived class "chuyên biệt" bằng mixin: AccountForCustomer extends Account with AccountValidations, AccountCustomerValidations. Code tự động áp dụng đúng validation tùy context. Hay không?

Cảm nhận của mình

Cá nhân mình thấy topic này rất "thấm". Hồi mới học Java ở đại học, mình từng nghĩ inheritance là thứ "quyền năng" nhất của OOP. Class nào cũng phải extends cái gì đó, không extends là thấy thiếu thiếu. Rồi mình từng làm việc với một dự án .NET mà base class có tới hơn 50 methods, đổ xuống hết cho mấy đứa con — không đứa nào xài hết, nhưng đứa nào cũng phải mang. Bảo trì là cực hình.

Sau này chuyển qua làm việc với Go, mình mới thực sự hiểu cái hay của composition over inheritance. Go không có inheritance (chỉ có embedding + interface), và điều đó buộc bạn phải nghĩ về thiết kế một cách khác. Bạn không thể lười được — bạn phải tự hỏi: "Cái này thực sự cần behavior gì? Interface nào?" Và kết quả là code ít coupling hơn, dễ test hơn, dễ thay đổi hơn rất nhiều.

Mình thấy Tip 54 — "Inheritance is Rarely the Answer" — không có nghĩa là cấm dùng inheritance tuyệt đối. Nó giống như cảnh báo: đừng với tay vào inheritance như một reflex. Hãy dừng lại, suy nghĩ, xem thử interfaces, delegation, hay mixins có giải quyết được vấn đề với ít coupling hơn không. 90% trường hợp câu trả lời là có.

Câu quote của Joe Armstrong mình sẽ nhớ mãi. Mỗi lần thấy mình chuẩn bị extends một class, mình sẽ tự hỏi: "Mày muốn trái chuối, hay mày muốn cả khu rừng?"

Kết

Topic 31 "Inheritance Tax" là một lời nhắc nhở mạnh mẽ rằng: inheritance là một dạng coupling, và coupling là kẻ thù của sự thay đổi. Trong chương "Bend, or Break" này, tác giả muốn chúng ta học cách viết code dẻo dai, không cứng nhắc. Và bước đầu tiên là bỏ thói quen lạm dụng inheritance.

Hãy nhớ 3 alternatives: Interfaces ↔ Polymorphism, Delegation ↔ Has-A trumps Is-A, Mixins ↔ Share functionality. Và đừng quên cái "thuế" inheritance bạn phải trả — không chỉ là runtime cost, mà là maintainability cost về lâu dài.

Hẹn mấy bạn ở topic sau nha! 🚀