Kế thừa so với tập hợp [đóng]


151

Có hai trường phái suy nghĩ về cách mở rộng, tăng cường và tái sử dụng mã tốt nhất trong một hệ thống hướng đối tượng:

  1. Kế thừa: mở rộng chức năng của một lớp bằng cách tạo một lớp con. Ghi đè các thành viên siêu lớp trong các lớp con để cung cấp chức năng mới. Tạo các phương thức trừu tượng / ảo để buộc các lớp con phải "điền vào chỗ trống" khi siêu lớp muốn có một giao diện cụ thể nhưng không rõ ràng về việc triển khai nó.

  2. Tập hợp: tạo chức năng mới bằng cách lấy các lớp khác và kết hợp chúng thành một lớp mới. Đính kèm một giao diện chung cho lớp mới này để có khả năng tương tác với mã khác.

Lợi ích, chi phí và hậu quả của mỗi là gì? Có những lựa chọn thay thế khác?

Tôi thấy cuộc tranh luận này diễn ra thường xuyên, nhưng tôi không nghĩ nó đã được hỏi về Stack Overflow chưa (mặc dù có một số cuộc thảo luận liên quan). Cũng có một sự thiếu sót đáng ngạc nhiên về kết quả tốt của Google cho nó.


9
Câu hỏi hay, tiếc là tôi không có đủ thời gian.
Toon Krijthe

4
Một câu trả lời tốt sẽ tốt hơn câu trả lời nhanh hơn ... Tôi sẽ xem câu hỏi của riêng tôi vì vậy tôi sẽ bỏ phiếu cho bạn ít nhất :-P
Craig Walker

Câu trả lời:


181

Đây không phải là vấn đề tốt nhất, mà là khi nào nên sử dụng cái gì.

Trong trường hợp 'bình thường', một câu hỏi đơn giản là đủ để tìm hiểu xem chúng ta có cần thừa kế hoặc tổng hợp hay không.

  • Nếu Lớp mới nhiều hơn hoặc ít hơn như lớp gốc. Sử dụng thừa kế. Lớp mới bây giờ là một lớp con của lớp ban đầu.
  • Nếu lớp mới phải lớp gốc. Sử dụng tổng hợp. Lớp mới hiện có lớp ban đầu là thành viên.

Tuy nhiên, có một khu vực màu xám lớn. Vì vậy, chúng tôi cần một số thủ thuật khác.

  • Nếu chúng tôi đã sử dụng tính kế thừa (hoặc chúng tôi dự định sử dụng nó) nhưng chúng tôi chỉ sử dụng một phần của giao diện hoặc chúng tôi buộc phải ghi đè rất nhiều chức năng để giữ cho mối tương quan hợp lý. Sau đó, chúng ta có một mùi khó chịu lớn cho thấy rằng chúng ta phải sử dụng tổng hợp.
  • Nếu chúng tôi đã sử dụng tổng hợp (hoặc chúng tôi dự định sử dụng nó) nhưng chúng tôi thấy rằng chúng tôi cần sao chép gần như tất cả các chức năng. Sau đó, chúng ta có một mùi chỉ theo hướng thừa kế.

Để cắt nó ngắn. Chúng ta nên sử dụng tổng hợp nếu một phần của giao diện không được sử dụng hoặc phải thay đổi để tránh tình huống phi logic. Chúng ta chỉ cần sử dụng tính kế thừa, nếu chúng ta cần gần như tất cả các chức năng mà không có thay đổi lớn. Và khi nghi ngờ, hãy sử dụng Uẩn.

Một khả năng khác, trong trường hợp chúng ta có một lớp cần một phần chức năng của lớp gốc, là tách lớp gốc trong lớp gốc và lớp phụ. Và để cho lớp mới kế thừa từ lớp gốc. Nhưng bạn nên cẩn thận với điều này, không tạo ra một sự tách biệt phi logic.

Hãy thêm một ví dụ. Chúng tôi có một lớp 'Chó' với các phương pháp: 'Ăn', 'Đi bộ', 'Bark', 'Chơi'.

class Dog
  Eat;
  Walk;
  Bark;
  Play;
end;

Bây giờ chúng ta cần một lớp 'Mèo', cần 'Ăn', 'Đi bộ', 'Purr' và 'Chơi'. Vì vậy, trước tiên hãy cố gắng mở rộng nó từ một con chó.

class Cat is Dog
  Purr; 
end;

Có vẻ, ổn, nhưng chờ đợi. Con mèo này có thể Bark (Những người yêu mèo sẽ giết tôi vì điều đó). Và một con mèo sủa vi phạm các nguyên tắc của vũ trụ. Vì vậy, chúng ta cần ghi đè phương thức Bark để nó không làm gì cả.

class Cat is Dog
  Purr; 
  Bark = null;
end;

Ok, nó hoạt động, nhưng nó có mùi hôi. Vì vậy, hãy thử tổng hợp:

class Cat
  has Dog;
  Eat = Dog.Eat;
  Walk = Dog.Walk;
  Play = Dog.Play;
  Purr;
end;

Ok, điều này là tốt đẹp. Con mèo này không sủa nữa, thậm chí không im lặng. Nhưng nó vẫn có một con chó nội bộ muốn ra ngoài. Vì vậy, hãy thử giải pháp số ba:

class Pet
  Eat;
  Walk;
  Play;
end;

class Dog is Pet
  Bark;
end;

class Cat is Pet
  Purr;
end;

Điều này là sạch sẽ hơn nhiều. Không có chó nội. Và mèo và chó ở cùng cấp độ. Chúng tôi thậm chí có thể giới thiệu các vật nuôi khác để mở rộng mô hình. Trừ khi nó là một con cá, hoặc một cái gì đó không đi bộ. Trong trường hợp đó chúng ta lại cần tái cấu trúc. Nhưng đó là một cái gì đó cho một thời gian khác.


5
"Tái sử dụng gần như tất cả các chức năng của một lớp" là lần duy nhất tôi thực sự thích kế thừa. Điều tôi thực sự thích là một ngôn ngữ có khả năng dễ dàng nói "ủy thác cho đối tượng tổng hợp này cho các phương thức cụ thể này"; đó là điều tốt nhất của cả hai thế giới.
Craig Walker

Tôi không chắc chắn về các ngôn ngữ khác, nhưng Delphi có một cơ chế, cho phép các thành viên thực hiện một phần của giao diện.
Toon Krijthe

2
Mục tiêu C sử dụng các Giao thức cho phép bạn quyết định xem chức năng của bạn là bắt buộc hay tùy chọn.
cantfindaname88

1
Câu trả lời tuyệt vời với một ví dụ thực tế. Tôi sẽ thiết kế lớp Pet với một phương thức được gọi makeSoundvà để mỗi lớp con thực hiện loại âm thanh của riêng chúng. Điều này sau đó sẽ giúp trong trường hợp bạn có nhiều vật nuôi và chỉ cần làm for_each pet in the list of pets, pet.makeSound.
blongho


27

Sự khác biệt thường được biểu thị bằng sự khác biệt giữa "là" và "có". Kế thừa, mối quan hệ "là một", được tóm tắt độc đáo trong Nguyên tắc thay thế Liskov . Tập hợp, mối quan hệ "có một", chỉ có thế - nó cho thấy đối tượng tổng hợp một trong các đối tượng tổng hợp.

Sự khác biệt hơn nữa tồn tại cũng như thừa kế riêng tư trong C ++ cho thấy mối quan hệ "được thực hiện theo quan hệ", cũng có thể được mô hình hóa bằng cách tổng hợp các đối tượng thành viên (không tiếp xúc).


Ngoài ra, sự khác biệt được tóm tắt độc đáo trong chính câu hỏi mà bạn có nghĩa vụ phải trả lời. Tôi muốn mọi người đọc câu hỏi trước khi bắt đầu dịch. Do yo mù quáng thúc đẩy patters thiết kế để trở thành một thành viên của câu lạc bộ ưu tú?
Val

Tôi không thích cách tiếp cận này, nó thiên về sáng tác
Alireza Rahmani Khalili

15

Đây là đối số phổ biến nhất của tôi:

Trong bất kỳ hệ thống hướng đối tượng nào, có hai phần cho bất kỳ lớp nào:

  1. Giao diện của nó : "khuôn mặt chung" của đối tượng. Đây là tập hợp các khả năng mà nó công bố với phần còn lại của thế giới. Trong rất nhiều ngôn ngữ, tập hợp được xác định rõ thành một "lớp". Thông thường đây là các chữ ký phương thức của đối tượng, mặc dù nó thay đổi một chút theo ngôn ngữ.

  2. Việc thực hiện của nó : công việc "đằng sau hậu trường" mà đối tượng thực hiện để đáp ứng giao diện của nó và cung cấp chức năng. Đây thường là mã và dữ liệu thành viên của đối tượng.

Một trong những nguyên tắc cơ bản của OOP là việc triển khai được gói gọn (nghĩa là: ẩn) trong lớp; Điều duy nhất mà người ngoài nên nhìn thấy là giao diện.

Khi một lớp con kế thừa từ một lớp con, nó thường kế thừa cả việc thực hiện và giao diện. Đến lượt mình, điều này có nghĩa là bạn buộc phải chấp nhận cả hai như những ràng buộc đối với lớp học của mình.

Với tổng hợp, bạn có thể chọn triển khai hoặc giao diện hoặc cả hai - nhưng bạn không bị ép buộc. Chức năng của một đối tượng được để lại cho chính đối tượng đó. Nó có thể trì hoãn các đối tượng khác tùy thích, nhưng cuối cùng nó tự chịu trách nhiệm. Theo kinh nghiệm của tôi, điều này dẫn đến một hệ thống linh hoạt hơn: một hệ thống dễ sửa đổi hơn.

Vì vậy, bất cứ khi nào tôi phát triển phần mềm hướng đối tượng, tôi hầu như luôn thích tổng hợp hơn kế thừa.


Tôi sẽ nghĩ rằng bạn sử dụng một lớp trừu tượng để xác định một giao diện và sau đó sử dụng kế thừa trực tiếp cho mỗi lớp triển khai cụ thể (làm cho nó khá nông). Bất kỳ tập hợp là dưới thực hiện cụ thể.
orcmid

Nếu bạn cần các giao diện bổ sung hơn thế, hãy trả lại chúng thông qua các phương thức trên giao diện của giao diện trừu tượng hóa cấp chính, rửa lại lặp lại.
orcmid

Bạn có nghĩ rằng, tổng hợp là tốt hơn trong kịch bản này? Một cuốn sách là SellItem, DigitalDisc là một codereview của SelliingItem.stackexchange.com/questions/14077/ Kẻ
LCJ

11

Tôi đã đưa ra câu trả lời cho "Is a" vs "Has a": cái nào tốt hơn? .

Về cơ bản, tôi đồng ý với những người khác: chỉ sử dụng tính kế thừa nếu lớp dẫn xuất của bạn thực sự loại bạn đang mở rộng, không chỉ vì nó chứa cùng một dữ liệu. Hãy nhớ rằng kế thừa có nghĩa là lớp con đạt được các phương thức cũng như dữ liệu.

Liệu nó có ý nghĩa cho lớp dẫn xuất của bạn có tất cả các phương thức của siêu lớp không? Hay bạn chỉ lặng lẽ tự hứa với bản thân rằng những phương thức đó nên được bỏ qua trong lớp dẫn xuất? Hoặc bạn có thấy mình ghi đè các phương thức từ siêu lớp, biến chúng thành các hàm không để không ai gọi chúng là vô tình? Hoặc đưa ra gợi ý cho công cụ tạo tài liệu API của bạn để bỏ qua phương thức từ tài liệu?

Đó là những manh mối mạnh mẽ rằng tập hợp là sự lựa chọn tốt hơn trong trường hợp đó.


6

Tôi thấy rất nhiều câu trả lời "is-a vs. has-a; chúng khác nhau về mặt khái niệm" về câu hỏi này và các câu hỏi liên quan.

Một điều tôi đã tìm thấy trong trải nghiệm của mình là cố gắng xác định liệu một mối quan hệ là "is-a" hay "has-a" chắc chắn sẽ thất bại. Ngay cả khi bạn có thể đưa ra quyết định chính xác cho các đối tượng ngay bây giờ, việc thay đổi các yêu cầu có nghĩa là bạn có thể sẽ sai ở một thời điểm nào đó trong tương lai.

Một điều khác tôi đã tìm thấy là rất khó để chuyển đổi từ thừa kế sang tổng hợp một khi có rất nhiều mã được viết xung quanh hệ thống phân cấp thừa kế. Chỉ cần chuyển từ siêu lớp sang giao diện có nghĩa là thay đổi gần như mọi lớp con trong hệ thống.

Và, như tôi đã đề cập ở những nơi khác trong bài viết này, tập hợp có xu hướng kém linh hoạt hơn so với thừa kế.

Vì vậy, bạn có một cơn bão tranh luận hoàn hảo chống lại sự kế thừa bất cứ khi nào bạn phải chọn cái này hay cái khác:

  1. Sự lựa chọn của bạn có thể sẽ là sai ở một số điểm
  2. Thay đổi lựa chọn đó là khó khăn một khi bạn đã thực hiện nó.
  3. Kế thừa có xu hướng là một lựa chọn tồi tệ hơn vì nó hạn chế hơn.

Vì vậy, tôi có xu hướng chọn tập hợp - ngay cả khi có mối quan hệ mạnh mẽ là một mối quan hệ.


Yêu cầu phát triển có nghĩa là thiết kế OO của bạn chắc chắn sẽ sai. Kế thừa so với tập hợp chỉ là phần nổi của tảng băng đó. Bạn không thể là kiến ​​trúc sư cho tất cả các tương lai có thể, vì vậy hãy thực hiện ý tưởng XP: giải quyết các yêu cầu ngày nay và chấp nhận rằng bạn có thể phải cấu trúc lại.
Bill Karwin

Tôi thích dòng điều tra và đặt câu hỏi này. Quan tâm về blurrung là-a, has-a và sử dụng-a mặc dù. Tôi bắt đầu với các giao diện (cùng với việc xác định các tóm tắt được triển khai mà tôi muốn giao diện). Mức độ bổ sung của vô định là vô giá. Đừng theo dõi kết luận tổng hợp của bạn.
orcmid

Đó là khá nhiều quan điểm của tôi mặc dù. Cả thừa kế và tổng hợp đều là phương pháp để giải quyết vấn đề. Một trong những phương pháp đó phải chịu tất cả các loại hình phạt khi bạn phải giải quyết vấn đề vào ngày mai.
Craig Walker

Trong tâm trí của tôi, các giao diện tổng hợp + là sự thay thế cho kế thừa / phân lớp; bạn không thể thực hiện đa hình chỉ dựa trên tập hợp.
Craig Walker

3

Câu hỏi thường được đặt ra là Thành phần so với Kế thừa và nó đã được hỏi ở đây trước đây.


Đúng là bạn .. buồn cười là SO đã không đưa ra một trong những câu hỏi liên quan.
Craig Walker

2
Tìm kiếm SO vẫn hút thời gian lớn
Vinko Vrsalovic

Đã đồng ý. Đó là lý do tại sao tôi không đóng cái này. Điều đó, và rất nhiều người đã trả lời ở đây.
Bill Lizard

1
SO cần một tính năng để cuộn 2 câu hỏi (và câu trả lời của họ) thành 1
Craig Walker

3

Tôi muốn làm cho nhận xét này về câu hỏi ban đầu, nhưng 300 ký tự cắn [; <).

Tôi nghĩ rằng chúng ta cần phải cẩn thận. Đầu tiên, có nhiều hương vị hơn hai ví dụ khá cụ thể được đưa ra trong câu hỏi.

Ngoài ra, tôi đề nghị rằng không nên nhầm lẫn mục tiêu với nhạc cụ. Người ta muốn đảm bảo rằng kỹ thuật hoặc phương pháp được chọn hỗ trợ đạt được mục tiêu chính, nhưng tôi không nghĩ rằng cuộc thảo luận tốt nhất về kỹ thuật là rất hữu ích. Nó giúp để biết những cạm bẫy của các phương pháp khác nhau cùng với các điểm ngọt rõ ràng của chúng.

Ví dụ, bạn đang chuẩn bị những gì để hoàn thành, bạn có sẵn những gì để bắt đầu và những hạn chế là gì?

Bạn đang tạo một khung thành phần, thậm chí là một mục đích đặc biệt? Các giao diện có thể tách rời khỏi các triển khai trong hệ thống lập trình hay nó được thực hiện bằng cách sử dụng một loại công nghệ khác? Bạn có thể tách cấu trúc kế thừa của các giao diện (nếu có) khỏi cấu trúc kế thừa của các lớp thực hiện chúng không? Có quan trọng để ẩn cấu trúc lớp của một triển khai khỏi mã dựa trên các giao diện mà việc triển khai thực hiện không? Có nhiều triển khai để có thể sử dụng cùng một lúc hay là sự biến đổi theo thời gian nhiều hơn do hậu quả của việc bảo trì và nâng cao? Điều này và nhiều hơn nữa cần được xem xét trước khi bạn sửa chữa trên một công cụ hoặc một phương pháp.

Cuối cùng, điều quan trọng là phải khóa sự khác biệt trong sự trừu tượng và cách bạn nghĩ về nó (như trong một so với có-a) đối với các tính năng khác nhau của công nghệ OO? Có lẽ vì vậy, nếu nó giữ cấu trúc khái niệm nhất quán và quản lý được cho bạn và những người khác. Nhưng thật khôn ngoan khi không bị nô lệ bởi điều đó và những mâu thuẫn mà bạn có thể sẽ tạo ra. Có lẽ tốt nhất là nên lùi lại một cấp độ và không quá cứng nhắc (nhưng hãy để lại lời tường thuật tốt để những người khác có thể nói lên điều gì). [Tôi tìm kiếm những gì làm cho một phần cụ thể của chương trình có thể giải thích được, nhưng đôi khi tôi đi tìm sự thanh lịch khi có một chiến thắng lớn hơn. Không phải lúc nào cũng là ý tưởng tốt nhất.]

Tôi là một người theo chủ nghĩa giao diện và tôi bị cuốn hút vào các loại vấn đề và cách tiếp cận trong đó tính thuần túy của giao diện là phù hợp, cho dù xây dựng khung Java hay tổ chức một số triển khai COM. Điều đó không làm cho nó phù hợp với mọi thứ, thậm chí không gần với mọi thứ, mặc dù tôi đã thề với nó. (Tôi có một vài dự án dường như cung cấp các ví dụ phản biện nghiêm trọng chống lại chủ nghĩa thuần túy giao diện, vì vậy sẽ rất thú vị khi xem cách tôi quản lý để đối phó.)


2

Tôi sẽ đề cập đến phần có thể áp dụng. Đây là một ví dụ về cả hai, trong một kịch bản trò chơi. Giả sử, có một trò chơi có nhiều loại lính khác nhau. Mỗi người lính có thể có một chiếc ba lô có thể chứa những thứ khác nhau.

Kế thừa ở đây? Có một biển, mũ nồi xanh và một tay bắn tỉa. Đây là những loại lính. Vì vậy, có một Lính lớp cơ sở với Marine, Beret xanh & Sniper là những lớp xuất phát

Tập hợp ở đây? Chiếc ba lô có thể chứa lựu đạn, súng (các loại khác nhau), dao, medikit, v.v ... Một người lính có thể được trang bị bất kỳ thứ gì trong số này tại bất kỳ thời điểm nào, ngoài ra anh ta cũng có thể có áo chống đạn hoạt động như áo giáp khi bị tấn công và chấn thương giảm đến một tỷ lệ nhất định. Lớp lính chứa một đối tượng của lớp áo chống đạn và lớp ba lô có chứa các tham chiếu đến các vật phẩm này.


2

Tôi nghĩ rằng đó không phải là một hoặc / hoặc tranh luận. Chỉ là:

  1. mối quan hệ is-a (thừa kế) xảy ra ít thường xuyên hơn mối quan hệ has-a (thành phần).
  2. Kế thừa là khó hơn để có được quyền, ngay cả khi nó phù hợp để sử dụng nó, do đó cần phải siêng năng vì nó có thể phá vỡ đóng gói, khuyến khích khớp nối chặt chẽ bằng cách phơi bày việc thực hiện và vv.

Cả hai đều có vị trí của họ, nhưng thừa kế là rủi ro hơn.

Mặc dù tất nhiên sẽ không có ý nghĩa gì khi có một lớp Shape 'có - một' Điểm và một lớp Square. Ở đây thừa kế là do.

Mọi người có xu hướng nghĩ về sự kế thừa trước tiên khi cố gắng thiết kế một cái gì đó mở rộng, đó là những gì sai.


1

Ưu đãi xảy ra khi cả hai ứng cử viên đủ điều kiện. A và B là các tùy chọn và bạn ủng hộ A. Lý do là thành phần cung cấp nhiều khả năng mở rộng / linh hoạt hơn so với khái quát hóa. Phần mở rộng / flexiblity này chủ yếu đề cập đến thời gian chạy / tính linh hoạt động.

Lợi ích không thể nhìn thấy ngay lập tức. Để thấy được lợi ích, bạn cần đợi yêu cầu thay đổi bất ngờ tiếp theo. Vì vậy, trong hầu hết các trường hợp, những người gắn bó với việc khái quát hóa đều thất bại khi so sánh với những người chấp nhận sáng tác (trừ một trường hợp rõ ràng được đề cập sau). Do đó quy tắc. Từ quan điểm học tập nếu bạn có thể thực hiện tiêm phụ thuộc thành công thì bạn nên biết nên ưu tiên cái nào và khi nào. Quy tắc giúp bạn trong việc đưa ra quyết định là tốt; nếu bạn không chắc chắn thì chọn thành phần.

Tóm tắt: Thành phần: Khớp nối được giảm bằng cách chỉ cần một số thứ nhỏ hơn bạn cắm vào một cái gì đó lớn hơn và đối tượng lớn hơn chỉ gọi lại đối tượng nhỏ hơn. Tạo ra: Từ quan điểm API xác định rằng một phương thức có thể bị ghi đè là một cam kết mạnh mẽ hơn so với việc xác định rằng một phương thức có thể được gọi. (rất ít lần xuất hiện khi Tổng quát hóa thắng). Và đừng bao giờ quên rằng với thành phần bạn cũng đang sử dụng tính kế thừa, từ một giao diện thay vì một lớp lớn


0

Cả hai phương pháp đều được sử dụng để giải quyết các vấn đề khác nhau. Bạn không phải luôn cần tổng hợp trên hai hoặc nhiều lớp khi kế thừa từ một lớp.

Đôi khi bạn phải tổng hợp một lớp duy nhất vì lớp đó bị niêm phong hoặc có các thành viên không phải là ảo mà bạn cần chặn để bạn tạo một lớp proxy rõ ràng là không hợp lệ về mặt kế thừa nhưng miễn là lớp bạn đang ủy quyền có một giao diện bạn có thể đăng ký để có thể hoạt động khá tốt.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.