Tại sao tôi nên thích sáng tác hơn thừa kế?


109

Tôi luôn đọc rằng thành phần là được ưu tiên hơn thừa kế. Một bài đăng trên blog không giống như các loại , ví dụ, ủng hộ việc sử dụng thành phần trên kế thừa, nhưng tôi không thể thấy được tính đa hình đạt được như thế nào.

Nhưng tôi có cảm giác rằng khi mọi người nói thích sáng tác, họ thực sự có nghĩa là thích kết hợp giữa bố cục và thực hiện giao diện. Làm thế nào bạn sẽ có được đa hình mà không cần thừa kế?

Đây là một ví dụ cụ thể nơi tôi sử dụng thừa kế. Làm thế nào điều này sẽ được thay đổi để sử dụng thành phần, và tôi sẽ đạt được gì?

Class Shape
{
    string name;
  public:
    void getName();
    virtual void draw()=0;
}

Class Circle: public Shape
{
    void draw(/*draw circle*/);
}

57
Không, khi mọi người nói thích sáng tác, họ thực sự có nghĩa là thích sáng tác, không bao giờ sử dụng tính kế thừa . Toàn bộ câu hỏi của bạn dựa trên một tiền đề bị lỗi. Sử dụng thừa kế khi nó thích hợp.
Lập hóa đơn cho thằn lằn

2
Bạn cũng có thể muốn xem qua chương
trình.stackexchange.com/questions/65179/ mẹo

2
Tôi đồng ý với Bill, tôi đã thấy việc sử dụng kế thừa là một thông lệ phổ biến trong phát triển GUI.
Cholachagudda Prashant

2
Bạn muốn sử dụng bố cục vì hình vuông chỉ là thành phần của hai hình tam giác, thực tế tôi nghĩ tất cả các hình dạng khác với hình elip chỉ là bố cục của hình tam giác. Đa hình là về nghĩa vụ hợp đồng và loại bỏ 100% từ thừa kế. Khi tam giác nhận được một loạt các điều kỳ lạ được thêm vào bởi vì ai đó muốn làm cho nó có khả năng tạo ra các kim tự tháp, nếu bạn thừa hưởng từ Tam giác, bạn sẽ nhận được tất cả những điều đó mặc dù bạn sẽ không bao giờ tạo ra một kim tự tháp 3d từ hình lục giác của mình .
Jimmy Hoffa

2
@BilltheLizard Tôi nghĩ rằng rất nhiều người nói rằng nó thực sự có nghĩa là không bao giờ sử dụng quyền thừa kế, nhưng họ đã sai.
Immibis

Câu trả lời:


51

Đa hình không nhất thiết ngụ ý thừa kế. Kế thừa thường được sử dụng như một phương tiện dễ dàng để thực hiện hành vi Đa hình, bởi vì nó thuận tiện để phân loại các đối tượng hành vi tương tự như có cấu trúc và hành vi gốc hoàn toàn phổ biến. Hãy nghĩ về tất cả những ví dụ về mã xe và chó mà bạn đã thấy trong nhiều năm qua.

Nhưng những gì về các đối tượng không giống nhau. Mô hình hóa một chiếc xe hơi và một hành tinh sẽ rất khác nhau, và cả hai có thể muốn thực hiện hành vi Move ().

Trên thực tế, về cơ bản, bạn đã trả lời câu hỏi của riêng bạn khi bạn nói "But I have a feeling that when people say prefer composition, they really mean prefer a combination of composition and interface implementation.". Hành vi thông thường có thể được cung cấp thông qua các giao diện và hỗn hợp hành vi.

Như là tốt hơn, câu trả lời là hơi chủ quan, và thực sự đi vào cách bạn muốn hệ thống của bạn hoạt động, điều gì có ý nghĩa cả về ngữ cảnh và kiến ​​trúc, và nó sẽ dễ dàng kiểm tra và bảo trì như thế nào.


Trong thực tế, tần suất "Đa hình qua Giao diện" xuất hiện thường xuyên như thế nào và nó có được coi là bình thường không (trái ngược với việc khai thác tính biểu cảm uể oải). Tôi muốn đặt cược rằng tính đa hình thông qua sự kế thừa là do thiết kế cẩn thận, không phải là một số hậu quả của ngôn ngữ (C ++) được phát hiện sau thông số kỹ thuật của nó.
samis

1
Ai sẽ gọi move () trên một hành tinh và một chiếc xe hơi và coi nó giống nhau?! Câu hỏi ở đây là trong bối cảnh nào họ có thể di chuyển? Nếu cả hai đều là đối tượng 2D trong một trò chơi 2D đơn giản, họ có thể thừa hưởng di chuyển, nếu họ là đối tượng trong một mô phỏng dữ liệu lớn, sẽ không có ý nghĩa gì khi để chúng kế thừa từ cùng một cơ sở và do đó bạn phải sử dụng một giao diện
NikkyD

2
@SamusArin Nó xuất hiện ở mọi nơi và được coi là hoàn toàn bình thường trong các ngôn ngữ hỗ trợ giao diện. Ý bạn là gì, "khai thác tính biểu cảm của ngôn ngữ"? Đây là những gì giao diện đang có .
Andres F.

@AresresF. Tôi vừa bắt gặp "Đa hình qua giao diện" trong một ví dụ trong khi đọc qua "Phân tích và thiết kế hướng đối tượng với các ứng dụng" đã trình diễn mẫu Observer. Sau đó tôi nhận ra câu trả lời của mình.
samis

@AresresF. Tôi đoán tầm nhìn của tôi hơi mù quáng về điều này vì cách tôi sử dụng đa hình trong dự án (đầu tiên) cuối cùng của mình. Tôi đã có 5 loại hồ sơ mà tất cả bắt nguồn từ cùng một cơ sở. Dù sao cũng cảm ơn sự giác ngộ.
samis

79

Thành phần ưa thích không chỉ là về đa hình. Mặc dù đó là một phần của nó, và bạn đúng rằng (ít nhất là trong các ngôn ngữ được đánh máy trên danh nghĩa) điều mà mọi người thực sự muốn nói là "thích sự kết hợp giữa bố cục và triển khai giao diện". Nhưng, lý do để thích sáng tác (trong nhiều trường hợp) là sâu sắc.

Đa hình là về một điều hành xử theo nhiều cách. Vì vậy, generic / template là một tính năng "đa hình" cho đến khi chúng cho phép một đoạn mã duy nhất thay đổi hành vi của nó với các loại. Trong thực tế, loại đa hình này thực sự là hành vi tốt nhất và thường được gọi là đa hình tham số vì sự biến đổi được xác định bởi một tham số.

Nhiều ngôn ngữ cung cấp một dạng đa hình được gọi là "quá tải" hoặc đa hình ad hoc trong đó nhiều quy trình có cùng tên được xác định theo cách thức ad hoc và trong đó một ngôn ngữ được chọn bởi ngôn ngữ (có lẽ là cụ thể nhất). Đây là loại đa hình hoạt động tốt nhất, vì không có gì kết nối hành vi của hai thủ tục ngoại trừ quy ước được phát triển.

Một loại đa hình thứ ba là đa hình phụ . Ở đây một thủ tục được xác định trên một kiểu nhất định, cũng có thể hoạt động trên cả một họ "kiểu con" của kiểu đó. Khi bạn thực hiện một giao diện hoặc mở rộng một lớp, bạn thường tuyên bố ý định tạo ra một kiểu con. Các kiểu con thực sự được điều chỉnh bởi Nguyên tắc thay thế của Liskov, trong đó nói rằng nếu bạn có thể chứng minh điều gì đó về tất cả các đối tượng trong một siêu kiểu, bạn có thể chứng minh điều đó về tất cả các trường hợp trong một kiểu con. Tuy nhiên, cuộc sống trở nên nguy hiểm, vì trong các ngôn ngữ như C ++ và Java, mọi người thường không có sự ràng buộc và các giả định thường không có giấy tờ về các lớp có thể đúng hoặc không đúng về các lớp con của họ. Đó là, mã được viết như thể có nhiều điều có thể chứng minh được hơn thực tế, nó tạo ra một loạt các vấn đề khi bạn phân loại một cách bất cẩn.

Kế thừa thực sự độc lập với đa hình. Với một số thứ "T" có tham chiếu đến chính nó, sự kế thừa xảy ra khi bạn tạo một thứ mới "S" từ "T" thay thế tham chiếu của "T" bằng tham chiếu đến "S". Định nghĩa đó là mơ hồ, vì sự kế thừa có thể xảy ra trong nhiều tình huống, nhưng phổ biến nhất là phân lớp một đối tượng có tác dụng thay thế thiscon trỏ được gọi bởi các hàm ảo bằng thiscon trỏ thành kiểu con.

Kế thừa là một điều nguy hiểm giống như tất cả những thứ rất mạnh mẽ, sự thừa kế có sức mạnh gây ra sự tàn phá. Ví dụ, giả sử bạn ghi đè một phương thức khi kế thừa từ một lớp nào đó: tất cả đều tốt và tốt cho đến khi một phương thức khác của lớp đó giả định phương thức bạn kế thừa để hành xử theo một cách nhất định, sau tất cả đó là cách tác giả của lớp ban đầu thiết kế nó . Bạn có thể bảo vệ một phần chống lại điều này bằng cách khai báo tất cả các phương thức được gọi bởi một phương thức khác là riêng tư hoặc không ảo (cuối cùng), trừ khi chúng được thiết kế để bị ghi đè. Ngay cả điều này mặc dù không phải lúc nào cũng đủ tốt. Đôi khi bạn có thể thấy một cái gì đó như thế này (trong Java giả, hy vọng có thể đọc được cho người dùng C ++ và C #)

interface UsefulThingsInterface {
    void doThings();
    void doMoreThings();
}

...

class WayOfDoingUsefulThings implements UsefulThingsInterface{
     private foo stuff;
     public final int getStuff();
     void doThings(){
       //modifies stuff, such that ...
       ...
     }
     ...
     void doMoreThings(){
       //ignores stuff
       ...
     }
 }

bạn nghĩ rằng điều này thật đáng yêu và có cách làm "việc" của riêng bạn, nhưng bạn sử dụng tính kế thừa để có được khả năng thực hiện "nhiều thứ" hơn,

class MyUsefulThings extends WayOfDoingUsefulThings{
     void doThings {
        //my way
     }
}

Và tất cả đều tốt và tốt. WayOfDoingUsefulThingsđược thiết kế theo cách thay thế một phương pháp không làm thay đổi ngữ nghĩa của bất kỳ phương pháp nào khác ... ngoại trừ chờ đợi, không có phương pháp nào. Nó trông giống như nó, nhưng doThingsthay đổi trạng thái có thể thay đổi quan trọng. Vì vậy, mặc dù nó không gọi bất kỳ chức năng nào có thể ghi đè,

 void dealWithStuff(WayOfDoingUsefulThings bar){
     bar.doThings()
     use(bar.getStuff());
 }

bây giờ làm một cái gì đó khác với dự kiến ​​khi bạn vượt qua nó a MyUsefulThings. Tệ hơn nữa, bạn thậm chí có thể không biết rằng WayOfDoingUsefulThingsđã thực hiện những lời hứa đó. Có thể dealWithStuffđến từ cùng một thư viện WayOfDoingUsefulThingsgetStuff()thậm chí không được thư viện xuất ra (nghĩ về các lớp bạn bè trong C ++). Tệ hơn nữa, bạn đã đánh bại kiểm tra tĩnh của ngôn ngữ mà không nhận ra nó: dealWithStuffmất một WayOfDoingUsefulThingschỉ để chắc chắn rằng nó sẽ có một getStuff()chức năng mà cư xử một cách nào đó.

Sử dụng thành phần

class MyUsefulThings implements UsefulThingsInterface{
     private way = new WayOfDoingUsefulThings()
     void doThings() {
        //my way
     }
     void doMoreThings() {
        this.way.doMoreThings();
     }
}

mang lại an toàn kiểu tĩnh. Trong thành phần chung là dễ sử dụng và an toàn hơn so với kế thừa khi thực hiện phân nhóm. Nó cũng cho phép bạn ghi đè các phương thức cuối cùng, điều đó có nghĩa là bạn nên thoải mái khai báo mọi thứ cuối cùng / không ảo ngoại trừ trong phần lớn thời gian.

Trong một thế giới tốt hơn, các ngôn ngữ sẽ tự động chèn bản tóm tắt với một delegationtừ khóa. Hầu hết không, vì vậy một nhược điểm là các lớp lớn hơn. Mặc dù, bạn có thể yêu cầu IDE của bạn viết ví dụ ủy nhiệm cho bạn.

Bây giờ, cuộc sống không chỉ là về đa hình. Bạn không cần phải phân nhóm mọi lúc. Mục tiêu của đa hình nói chung là tái sử dụng mã nhưng đó không phải là cách duy nhất để đạt được mục tiêu đó. Thông thường, nó có ý nghĩa để sử dụng thành phần, mà không có đa hình phụ, như một cách quản lý chức năng.

Ngoài ra, kế thừa hành vi có công dụng của nó. Đó là một trong những ý tưởng mạnh mẽ nhất trong khoa học máy tính. Chỉ có điều, hầu hết thời gian, các ứng dụng OOP tốt có thể được viết chỉ bằng cách sử dụng kế thừa giao diện và các tác phẩm. Hai nguyên tắc

  1. Cấm thừa kế hoặc thiết kế cho nó
  2. Thích thành phần

là một hướng dẫn tốt cho những lý do trên và không phải chịu bất kỳ chi phí đáng kể nào.


4
Câu trả lời tốt đẹp. Tôi sẽ tóm tắt rằng cố gắng để đạt được tái sử dụng mã với thừa kế là đường dẫn sai. Kế thừa là ràng buộc rất mạnh (imho thêm "sức mạnh" là tương tự sai!) Và tạo ra sự phụ thuộc mạnh mẽ giữa các lớp được kế thừa. Quá nhiều phụ thuộc = mã xấu :) Vì vậy, tính kế thừa (còn gọi là "hành xử như") thường tỏa sáng cho các giao diện hợp nhất (= ẩn sự phức tạp của các lớp được kế thừa), đối với bất kỳ điều gì khác hãy nghĩ hai lần hoặc sử dụng thành phần ...
MaR

2
Đây là một câu trả lời tốt. +1. Ít nhất trong mắt tôi, có vẻ như sự phức tạp thêm có thể là một chi phí đáng kể, và điều này đã khiến cá nhân tôi có chút do dự khi đưa ra một vấn đề lớn từ việc thích sáng tác. Đặc biệt là khi bạn đang cố gắng tạo ra nhiều mã thân thiện với thử nghiệm đơn vị thông qua giao diện, thành phần và DI (tôi biết điều này đang thêm vào những thứ khác), có vẻ rất dễ dàng để khiến ai đó đi qua một số tệp khác nhau để tìm kiếm rất nhiều vài chi tiết Tại sao điều này không được đề cập thường xuyên hơn, ngay cả khi chỉ xử lý các thành phần trên nguyên tắc thiết kế thừa kế?
Panzercrisis

27

Lý do mọi người nói rằng đó là vì các lập trình viên OOP mới bắt đầu, thoát khỏi các bài giảng kế thừa đa hình của họ, có xu hướng viết các lớp lớn với nhiều phương pháp đa hình, và rồi ở đâu đó trên đường, họ kết thúc với một mớ hỗn độn không thể nhầm lẫn.

Một ví dụ điển hình đến từ thế giới phát triển trò chơi. Giả sử bạn có một lớp cơ sở cho tất cả các thực thể trò chơi của mình - tàu vũ trụ, quái vật, đạn, v.v. của người chơi; mỗi loại thực thể có lớp con riêng của nó. Cách tiếp cận thừa kế sẽ sử dụng một vài phương pháp đa hình, ví dụ như update_controls(), update_physics(), draw(), vv, và thực hiện chúng cho mỗi lớp. Tuy nhiên, điều này có nghĩa là bạn đang kết hợp chức năng không liên quan: không liên quan đến vật thể trông như thế nào khi di chuyển nó và bạn không cần biết gì về AI của nó để vẽ nó. Thay vào đó, cách tiếp cận thành phần định nghĩa một số lớp cơ sở (hoặc giao diện), ví dụ EntityBrain(lớp con triển khai AI hoặc đầu vào của người chơi), EntityPhysics(lớp con thực hiện chuyển động vật lý) và EntityPainter(lớp con đảm nhiệm việc vẽ) và lớp không đa hìnhEntitygiữ một ví dụ của mỗi. Bằng cách này, bạn có thể kết hợp bất kỳ sự xuất hiện nào với bất kỳ mô hình vật lý nào và bất kỳ AI nào, và vì bạn tách chúng ra, mã của bạn cũng sẽ sạch hơn rất nhiều. Ngoài ra, các vấn đề như "Tôi muốn một con quái vật trông giống quái vật bóng bay ở cấp 1, nhưng hành xử như chú hề điên ở cấp 15" biến mất: bạn chỉ cần lấy các thành phần phù hợp và dán chúng lại với nhau.

Lưu ý rằng phương pháp thành phần vẫn sử dụng tính kế thừa trong mỗi thành phần; mặc dù lý tưởng là bạn chỉ sử dụng giao diện và triển khai chúng ở đây.

"Tách các mối quan tâm" là cụm từ chính ở đây: đại diện cho vật lý, thực hiện AI và vẽ một thực thể, là ba mối quan tâm, kết hợp chúng thành một thực thể là một thứ tư. Với cách tiếp cận thành phần, mỗi mối quan tâm được mô hình hóa thành một lớp.


1
Đây là tất cả tốt, và ủng hộ trong bài viết được liên kết trong câu hỏi ban đầu. Tôi rất thích (bất cứ khi nào bạn có thời gian) nếu bạn có thể cung cấp một ví dụ đơn giản liên quan đến tất cả các thực thể này, trong khi vẫn duy trì tính đa hình (trong C ++). Hoặc chỉ cho tôi một tài nguyên. Cảm ơn.
MustafaM

Tôi biết câu trả lời của bạn rất cũ nhưng tôi đã làm chính xác như bạn đã đề cập trong trò chơi của tôi và bây giờ sẽ sáng tác theo phương pháp kế thừa.
Sneh

"Tôi muốn một con quái vật trông giống như ..." điều này có ý nghĩa như thế nào? Một giao diện không cung cấp bất kỳ triển khai nào, bạn sẽ phải sao chép giao diện và mã hành vi theo cách này hay cách khác
NikkyD

1
@NikkyD Bạn lấy MonsterVisuals balloonVisualsMonsterBehaviour crazyClownBehaviourkhởi tạo a Monster(balloonVisuals, crazyClownBehaviour), cùng với Monster(balloonVisuals, balloonBehaviour)và các Monster(crazyClownVisuals, crazyClownBehaviour)trường hợp đã được khởi tạo ở cấp 1 và cấp 15
Caleth

13

Ví dụ bạn đã đưa ra là một trong đó thừa kế là sự lựa chọn tự nhiên. Tôi không nghĩ ai sẽ cho rằng sáng tác luôn là lựa chọn tốt hơn thừa kế - đó chỉ là một hướng dẫn có nghĩa là thường lắp ráp một số đối tượng tương đối đơn giản hơn là tạo ra nhiều đối tượng chuyên môn cao.

Ủy quyền là một ví dụ về cách sử dụng thành phần thay vì kế thừa. Phân quyền cho phép bạn sửa đổi hành vi của một lớp mà không cần phân lớp. Hãy xem xét một lớp cung cấp kết nối mạng, NetStream. Có thể là tự nhiên khi phân lớp NetStream để thực hiện một giao thức mạng chung, vì vậy bạn có thể đến với FTPStream và HTTPStream. Nhưng thay vì tạo một lớp con HTTPStream rất cụ thể cho một mục đích duy nhất, giả sử, UpdateMyWebServiceHTTPStream, tốt hơn là sử dụng một phiên bản HTTPStream cũ đơn giản cùng với một đại biểu biết phải làm gì với dữ liệu mà nó nhận được từ đối tượng đó. Một lý do tốt hơn là nó tránh được sự gia tăng của các lớp học phải được duy trì nhưng bạn sẽ không bao giờ có thể sử dụng lại.


11

Bạn sẽ thấy chu trình này rất nhiều trong diễn ngôn phát triển phần mềm:

  1. Một số tính năng hoặc mẫu (hãy gọi nó là "Mẫu X") được phát hiện là hữu ích cho một số mục đích cụ thể. Các bài đăng trên blog được viết ra những ưu điểm về Mẫu X.

  2. Sự cường điệu khiến một số người nghĩ rằng bạn nên sử dụng Mẫu X bất cứ khi nào có thể .

  3. Những người khác cảm thấy khó chịu khi thấy Mẫu X được sử dụng trong các bối cảnh không phù hợp và họ viết các bài đăng trên blog nói rằng bạn không nên luôn luôn sử dụng Mẫu X và nó có hại trong một số bối cảnh.

  4. Phản ứng dữ dội khiến một số người tin rằng Mẫu X luôn có hại và không bao giờ nên được sử dụng.

Bạn sẽ thấy chu trình thổi phồng / phản ứng dữ dội này xảy ra với hầu hết mọi tính năng từ GOTOmẫu đến SQL sang NoQuery và, vâng, có, kế thừa. Các thuốc giải độc là luôn luôn xem xét bối cảnh .

Circlenguồn gốc từ chính xácShape là cách thừa kế được sử dụng trong các ngôn ngữ OO hỗ trợ kế thừa.

Quy tắc "thích sáng tác hơn thừa kế" thực sự sai lệch nếu không có ngữ cảnh. Bạn nên ưu tiên thừa kế khi kế thừa phù hợp hơn, nhưng thích thành phần khi thành phần phù hợp hơn. Câu này hướng tới những người ở giai đoạn 2 trong chu kỳ cường điệu, những người nghĩ rằng kế thừa nên được sử dụng ở mọi nơi. Nhưng chu kỳ đã tiếp tục, và ngày nay nó dường như khiến một số người nghĩ rằng thừa kế có phần xấu.

Hãy nghĩ về nó như một cái búa so với một cái tuốc nơ vít. Bạn có nên thích một tuốc nơ vít hơn một cái búa? Câu hỏi không có ý nghĩa. Bạn nên sử dụng công cụ phù hợp với công việc, và tất cả phụ thuộc vào nhiệm vụ bạn cần hoàn thành.

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.