"Thích thành phần hơn thừa kế" chỉ là một heuristic tốt
Bạn nên xem xét bối cảnh, vì đây không phải là một quy tắc phổ quát. Đừng coi đó là việc không bao giờ sử dụng tính kế thừa khi bạn có thể sáng tác. Nếu đó là trường hợp, bạn sẽ sửa nó bằng cách cấm thừa kế.
Tôi hy vọng sẽ làm cho điểm này được xóa dọc theo bài này.
Tôi sẽ không cố gắng bảo vệ giá trị của sáng tác. Mà tôi coi là lạc đề. Thay vào đó, tôi sẽ nói về một số tình huống khi nhà phát triển có thể xem xét kế thừa sẽ tốt hơn khi sử dụng bố cục. Trên lưu ý đó, thừa kế có giá trị riêng của nó, mà tôi cũng xem xét ngoài chủ đề.
Ví dụ xe
Tôi đang viết về các nhà phát triển, những người cố gắng làm mọi thứ theo cách ngu ngốc, cho mục đích tường thuật
Chúng ta hãy đi cho một biến thể của các ví dụ cổ điển rằng một số các khóa học OOP sử dụng ... Chúng tôi có một Vehicle
lớp, sau đó chúng tôi lấy được Car
, Airplane
, Balloon
và Ship
.
Lưu ý : Nếu bạn cần căn cứ vào ví dụ này, giả vờ đây là những loại đối tượng trong trò chơi video.
Sau đó Car
và Airplane
có thể có một số mã phổ biến, bởi vì cả hai đều có thể lăn trên bánh xe trên đất liền. Các nhà phát triển có thể xem xét việc tạo một lớp trung gian trong chuỗi thừa kế cho điều đó. Tuy nhiên, thực sự cũng có một số mã được chia sẻ giữa Airplane
và Balloon
. Họ có thể xem xét việc tạo một lớp trung gian khác trong chuỗi thừa kế cho điều đó.
Do đó, nhà phát triển sẽ tìm kiếm nhiều kế thừa. Tại thời điểm mà các nhà phát triển đang tìm kiếm nhiều kế thừa, thiết kế đã sai.
Tốt hơn là mô hình hóa hành vi này như các giao diện và thành phần, vì vậy chúng ta có thể sử dụng lại nó mà không cần phải chạy vào kế thừa nhiều lớp. Nếu các nhà phát triển, ví dụ, tạo FlyingVehicule
lớp. Họ sẽ nói rằng đó Airplane
là một FlyingVehicule
(kế thừa lớp), nhưng thay vào đó chúng ta có thể nói rằng nó Airplane
có một Flying
thành phần (thành phần) và Airplane
là một IFlyingVehicule
(kế thừa giao diện).
Sử dụng các giao diện, nếu cần thiết, chúng ta có thể có nhiều kế thừa (của các giao diện). Ngoài ra, bạn không khớp với một triển khai cụ thể. Tăng khả năng sử dụng lại và kiểm tra mã của bạn.
Hãy nhớ rằng sự kế thừa là một công cụ cho đa hình. Ngoài ra, đa hình là một công cụ để tái sử dụng. Nếu bạn có thể tăng khả năng sử dụng lại mã của mình bằng cách sử dụng thành phần, thì hãy làm như vậy. Nếu bạn không chắc chắn bất cứ điều gì hoặc không thành phần cung cấp khả năng tái sử dụng tốt hơn, "Ưu tiên thành phần hơn thừa kế" là một heuristic tốt.
Tất cả những điều đó mà không đề cập đến Amphibious
.
Trong thực tế, chúng ta có thể không cần những thứ đi lên khỏi mặt đất. Stephen Hurn có một ví dụ hùng hồn hơn trong các bài viết của mình về Ưu đãi Thành phần qua quyền thừa kế Phần 1 và phần 2 .
Thay thế và đóng gói
Nên A
kế thừa hay sáng tác B
?
Nếu A
Là một chuyên môn về B
điều đó phải đáp ứng nguyên tắc thay thế Liskov , sự kế thừa là khả thi, thậm chí là mong muốn. Nếu có những tình huống A
không phải là sự thay thế hợp lệ B
thì chúng ta không nên sử dụng tính kế thừa.
Chúng ta có thể quan tâm đến sáng tác như một hình thức lập trình phòng thủ, để bảo vệ lớp dẫn xuất . Đặc biệt, một khi bạn bắt đầu sử dụng B
cho các mục đích khác nhau, có thể có áp lực phải thay đổi hoặc mở rộng nó để phù hợp hơn cho các mục đích đó. Nếu có rủi ro B
có thể làm lộ ra các phương pháp có thể dẫn đến trạng thái không hợp lệ, A
chúng ta nên sử dụng thành phần thay vì kế thừa. Ngay cả khi chúng tôi là tác giả của cả hai B
và A
, đó là một điều ít phải lo lắng hơn, do đó thành phần làm giảm khả năng tái sử dụng B
.
Chúng tôi thậm chí có thể lập luận rằng nếu có những tính năng trong B
đó A
không cần (và chúng tôi không biết nếu những tính năng có thể dẫn đến một trạng thái không hợp lệ cho A
, hoặc là trong việc thực hiện hiện tại hay trong tương lai), nó là một ý tưởng tốt để thành phần sử dụng thay vì thừa kế.
Thành phần cũng có những lợi thế của việc cho phép thực hiện chuyển đổi và giảm bớt chế độ chế nhạo.
Lưu ý : có những tình huống chúng tôi muốn sử dụng thành phần mặc dù sự thay thế là hợp lệ. Chúng tôi lưu trữ tính thay thế đó bằng cách sử dụng các giao diện hoặc các lớp trừu tượng (cái nào sẽ sử dụng khi là chủ đề khác) và sau đó sử dụng thành phần với nội dung phụ thuộc của việc triển khai thực tế.
Cuối cùng, tất nhiên, có lập luận rằng chúng ta nên sử dụng thành phần để bảo vệ lớp cha vì sự kế thừa phá vỡ sự đóng gói của lớp cha:
Kế thừa cho thấy một lớp con với các chi tiết về việc thực hiện của cha mẹ nó, người ta thường nói rằng 'thừa kế phá vỡ đóng gói'
- Các mẫu thiết kế: Các yếu tố của phần mềm hướng đối tượng có thể tái sử dụng, Gang of Four
Vâng, đó là một lớp cha mẹ được thiết kế kém. Đó là lý do tại sao bạn nên:
Thiết kế để thừa kế, hoặc cấm nó.
- Java hiệu quả, Josh Bloch
Vấn đề Yo-Yo
Một trường hợp khác mà thành phần giúp là vấn đề Yo-Yo . Đây là một trích dẫn từ Wikipedia:
Trong phát triển phần mềm, vấn đề yo-yo là một kiểu chống xảy ra khi lập trình viên phải đọc và hiểu một chương trình có biểu đồ kế thừa quá dài và phức tạp đến nỗi lập trình viên phải tiếp tục lật giữa nhiều định nghĩa lớp khác nhau để tuân theo dòng điều khiển của chương trình.
Bạn có thể giải quyết, ví dụ: Lớp của bạn C
sẽ không kế thừa từ lớp B
. Thay vào đó, lớp của bạn C
sẽ có một thành viên của loại A
, có thể có hoặc không (hoặc có) một đối tượng của loại B
. Bằng cách này, bạn sẽ không được lập trình dựa trên chi tiết triển khai B
, mà trái với hợp đồng mà giao diện (của) A
cung cấp.
Ví dụ truy cập
Nhiều khung ủng hộ sự kế thừa so với thành phần (điều này trái ngược với những gì chúng ta đã tranh luận). Nhà phát triển có thể làm điều này bởi vì họ đặt rất nhiều công việc vào lớp cơ sở của họ rằng việc triển khai nó với thành phần sẽ làm tăng kích thước của mã máy khách. Đôi khi điều này là do những hạn chế của ngôn ngữ.
Ví dụ, khung công tác ORM của PHP có thể tạo một lớp cơ sở sử dụng các phương thức ma thuật để cho phép viết mã như thể đối tượng có các thuộc tính thực. Thay vào đó, mã được xử lý bởi các phương thức ma thuật sẽ được chuyển đến cơ sở dữ liệu, truy vấn cho trường được yêu cầu cụ thể (có thể lưu vào bộ đệm cho yêu cầu trong tương lai) và trả lại. Làm điều đó với thành phần sẽ yêu cầu khách hàng tạo các thuộc tính cho từng trường hoặc viết một số phiên bản của mã phương thức ma thuật.
Phụ lục : Có một số cách khác người ta có thể cho phép mở rộng các đối tượng ORM. Vì vậy, tôi không nghĩ rằng thừa kế là cần thiết trong trường hợp này. Nó rẻ hơn.
Đối với một ví dụ khác, công cụ trò chơi video có thể tạo một lớp cơ sở sử dụng mã gốc tùy thuộc vào nền tảng đích để thực hiện kết xuất 3D và xử lý sự kiện. Mã này là phức tạp và nền tảng cụ thể. Nó sẽ là tốn kém và dễ bị lỗi cho người sử dụng nhà phát triển của công cụ để xử lý mã này, trên thực tế, đó là một phần lý do của việc sử dụng công cụ.
Hơn nữa, không có phần kết xuất 3D, đây là cách hoạt động của nhiều khung công cụ. Điều này giúp bạn không phải lo lắng về việc xử lý các thông báo hệ điều hành trên thực tế, trong nhiều ngôn ngữ, bạn không thể viết mã như vậy mà không có một số hình thức tạm thời riêng. Hơn nữa, nếu bạn làm điều đó, nó sẽ thắt chặt tính di động của bạn. Thay vào đó, với sự kế thừa, miễn là nhà phát triển không phá vỡ tính tương thích (quá nhiều); bạn sẽ có thể dễ dàng chuyển mã của mình sang bất kỳ nền tảng mới nào mà họ hỗ trợ trong tương lai.
Ngoài ra, hãy xem xét rằng nhiều lần chúng tôi chỉ muốn ghi đè một vài phương thức và để mọi thứ khác với các cài đặt mặc định. Nếu chúng ta đang sử dụng thành phần, chúng ta sẽ phải tạo ra tất cả các phương thức đó, ngay cả khi chỉ ủy thác cho đối tượng được bao bọc.
Theo lập luận này, có một điểm mà thành phần có thể tệ nhất đối với khả năng bảo trì hơn là kế thừa (khi lớp cơ sở quá phức tạp). Tuy nhiên, hãy nhớ rằng khả năng duy trì của thừa kế có thể tồi tệ hơn so với thành phần (khi cây thừa kế quá phức tạp), đó là những gì tôi đề cập đến trong vấn đề yo-yo.
Trong các ví dụ được trình bày, các nhà phát triển hiếm khi có ý định sử dụng lại mã được tạo thông qua kế thừa trong các dự án khác. Điều đó làm giảm khả năng tái sử dụng giảm dần của việc sử dụng thừa kế thay vì thành phần. Ngoài ra, bằng cách sử dụng tính kế thừa, các nhà phát triển khung có thể cung cấp rất nhiều mã dễ sử dụng và dễ khám phá.
Suy nghĩ cuối cùng
Như bạn có thể thấy, sáng tác có một số lợi thế so với thừa kế trong một số tình huống, không phải lúc nào cũng vậy. Điều quan trọng là phải xem xét bối cảnh và các yếu tố khác nhau có liên quan (như khả năng sử dụng lại, khả năng bảo trì, khả năng kiểm tra, vv.) Để đưa ra quyết định. Về điểm thứ nhất: "thích thành phần trên thừa kế" là một chỉ heuristic tốt.
Bạn cũng có thể có thông báo rằng nhiều tình huống tôi mô tả có thể được giải quyết bằng Traits hoặc Mixins ở một mức độ nào đó. Đáng buồn thay, đây không phải là những tính năng phổ biến trong danh sách các ngôn ngữ tuyệt vời và chúng thường đi kèm với một số chi phí hiệu suất. Rất may, Phương pháp mở rộng anh em họ phổ biến của họ và Triển khai mặc định giảm thiểu một số tình huống.
Tôi có một bài đăng gần đây nơi tôi nói về một số lợi thế của giao diện tại sao chúng tôi yêu cầu giao diện giữa truy cập UI, Business và Data trong C # . Nó giúp tách rời và giảm bớt khả năng sử dụng lại và kiểm tra, bạn có thể quan tâm.