Tại sao tôi nên tránh nhiều kế thừa trong C ++?


167

Đây có phải là một khái niệm tốt để sử dụng nhiều kế thừa hay tôi có thể làm những việc khác thay thế?

Câu trả lời:


259

Nhiều di sản (viết tắt là MI) có mùi , có nghĩa là thông thường , nó được thực hiện vì lý do xấu, và nó sẽ thổi ngược lại vào mặt của người bảo trì.

Tóm lược

  1. Xem xét thành phần của các tính năng, thay vì kế thừa
  2. Hãy cảnh giác với Diamond of Dread
  3. Xem xét kế thừa của nhiều giao diện thay vì các đối tượng
  4. Đôi khi, Đa kế thừa là điều đúng đắn. Nếu có, sau đó sử dụng nó.
  5. Hãy chuẩn bị để bảo vệ kiến ​​trúc đa kế thừa của bạn trong các đánh giá mã

1. Có lẽ thành phần?

Điều này đúng với thừa kế, và do đó, nó thậm chí còn đúng hơn đối với nhiều kế thừa.

Có phải đối tượng của bạn thực sự cần phải kế thừa từ người khác? A Carkhông cần kế thừa từ một Engineđể làm việc, cũng không phải từ a Wheel. A Carcó một Enginevà bốn Wheel.

Nếu bạn sử dụng nhiều kế thừa để giải quyết các vấn đề này thay vì thành phần, thì bạn đã làm sai điều gì đó.

2. Viên kim cương đáng sợ

Thông thường, bạn có một lớp A, sau đó BCcả hai đều kế thừa từ A. Và (đừng hỏi tôi tại sao) sau đó ai đó quyết định rằng Dphải thừa hưởng cả từ BC .

Tôi đã gặp loại vấn đề này hai lần trong 8 năm, và thật thú vị khi thấy vì:

  1. Bao nhiêu sai lầm ngay từ đầu (Trong cả hai trường hợp, Dkhông nên thừa hưởng từ cả hai BC), bởi vì đây là kiến ​​trúc tồi (trên thực tế, hoàn toàn Ckhông nên tồn tại ...)
  2. Bao nhiêu người bảo trì đã trả tiền cho điều đó, bởi vì trong C ++, lớp cha mẹ Ađã có mặt hai lần trong lớp cháu của nó D, và do đó, cập nhật một trường cha mẹ A::fieldcó nghĩa là cập nhật hai lần (thông qua B::fieldC::field), hoặc có gì đó âm thầm bị lỗi và sụp đổ, sau đó (một con trỏ mới B::fieldvà xóa C::field...)

Sử dụng từ khóa ảo trong C ++ để đủ điều kiện thừa kế sẽ tránh bố cục kép được mô tả ở trên nếu đây không phải là điều bạn muốn, nhưng dù sao, theo kinh nghiệm của tôi, có lẽ bạn đã làm sai ...

Trong hệ thống phân cấp Đối tượng, bạn nên cố gắng giữ cấu trúc phân cấp dưới dạng Cây (một nút có MỘT cha mẹ), không phải dưới dạng biểu đồ.

Tìm hiểu thêm về Kim cương (chỉnh sửa 2017-05-03)

Vấn đề thực sự với Diamond of Dread trong C ++ ( giả sử thiết kế là âm thanh - hãy xem lại mã của bạn! ), Đó là bạn cần đưa ra lựa chọn :

  • Có phải lớp mong muốn Atồn tại hai lần trong bố cục của bạn không, và nó có nghĩa là gì? Nếu có, thì bằng mọi cách hãy thừa hưởng từ nó hai lần.
  • nếu nó chỉ tồn tại một lần, thì kế thừa từ nó hầu như.

Sự lựa chọn này là vấn đề cố hữu và trong C ++, không giống như các ngôn ngữ khác, bạn thực sự có thể làm điều đó mà không cần giáo điều buộc thiết kế của bạn ở cấp độ ngôn ngữ.

Nhưng giống như tất cả các quyền hạn, với sức mạnh đó có trách nhiệm: Hãy xem xét thiết kế của bạn.

3. Giao diện

Nhiều kế thừa của 0 hoặc một lớp cụ thể và không hoặc nhiều giao diện thường được chấp nhận, bởi vì bạn sẽ không gặp phải Diamond of Dread được mô tả ở trên. Trong thực tế, đây là cách mọi thứ được thực hiện trong Java.

Thông thường, ý của bạn là gì khi C thừa hưởng từ ABngười dùng có thể sử dụng Cnhư thể nó là một A, và / hoặc như thể nó là một B.

Trong C ++, giao diện là một lớp trừu tượng có:

  1. tất cả phương thức của nó được khai báo là thuần ảo (có hậu tố = 0) (đã xóa 2017-05-03)
  2. không có biến thành viên

Đa kế thừa từ 0 đến một đối tượng thực và không hoặc nhiều giao diện không được coi là "có mùi" (ít nhất, không nhiều như vậy).

Tìm hiểu thêm về Giao diện Trừu tượng C ++ (chỉnh sửa 2017-05-03)

Đầu tiên, mẫu NVI có thể được sử dụng để tạo giao diện, bởi vì tiêu chí thực sự là không có trạng thái (tức là không có biến thành viên, ngoại trừ this). Quan điểm của giao diện trừu tượng của bạn là xuất bản hợp đồng ("bạn có thể gọi tôi theo cách này và theo cách này"), không hơn, không kém. Giới hạn của việc chỉ có phương pháp ảo trừu tượng nên là một lựa chọn thiết kế, không phải là nghĩa vụ.

Thứ hai, trong C ++, nó có ý nghĩa để kế thừa hầu như từ các giao diện trừu tượng, (ngay cả với chi phí / chỉ định bổ sung). Nếu bạn không và kế thừa giao diện xuất hiện nhiều lần trong hệ thống phân cấp của bạn, thì bạn sẽ có sự mơ hồ.

Thứ ba, định hướng đối tượng là tuyệt vời, nhưng nó không phải là Sự thật duy nhất ngoài TM trong C ++. Sử dụng các công cụ phù hợp và luôn nhớ rằng bạn có các mô hình khác trong C ++ cung cấp các loại giải pháp khác nhau.

4. Bạn có thực sự cần nhiều kế thừa?

Thỉnh thoảng đúng.

Thông thường, bạn Clớp được kế thừa từ ABABhai đối tượng liên quan (tức là không có trong hệ thống cấp bậc tương tự, không có gì chung, khái niệm khác nhau, vv).

Ví dụ: bạn có thể có một hệ thống Nodesvới các tọa độ X, Y, Z, có thể thực hiện nhiều phép tính hình học (có thể là một điểm, một phần của các đối tượng hình học) và mỗi Node là một Tác nhân tự động, có thể giao tiếp với các tác nhân khác.

Có lẽ bạn đã có quyền truy cập vào hai thư viện, mỗi thư viện có không gian tên riêng (một lý do khác để sử dụng không gian tên ... Nhưng bạn sử dụng không gian tên, phải không?), Một người geovà người kiaai

Vì vậy, bạn có own::Nodenguồn gốc của riêng bạn cả từ ai::Agentgeo::Point.

Đây là lúc bạn nên tự hỏi mình rằng bạn không nên sử dụng bố cục thay thế. Nếu own::Nodethực sự thực sự là cả a ai::Agentvà a geo::Point, thì thành phần sẽ không làm được.

Sau đó, bạn sẽ cần nhiều kế thừa, own::Nodegiao tiếp với các đại lý khác theo vị trí của họ trong không gian 3D.

(Bạn sẽ lưu ý rằng ai::Agentgeo::Pointhoàn toàn, hoàn toàn, KHÔNG GIỚI HẠN ... Điều này làm giảm đáng kể nguy cơ đa thừa kế)

Các trường hợp khác (chỉnh sửa 2017-05-03)

Có những trường hợp khác:

  • sử dụng kế thừa (hy vọng là riêng tư) làm chi tiết triển khai
  • một số thành ngữ C ++ như chính sách có thể sử dụng nhiều kế thừa (khi mỗi phần cần giao tiếp với các phần khác thông qua this)
  • kế thừa ảo từ std :: ngoại lệ ( Có phải thừa kế ảo cần thiết cho ngoại lệ không? )
  • Vân vân.

Đôi khi bạn có thể sử dụng thành phần, và đôi khi MI là tốt hơn. Vấn đề là: Bạn có một sự lựa chọn. Làm điều đó có trách nhiệm (và xem lại mã của bạn).

5. Vậy tôi có nên làm thừa kế không?

Hầu hết thời gian, theo kinh nghiệm của tôi, không. MI không phải là công cụ phù hợp, ngay cả khi nó có vẻ hoạt động, bởi vì nó có thể được sử dụng bởi những kẻ lười biếng để chồng các tính năng lại với nhau mà không nhận ra hậu quả (như tạo Carcả hai Enginevà a Wheel).

Nhưng đôi khi, có. Và tại thời điểm đó, không có gì sẽ hoạt động tốt hơn MI.

Nhưng vì MI có mùi, hãy chuẩn bị để bảo vệ kiến ​​trúc của bạn trong các bài đánh giá mã (và bảo vệ nó là một điều tốt, bởi vì nếu bạn không thể bảo vệ nó, thì bạn không nên làm điều đó).


4
Tôi sẽ lập luận rằng nó không bao giờ cần thiết và hiếm khi hữu ích đến mức ngay cả khi nó phù hợp hoàn hảo, việc tiết kiệm cho phái đoàn sẽ không bù đắp cho sự nhầm lẫn đối với các lập trình viên không quen sử dụng nó, nhưng nếu không thì tóm tắt tốt.
Bill K

2
Tôi đã thêm vào điểm 5, rằng mã sử dụng MI nên có một nhận xét ngay bên cạnh nó giải thích lý do, vì vậy bạn không phải giải thích nó bằng lời nói trong bài đánh giá mã. Nếu không có điều này thì một người không phải là người đánh giá của bạn có thể nghi ngờ phán quyết tốt của bạn khi họ thấy mã của bạn và bạn có thể không có cơ hội để bảo vệ nó.
Tim Abell

" Bởi vì bạn sẽ không gặp phải Diamond of Dread được mô tả ở trên. " Tại sao không?
tò mò

1
Tôi sử dụng MI cho mẫu quan sát.
Calmarius

13
@BillK: Tất nhiên là không cần thiết. Nếu bạn có ngôn ngữ Biến hoàn toàn không có MI, bạn có thể làm bất cứ điều gì ngôn ngữ có MI có thể làm. Vì vậy, có, nó không cần thiết. Không bao giờ. Điều đó nói rằng, nó có thể là một công cụ rất hữu ích và cái gọi là Kim cương đáng sợ ... Tôi chưa bao giờ thực sự hiểu tại sao từ "sợ hãi" lại xuất hiện ở đó. Nó thực sự khá dễ dàng để lý luận và thường không phải là một vấn đề.
Thomas Eding

145

Từ một cuộc phỏng vấn với Bjarne Stroustrup :

Mọi người hoàn toàn nói chính xác rằng bạn không cần nhiều thừa kế, bởi vì bất cứ điều gì bạn có thể làm với nhiều thừa kế bạn cũng có thể làm với một thừa kế duy nhất. Bạn chỉ cần sử dụng thủ thuật đoàn tôi đã đề cập. Hơn nữa, bạn không cần bất kỳ sự thừa kế nào cả, bởi vì bất kỳ điều gì bạn làm với một thừa kế duy nhất bạn cũng có thể làm mà không cần thừa kế bằng cách chuyển tiếp qua một lớp. Trên thực tế, bạn cũng không cần bất kỳ lớp nào, bởi vì bạn có thể làm tất cả với con trỏ và cấu trúc dữ liệu. Nhưng tại sao bạn lại muốn làm điều đó? Khi nào thuận tiện để sử dụng các cơ sở ngôn ngữ? Khi nào bạn thích một cách giải quyết? Tôi đã thấy các trường hợp trong đó nhiều kế thừa là hữu ích và thậm chí tôi đã thấy các trường hợp trong đó nhiều kế thừa khá phức tạp là hữu ích. Nói chung, tôi thích sử dụng các phương tiện được cung cấp bởi ngôn ngữ để thực hiện các giải pháp thay thế


6
Có một số tính năng của C ++ mà tôi đã bỏ lỡ trong C # và Java, mặc dù việc có chúng sẽ khiến việc thiết kế trở nên khó khăn / phức tạp hơn. Ví dụ, các đảm bảo bất biến của const- Tôi đã phải viết các cách giải quyết khó hiểu (thường sử dụng giao diện và thành phần) khi một lớp thực sự cần thiết để có các biến thể có thể thay đổi và bất biến. Tuy nhiên, tôi chưa bao giờ một lần bỏ lỡ đa kế thừa, và chưa bao giờ cảm thấy rằng tôi đã phải viết một workaround do thiếu tính năng này. Đó là sự khác biệt. Trong mọi trường hợp tôi từng thấy, không sử dụng MI là lựa chọn thiết kế tốt hơn, không phải là cách giải quyết.
BlueRaja - Daniel Pflughoeft

24
+1: Câu trả lời hoàn hảo: Nếu bạn không muốn sử dụng nó, thì đừng sử dụng nó.
Thomas Eding

38

Không có lý do để tránh nó và nó có thể rất hữu ích trong các tình huống. Bạn cần phải nhận thức được các vấn đề tiềm năng mặc dù.

Người lớn nhất là viên kim cương của cái chết:

class GrandParent;
class Parent1 : public GrandParent;
class Parent2 : public GrandParent;
class Child : public Parent1, public Parent2;

Bây giờ bạn có hai "bản sao" của GrandParent trong Child.

C ++ đã nghĩ về điều này mặc dù và cho phép bạn thực hiện kế thừa ảo để khắc phục các vấn đề.

class GrandParent;
class Parent1 : public virtual GrandParent;
class Parent2 : public virtual GrandParent;
class Child : public Parent1, public Parent2;

Luôn xem lại thiết kế của bạn, đảm bảo bạn không sử dụng tính kế thừa để tiết kiệm khi sử dụng lại dữ liệu. Nếu bạn có thể đại diện cho điều tương tự với thành phần (và thông thường bạn có thể) thì đây là một cách tiếp cận tốt hơn nhiều.


21
Và nếu bạn cấm thừa kế và chỉ sử dụng thành phần thay thế, bạn luôn có hai GrandParenttrong Child. Có một nỗi sợ MI, bởi vì mọi người chỉ nghĩ rằng họ có thể không hiểu các quy tắc ngôn ngữ. Nhưng bất cứ ai không thể có được các quy tắc đơn giản này cũng không thể viết một chương trình không tầm thường.
tò mò

1
Nghe có vẻ khắc nghiệt, quy tắc C ++ tất cả đều rất công phu. OK, vì vậy ngay cả khi các quy tắc riêng lẻ đơn giản, có rất nhiều trong số chúng, điều đó làm cho toàn bộ điều không đơn giản, ít nhất là cho một con người tuân theo.
Zebrafish

11

Xem w: Đa kế thừa .

Nhiều kế thừa đã nhận được sự chỉ trích và như vậy, không được thực hiện bằng nhiều ngôn ngữ. Các bài phê bình bao gồm:

  • Độ phức tạp tăng
  • Sự mơ hồ ngữ nghĩa thường được tóm tắt là vấn đề kim cương .
  • Không thể kế thừa một cách rõ ràng nhiều lần từ một lớp
  • Thứ tự kế thừa thay đổi ngữ nghĩa lớp.

Nhiều kế thừa trong các ngôn ngữ với các hàm tạo kiểu C ++ / Java làm trầm trọng thêm vấn đề kế thừa của các hàm tạo và chuỗi hàm tạo, do đó tạo ra các vấn đề về bảo trì và mở rộng trong các ngôn ngữ này. Các đối tượng trong các mối quan hệ thừa kế với các phương thức xây dựng rất khác nhau rất khó thực hiện theo mô hình chuỗi xây dựng.

Cách hiện đại để giải quyết điều này để sử dụng giao diện (lớp trừu tượng thuần túy) như giao diện COM và Java.

Tôi có thể làm những thứ khác thay thế cho điều này?

Vâng, bạn có thể. Tôi sẽ ăn cắp từ GoF .

  • Chương trình cho một giao diện, không phải là một triển khai
  • Thích thành phần hơn thừa kế

8

Kế thừa công khai là mối quan hệ IS-A và đôi khi một lớp sẽ là một loại của một số lớp khác nhau và đôi khi điều quan trọng là phải phản ánh điều này.

"Mixins" đôi khi cũng hữu ích. Chúng thường là các lớp nhỏ, thường không kế thừa từ bất cứ thứ gì, cung cấp chức năng hữu ích.

Miễn là hệ thống phân cấp thừa kế khá nông (vì nó hầu như luôn luôn như vậy) và được quản lý tốt, bạn sẽ không có được quyền thừa kế kim cương đáng sợ. Kim cương không phải là vấn đề với tất cả các ngôn ngữ sử dụng nhiều tính kế thừa, nhưng cách xử lý của C ++ về nó thường rất khó xử và đôi khi khó hiểu.

Mặc dù tôi gặp phải trường hợp nhiều thừa kế rất tiện dụng, nhưng chúng thực sự khá hiếm. Điều này có thể là do tôi thích sử dụng các phương pháp thiết kế khác khi tôi không thực sự cần nhiều kế thừa. Tôi thích tránh các cấu trúc ngôn ngữ khó hiểu và thật dễ dàng để xây dựng các trường hợp thừa kế trong đó bạn phải đọc hướng dẫn thực sự tốt để tìm hiểu điều gì đang xảy ra.


6

Bạn không nên "tránh" nhiều kế thừa nhưng bạn nên chú ý đến các vấn đề có thể phát sinh, chẳng hạn như 'vấn đề kim cương' ( http://en.wikipedia.org/wiki/Diamond_probols ) và chăm sóc sức mạnh cho bạn , như bạn nên với tất cả các quyền hạn.


1
+1: Giống như bạn không nên tránh con trỏ; bạn chỉ cần nhận thức được sự phân nhánh của họ. Mọi người cần ngừng lục lọi các cụm từ (chẳng hạn như "MI là xấu xa", "không tối ưu hóa", "goto là xấu xa") họ đã học được trên mạng chỉ vì một số người hippy nói rằng điều đó không tốt cho vệ sinh của họ. Điều tồi tệ nhất là họ thậm chí không bao giờ thử sử dụng những thứ như vậy và chỉ xem nó như thể được nói từ kinh thánh.
Thomas Eding

1
Tôi đồng ý. Đừng "tránh" nó, nhưng hãy biết cách tốt nhất để xử lý vấn đề. Có lẽ tôi muốn sử dụng các đặc điểm cấu thành, nhưng C ++ không có điều đó. Có lẽ tôi muốn sử dụng ủy quyền tự động 'xử lý', nhưng C ++ không có điều đó. Vì vậy, tôi sử dụng công cụ chung và cùn của MI cho nhiều mục đích khác nhau, có lẽ đồng thời.
JDługosz

3

Có nguy cơ nhận được một chút trừu tượng, tôi thấy nó sáng tỏ khi nghĩ về sự kế thừa trong khung lý thuyết thể loại.

Nếu chúng ta nghĩ về tất cả các lớp và mũi tên giữa chúng biểu thị quan hệ thừa kế, thì đại loại như thế này

A --> B

có nghĩa là class Bxuất phát từ class A. Lưu ý rằng, đã cho

A --> B, B --> C

chúng ta nói C xuất phát từ B xuất phát từ A, do đó C cũng được cho là xuất phát từ A, do đó

A --> C

Hơn nữa, chúng tôi nói rằng đối với mọi lớp AAnguồn gốc tầm thường A, do đó mô hình thừa kế của chúng tôi đáp ứng định nghĩa của một thể loại. Trong ngôn ngữ truyền thống hơn, chúng ta có một thể loại Classvới các đối tượng tất cả các lớp và hình thái quan hệ thừa kế.

Đó là một chút thiết lập, nhưng với điều đó, hãy xem Diamond of Doom của chúng tôi:

C --> D
^     ^
|     |
A --> B

Đó là một sơ đồ mờ ám, nhưng nó sẽ làm được. Vì vậy, Dkế thừa từ tất cả các A, BC. Hơn nữa, và tiến gần hơn đến việc giải quyết câu hỏi của OP, Dcũng thừa hưởng từ bất kỳ siêu lớp nào A. Chúng ta có thể vẽ sơ đồ

C --> D --> R
^     ^
|     |
A --> B
^ 
|
Q

Bây giờ, các vấn đề liên quan đến Kim cương tử thần ở đây là khi CBchia sẻ một số tên thuộc tính / phương thức và mọi thứ trở nên mơ hồ; tuy nhiên, nếu chúng ta di chuyển bất kỳ hành vi chia sẻ nào vào Athì sự mơ hồ sẽ biến mất.

Đặt trong các thuật ngữ phân loại, chúng tôi muốn A, BCđể được nếu BCkế thừa từ Qđó Acó thể được viết lại như là lớp con của Q. Điều này làm cho Amột cái gì đó được gọi là đẩy ra .

Ngoài ra còn có một cấu trúc đối xứng Dđược gọi là pullback . Đây về cơ bản là lớp hữu ích chung nhất mà bạn có thể xây dựng kế thừa từ cả hai BC. Đó là, nếu bạn có bất kỳ lớp nào khác được Rthừa kế từ BC, thì đó Dlà một lớp Rcó thể được viết lại dưới dạng lớp con của D.

Đảm bảo rằng các mẹo về viên kim cương của bạn là pullback và đẩy cho chúng ta một cách hay để xử lý chung các vấn đề về xung đột tên hoặc bảo trì có thể phát sinh theo cách khác.

Lưu ý câu trả lời của Paercebal đã truyền cảm hứng cho điều này vì những lời khuyên của anh ta được ngụ ý bởi mô hình trên cho rằng chúng tôi làm việc trong toàn thể loại Lớp của tất cả các lớp có thể.

Tôi muốn khái quát hóa lập luận của anh ấy đến một cái gì đó cho thấy mức độ phức tạp của nhiều mối quan hệ thừa kế có thể vừa mạnh mẽ vừa không có vấn đề.

TL; DR Hãy nghĩ về các mối quan hệ thừa kế trong chương trình của bạn như là một thể loại. Sau đó, bạn có thể tránh các vấn đề của Diamond of Doom bằng cách tạo các lớp đẩy được thừa kế nhiều lần và đối xứng, tạo một lớp cha phổ biến là một pullback.


3

Chúng tôi sử dụng Eiffel. Chúng tôi có MI tuyệt vời. Đừng lo lắng. Không vấn đề. Dễ dàng quản lý. Có những lúc KHÔNG sử dụng MI. Tuy nhiên, nó hữu ích hơn mọi người nhận ra bởi vì họ: A) trong một ngôn ngữ nguy hiểm không quản lý tốt nó -OR- B) hài lòng với cách họ đã làm việc xung quanh MI trong nhiều năm và nhiều năm -OR- C) những lý do khác ( quá nhiều để liệt kê tôi khá chắc chắn - xem câu trả lời ở trên).

Đối với chúng tôi, sử dụng Eiffel, MI cũng tự nhiên như mọi thứ khác và một công cụ tốt khác trong hộp công cụ. Thành thật mà nói, chúng tôi khá không quan tâm rằng không ai khác đang sử dụng Eiffel. Đừng lo lắng. Chúng tôi hài lòng với những gì chúng tôi có và mời bạn có một cái nhìn.

Trong khi bạn đang tìm kiếm: Lưu ý đặc biệt về sự an toàn của Void và việc xóa bỏ hội thảo con trỏ Null. Trong khi tất cả chúng ta đang nhảy múa xung quanh MI, con trỏ của bạn đang bị lạc! :-)


2

Mỗi ngôn ngữ lập trình có một cách xử lý khác nhau về lập trình hướng đối tượng với những ưu và nhược điểm. Phiên bản của C ++ đặt sự nhấn mạnh vào hiệu năng và có nhược điểm đi kèm là rất dễ viết mã không hợp lệ - và điều này đúng với nhiều kế thừa. Kết quả là có một xu hướng lèo lái các lập trình viên ra khỏi tính năng này.

Những người khác đã giải quyết câu hỏi về việc thừa kế nhiều thứ không tốt cho việc gì. Nhưng chúng tôi đã thấy khá nhiều ý kiến ​​cho rằng ít nhiều ngụ ý rằng lý do để tránh nó là vì nó không an toàn. Vâng, có và không.

Như thường đúng trong C ++, nếu bạn làm theo một hướng dẫn cơ bản, bạn có thể sử dụng nó một cách an toàn mà không phải "nhìn qua vai" liên tục. Ý tưởng chính là bạn phân biệt một loại định nghĩa lớp đặc biệt gọi là "hỗn hợp"; lớp là một hỗn hợp nếu tất cả các hàm thành viên của nó là ảo (hoặc ảo thuần). Sau đó, bạn được phép kế thừa từ một lớp chính duy nhất và bao nhiêu "hỗn hợp" tùy thích - nhưng bạn nên kế thừa các mixin với từ khóa "ảo". ví dụ

class CounterMixin {
    int count;
public:
    CounterMixin() : count( 0 ) {}
    virtual ~CounterMixin() {}
    virtual void increment() { count += 1; }
    virtual int getCount() { return count; }
};

class Foo : public Bar, virtual public CounterMixin { ..... };

Đề nghị của tôi là nếu bạn có ý định sử dụng một lớp làm lớp hỗn hợp, bạn cũng áp dụng quy ước đặt tên để giúp mọi người đọc mã dễ dàng xem những gì đang xảy ra & để xác minh bạn đang chơi theo quy tắc của hướng dẫn cơ bản . Và bạn sẽ thấy nó hoạt động tốt hơn nhiều nếu các mix-in của bạn cũng có các hàm tạo mặc định, chỉ vì cách các lớp cơ sở ảo hoạt động. Và hãy nhớ làm cho tất cả các kẻ hủy diệt ảo quá.

Lưu ý rằng việc tôi sử dụng từ "hỗn hợp" ở đây không giống với lớp mẫu được tham số hóa (xem liên kết này để được giải thích tốt) nhưng tôi nghĩ đó là cách sử dụng hợp lý thuật ngữ.

Bây giờ tôi không muốn tạo ấn tượng rằng đây là cách duy nhất để sử dụng nhiều kế thừa một cách an toàn. Đó chỉ là một cách khá dễ kiểm tra.


2

Bạn nên sử dụng nó một cách cẩn thận, có một số trường hợp, như Vấn đề Kim cương , khi mọi thứ có thể trở nên phức tạp.

văn bản thay thế
(nguồn: learncpp.com )


12
Đây là một ví dụ tồi, bởi vì một máy photocopy nên bao gồm một máy quét và máy in, không được thừa kế từ chúng.
Svante

2
Dù sao, Printerthậm chí không nên là một PoweredDevice. A Printerlà để in, không phải quản lý năng lượng. Việc triển khai một máy in cụ thể có thể phải thực hiện một số quản lý nguồn, nhưng các lệnh quản lý nguồn này không được tiếp xúc trực tiếp với người dùng máy in. Tôi không thể tưởng tượng bất kỳ việc sử dụng thế giới thực này của hệ thống phân cấp này.
tò mò


1

Ngoài mô hình kim cương, nhiều kế thừa có xu hướng làm cho mô hình đối tượng khó hiểu hơn, điều này làm tăng chi phí bảo trì.

Thành phần về bản chất là dễ hiểu, hiểu và giải thích. Việc viết mã có thể trở nên tẻ nhạt, nhưng một IDE tốt (đã vài năm kể từ khi tôi làm việc với Visual Studio, nhưng chắc chắn các IDE Java đều có các công cụ tự động tắt phím sáng tác tuyệt vời) sẽ giúp bạn vượt qua rào cản đó.

Ngoài ra, về mặt bảo trì, "vấn đề kim cương" cũng xuất hiện trong các trường hợp thừa kế không theo nghĩa đen. Chẳng hạn, nếu bạn có A và B và lớp C của bạn mở rộng cả hai và A có phương pháp 'makeJuice' tạo ra nước cam và bạn mở rộng điều đó để tạo ra nước cam với một vắt vôi: điều gì xảy ra khi nhà thiết kế cho ' B 'thêm phương pháp' makeJuice 'tạo ra và dòng điện? 'A' và 'B' có thể tương thích với "cha mẹ" ngay bây giờ , nhưng điều đó không có nghĩa là họ sẽ luôn như vậy!

Nhìn chung, châm ngôn của xu hướng tránh kế thừa, và đặc biệt là nhiều kế thừa, là âm thanh. Như tất cả các câu châm ngôn, đều có ngoại lệ, nhưng bạn cần chắc chắn rằng có một dấu hiệu neon màu xanh lá cây nhấp nháy chỉ vào bất kỳ trường hợp ngoại lệ nào bạn mã hóa (và huấn luyện bộ não của bạn để bất cứ khi nào bạn nhìn thấy những cây thừa kế như vậy bạn vẽ trong đèn neon màu xanh lá cây nhấp nháy của riêng bạn ký) và rằng bạn kiểm tra để đảm bảo tất cả đều có ý nghĩa mỗi lần.


what happens when the designer for 'B' adds a 'makeJuice' method which generates and electrical current?Uhhh, bạn nhận được một lỗi biên dịch tất nhiên (nếu việc sử dụng là mơ hồ).
Thomas Eding

1

Vấn đề chính với MI của các đối tượng cụ thể là hiếm khi bạn có một đối tượng hợp pháp nên "Trở thành A VÀ là B", vì vậy hiếm khi đó là giải pháp chính xác trên cơ sở logic. Thường xuyên hơn, bạn có một đối tượng C tuân theo "C có thể hoạt động như A hoặc B", mà bạn có thể đạt được thông qua kế thừa và bố cục giao diện. Nhưng đừng nhầm lẫn - kế thừa nhiều giao diện vẫn là MI, chỉ là một tập hợp con của nó.

Đối với C ++ nói riêng, điểm yếu chính của tính năng không phải là SỰ TUYỆT VỜI thực tế của Đa kế thừa, nhưng một số cấu trúc mà nó cho phép hầu như luôn bị sai. Ví dụ: kế thừa nhiều bản sao của cùng một đối tượng như:

class B : public A, public A {};

bị dị hình B DENG ĐỊNH NGH DEA. Được dịch sang tiếng Anh đây là "B là A và A". Vì vậy, ngay cả trong ngôn ngữ của con người cũng có một sự mơ hồ nghiêm trọng. Ý bạn là "B có 2 As" hay chỉ "B là A"?. Cho phép mã bệnh lý như vậy, và tệ hơn là biến nó thành một ví dụ sử dụng, C ++ không ủng hộ khi đưa ra một trường hợp để giữ tính năng này trong các ngôn ngữ kế nhiệm.


0

Bạn có thể sử dụng thành phần trong sở thích để thừa kế.

Cảm giác chung là thành phần tốt hơn, và nó được thảo luận rất tốt.


4
-1: The general feeling is that composition is better, and it's very well discussed.Điều đó không có nghĩa là thành phần tốt hơn.
Thomas Eding

Ngoại trừ nó, thực sự, tốt hơn.
Ali Afshar

12
Ngoại trừ những trường hợp không tốt hơn.
Thomas Eding

0

phải mất 4/8 byte cho mỗi lớp liên quan. (Một con trỏ này cho mỗi lớp).

Điều này có thể không bao giờ là một mối quan tâm, nhưng nếu một ngày nào đó bạn có một cấu trúc dữ liệu vi mô, nó sẽ mất hàng tỷ thời gian.


1
Chắc chắn với điều đó? Thông thường, bất kỳ sự kế thừa nghiêm túc nào cũng sẽ yêu cầu một con trỏ trong mỗi thành viên lớp, nhưng điều đó có nhất thiết phải chia tỷ lệ với các lớp được kế thừa không? Hơn nữa, bạn đã có hàng tỷ cấu trúc dữ liệu tuổi teen bao nhiêu lần?
David Thornley

3
@DavidThornley " bạn đã có hàng tỷ cấu trúc dữ liệu tuổi teen bao nhiêu lần? " Khi bạn đang lập luận chống lại MI.
tò mò
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.