2008-11-06 29 views
123

Có hai trường phái tư tưởng về cách mở rộng tốt nhất, nâng cao, và tái sử dụng mã trong một hệ thống hướng đối tượng:Inheritance vs Aggregation

  1. Inheritance: mở rộng chức năng của một lớp bằng cách tạo ra một lớp con . Ghi đè lên 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 abstract/virtual để buộc các lớp con thành "fill-in-the-blanks" khi lớp cha muốn một giao diện cụ thể nhưng không thuyết phục về việc triển khai nó.

  2. Kết hợp: tạo chức năng mới bằng cách tham gia các lớp khác và kết hợp chúng vào 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 loại là gì? Có lựa chọn thay thế nào khác không?

Tôi thấy cuộc tranh luận này xuất hiện thường xuyên, nhưng tôi không nghĩ rằng nó đã được yêu cầu trên Stack Overflow (mặc dù có một số thảo luận liên quan). Ngoài ra còn có một thiếu đáng ngạc nhiên của kết quả tốt của Google cho nó.

+5

Câu hỏi hay, rất tiếc là hiện tại tôi không có đủ thời gian. –

+3

Câu trả lời hay hơn là 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ầu cho bạn ít nhất :-P –

+1

Nó được mở rộng ngay bây giờ ;-). –

Trả lời

146

Nó không phải là một vấn đề trong số đó là tốt nhất, nhưng khi sử dụng những 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ế hay tổng hợp hay không.

  • Nếu lớp mới nhiều hơn hoặc ít hơn với 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 gốc.
  • Nếu lớp mới phải lớp gốc. Sử dụng tổng hợp. Lớp học mới bây giờ là lớp học ban đầu với tư cách là một 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 thừa kế (hoặc chúng tôi dự định sử dụng) 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ữ mối tương quan logic. Sau đó, chúng tôi có một mùi khó chịu lớn cho thấy rằng chúng tôi đã phải sử dụng tập 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 tìm ra chúng tôi cần phải sao chép gần như tất cả các chức năng. Sau đó, chúng tôi có một mùi mà điểm theo hướng thừa kế.

Để cắt ngắn. Chúng ta nên sử dụng tập hợp nếu một phần của giao diện không được sử dụng hoặc phải được thay đổi để tránh tình huống phi logic. Chúng ta chỉ cần sử dụng thừa kế, 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 Aggregation.

Một khả năng khác, trường hợp chúng tôi có một lớp cần một phần chức năng của lớp gốc, là chia lớp gốc thành lớp gốc và lớp con. 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 phải để tạo ra sự tách biệt phi logic.

Cho phép thêm ví dụ. Chúng tôi có một lớp 'Chó' với các phương pháp: 'Ăn', 'Đi bộ', 'Vỏ cây', '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à 'Phát'. Vì vậy, trước tiên hãy thử mở rộng nó từ một con chó.

class Cat is Dog 
    Purr; 
end; 

Có vẻ, được rồi, nhưng chờ đã. 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 sủa vi phạm các nguyên tắc của vũ trụ. Vì vậy, chúng ta cần phải ghi đè lên phương pháp Bark để nó không có gì.

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

Ok, công trình này, nhưng nó có mùi xấu. Vì vậy, hãy thử tập hợp:

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

Ok, điều này thật tuyệt. 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 sạch hơn nhiều. Không có chó nội bộ. Và chó và mèo ở 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á, hay cái gì đó không đi. Trong trường hợp đó, chúng ta lại cần tái cấu trúc lại. Nhưng đó là một cái gì đó cho một thời gian khác.

+4

Việc "sử dụng lại gần như tất cả các chức năng của một lớp" là một lần tôi thực sự thích thừa kế. Những gì tôi muốn * thực sự * thích là một ngôn ngữ có khả năng dễ dàng nói "đại biểu cho đối tượng tổng hợp này cho các phương pháp cụ thể"; đó là điều tốt nhất của cả hai thế giới. –

+0

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. –

+2

Mục tiêu C sử dụng 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

26

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

Sự phân biệt khác tồn tại cũng như thừa kế riêng trong C++ cho biết 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ập hợp các đối tượng thành viên (không tiếp xúc).

+0

Ngoài ra, sự khác biệt được tóm tắt một cách độc đáo trong chính câu hỏi mà bạn 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 quảng bá thiết kế patters để trở thành một thành viên của câu lạc bộ ưu tú? – Val

33

Vào đầu GOF họ nêu

thành phần đối tượng Favor trên lớp kế thừa.

này là tiếp tục thảo luận here

1

Tôi sẽ đề cập đến phần có thể áp dụng ở đâu. Đây là một ví dụ của 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 binh lính khác nhau. Mỗi người lính có thể có một chiếc ba lô có thể chứa nhiều thứ khác nhau.

Thừa kế ở đây? Có một cái mũ biển, màu xanh lá cây & một tay bắn tỉa. Đây là những loại lính. Vì vậy, có một lớp cơ sở Soldier với Marine, Green Beret & Sniper dưới dạng lớp bắt nguồn

Tổng hợp ở đây? Ba lô có thể chứa lựu đạn, súng (loại khác nhau), dao, medikit, vv Một người lính có thể được trang bị bất kỳ cái nào tại bất kỳ thời điểm nào, cộng với áo giáp chống đạn và chấn thương của anh ta giảm xuống một tỷ lệ nhất định. Lớp lính có chứa một đối tượng của lớp vest chống đạn và lớp knapsack có chứa các tham chiếu đến các mục này.

0

Cả hai cách tiếp cận được sử dụng để giải quyết các vấn đề khác nhau. Bạn không cần phải tổng hợp hơ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 đơn vì lớp đó được niêm phong hoặc có các thành viên khác không phải ả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ề thừa kế nhưng miễn là lớp bạn đang proxy có một giao diện bạn có thể đăng ký để có thể làm việc khá tốt.

2

Tôi nghĩ đó không phải là một trong hai cuộc tranh luận. Chỉ là:

  1. mối quan hệ (thừa kế) xảy ra ít hơn mối quan hệ có thành phần (thành phần).
  2. Thừa kế là khó khăn hơn để có được ngay cả khi nó thích hợp để sử dụng nó, vì vậy cần phải được thực hiện bởi 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 thực hiện và vv.

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

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

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

3

Câu hỏi thường được đặt là Composition vs. Inheritance và câu hỏi đã được hỏi ở đây trước đây.

+0

Phải rồi ..vui mà SO đã không cho rằng một trong những câu hỏi liên quan. –

+2

SO tìm kiếm vẫn còn hút thời gian lớn –

+0

Đồ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. –

14

Dưới đây là lập luận phổ biến nhất của tôi:

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

  1. giao diện: "khuôn mặt công cộng" của đối tượng. Đây là tập hợp các khả năng mà nó thông báo cho phần còn lại của thế giới. Trong nhiều ngôn ngữ, tập hợp được định nghĩa rõ ràng thành một "lớp". Thông thường đây là những 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. Thực hiện : công việc "đằng sau hậu trường" của đối tượng để thỏa mãn giao diệ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 thực hiện là đóng gói (ví dụ: ẩn) trong lớp học; điều duy nhất mà người ngoài xem 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ả hai cách triển khai và giao diện. Điều này, đến lượt mình, có nghĩa là bạn đang buộc chấp nhận cả hai dưới dạng các ràng buộc đối với lớp học của bạn.

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 cũng 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 như nó thích, nhưng cuối cùng nó chịu trách nhiệm cho chính nó. 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 luôn thích tập hợp hơn thừa kế.

+0

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

+0

Nếu bạn cần thêm giao diện, 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 cấp chính, rửa lại lặp lại. – orcmid

+0

Bạn có nghĩ rằng, tập hợp tốt hơn trong trường hợp này? Sách là một SellingItem, A DigitalDisc là một SelliingItem http://codereview.stackexchange.com/questions/14077/is-it-proper-tpt-inheritance – Lijo

11

Tôi đã trả lời cho "Is a" vs "Has a" : which one is better?.

Về cơ bản tôi đồng ý với folks khác: sử dụng thừa kế duy nhất nếu lớp có nguồn gốc thực sự của bạn 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 thu được các phương thức cũng như dữ liệu.

Điều đó có ý nghĩa đối với lớp dẫn xuất của bạn để có tất cả các phương pháp của lớp bậc trên không? Hay bạn chỉ lặng lẽ tự hứa với mình rằng những phương pháp đó nên được bỏ qua trong lớp dẫn xuất? Hay bạn có thấy mình ghi đè các phương thức từ siêu lớp, làm cho chúng trở nên vô dụng, vì vậy 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ổng hợp là 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 "là-a so với có-a; chúng khác nhau về khái niệm" về vấn đề này và các câu hỏi liên quan.

Điều mà tôi đã tìm thấy trong kinh nghiệm của mình là cố gắng xác định xem mối quan hệ là "is-a" hoặc "has-a" nhất định không thành công. Ngay cả khi bạn có thể thực hiện chính xác quyết định đó 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 số điểm trong tương lai.

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

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

Vì vậy, bạn có một cơn bão hoàn hảo của lập luận chống lại thừa kế bất cứ khi nào bạn phải chọn một trong hai:

  1. sự lựa chọn của bạn có thể sẽ là một sai tại 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. Thừa kế có xu hướng trở thành lựa chọn tồi tệ hơn vì nó có nhiều ràng buộc hơn.

Do đó, tôi có xu hướng chọn tổng hợp - ngay cả khi có vẻ là mối quan hệ mạnh mẽ.

+0

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. Thừa kế và tập hợp chỉ là đỉnh của tảng băng trôi đó. Bạn không thể kiến ​​trúc sư cho tất cả các tương lai có thể, vì vậy chỉ cần đi với ý tưởng XP: giải quyết các yêu cầu của ngày hôm nay và chấp nhận rằng bạn có thể phải cấu trúc lại. –

+0

Tôi thích dòng yêu cầu và câu hỏi này. Lo ngại về blurrung là-a, has-a, và use-a. Tôi bắt đầu với các giao diện (cùng với việc xác định các trừu tượng thực hiện mà tôi muốn các giao diện). Mức độ gián tiếp bổ sung là vô giá. Đừng theo kết luận tổng hợp của bạn. – orcmid

+0

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

3

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

Tôi nghĩ 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ụ cụ thể được thực hiện trong câu hỏi.

Ngoài ra, tôi khuyên bạn không nên nhầm lẫn mục tiêu với công cụ. Người ta muốn đảm bảo rằng kỹ thuật hoặc phương pháp được lựa chọn hỗ trợ việc đạt được mục tiêu chính, nhưng tôi không điều gì ngoài ngữ cảnh mà thảo luận kỹ thuật tốt nhấ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 những điểm ngọt ngào rõ ràng của chúng.

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

Bạn đang tạo một khung thành phần, ngay cả 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 thừa kế của các giao diện (nếu có) khỏi cấu trúc thừa kế của các lớp thực hiện chúng không? Điều quan trọng là phải ẩn cấu trúc lớp của một triển khai từ mã dựa trên các giao diện mà triển khai thực hiện? Có nhiều triển khai có thể sử dụng được cùng một lúc hay là biến thể theo thời gian 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 phải đượ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 luận.

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

Tôi là một người thuần túy giao diện và tôi bị lôi cuốn vào các loại vấn đề và cách tiếp cận mà giao diện thuần khiết là phù hợp, cho dù xây dựng một khung công tác Java hay tổ chức một số triển khai COM. Điều đó không thích hợp cho 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 khiết giao diện, vì vậy sẽ rất thú vị khi xem cách tôi quản lý để đối phó.)

1

Lợi ích xảy ra khi cả hai ứng cử viên đủ tiêu chuẩ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 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 tính linh hoạt thời gian chạy/động.

Lợi ích không hiển thị ngay lập tức. Để xem lợi ích bạn cần phải đợi cho 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 generalation không thành công khi so sánh với những người chấp nhận thành phần (ngoại trừ một trường hợp rõ ràng được đề cập sau). Do đó quy tắc. Từ một quan điểm học tập nếu bạn có thể thực hiện một tiêm phụ thuộc thành công thì bạn nên biết cái nào để ủng hộ và khi nào. Quy tắc cũng giúp bạn đưa ra quyết định; nếu bạn không chắc chắn thì hãy chọn bố cục.

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