Thích thành phần hơn thừa kế?


1604

Tại sao thích thành phần hơn thừa kế? Có sự đánh đổi nào cho mỗi cách tiếp cận? Khi nào bạn nên chọn thừa kế trên thành phần?



40
Có một bài viết hay về câu hỏi đó ở đây . Ý kiến ​​cá nhân của tôi là không có nguyên tắc "tốt hơn" hay "tệ hơn" để thiết kế. Có thiết kế "phù hợp" và "không đầy đủ" cho nhiệm vụ cụ thể. Nói cách khác - tôi sử dụng cả thừa kế hoặc thành phần, tùy thuộc vào tình huống. Mục tiêu là tạo ra mã nhỏ hơn, dễ đọc hơn, tái sử dụng và cuối cùng mở rộng hơn nữa.
m_pGladinator

1
kế thừa trong một câu là công khai nếu bạn có một phương thức công khai và bạn thay đổi nó sẽ thay đổi api đã xuất bản. nếu bạn có thành phần và đối tượng sáng tác đã thay đổi, bạn không phải thay đổi api đã xuất bản của mình.
Tomer Ben David

Câu trả lời:


1188

Thích thành phần hơn kế thừa vì nó dễ uốn hơn / dễ sửa đổi sau này, nhưng không sử dụng cách tiếp cận soạn thảo luôn. Với thành phần, thật dễ dàng để thay đổi hành vi một cách nhanh chóng với Dependency Injection / Setters. Kế thừa cứng nhắc hơn vì hầu hết các ngôn ngữ không cho phép bạn xuất phát từ nhiều hơn một loại. Vì vậy, con ngỗng ít nhiều được nấu chín sau khi bạn xuất phát từ TypeA.

Xét nghiệm axit của tôi cho ở trên là:

  • Có phải TypeB muốn hiển thị giao diện hoàn chỉnh (tất cả các phương thức công khai không kém) của TypeA sao cho TypeB có thể được sử dụng ở nơi TypeA được mong đợi? Biểu thị sự kế thừa .

    • ví dụ: Biplane Cessna sẽ hiển thị giao diện hoàn chỉnh của máy bay, nếu không muốn nói là nhiều hơn. Vì vậy, nó làm cho nó phù hợp để xuất phát từ Máy bay.
  • Có phải TypeB chỉ muốn một số / một phần hành vi được TypeA thể hiện? Chỉ ra sự cần thiết cho Thành phần.

    • ví dụ: Chim có thể chỉ cần hành vi bay của Máy bay. Trong trường hợp này, sẽ hợp lý khi trích xuất nó ra dưới dạng giao diện / lớp / cả hai và biến nó thành thành viên của cả hai lớp.

Cập nhật: Chỉ cần quay lại câu trả lời của tôi và có vẻ như bây giờ nó chưa hoàn chỉnh mà không đề cập cụ thể đến Nguyên tắc thay thế Liskov của Barbara Liskov như một thử nghiệm cho 'Tôi có nên thừa hưởng từ loại này không?'


81
Ví dụ thứ hai là ra khỏi cuốn sách Thiết kế đầu tiên ( amazon.com/First-Design-Potypes-Elisabeth-Freeman/dp/iêu ) :) Tôi rất muốn giới thiệu cuốn sách đó cho bất kỳ ai đang tìm hiểu câu hỏi này.
Jeshurun

4
Điều đó rất rõ ràng, nhưng nó có thể bỏ lỡ điều gì đó: "Có phải TypeB muốn phơi bày giao diện hoàn chỉnh (tất cả các phương thức công khai không kém) của TypeA sao cho TypeB có thể được sử dụng ở nơi TypeA được mong đợi?" Nhưng nếu điều này là đúng và TypeB cũng phơi bày giao diện hoàn chỉnh của TypeC thì sao? Và nếu TypeC chưa được mô hình hóa thì sao?
Tristan

4
Bạn ám chỉ những gì tôi nghĩ nên là thử nghiệm cơ bản nhất: "Đối tượng này có thể sử dụng được bằng mã mà mong đợi các đối tượng của (loại sẽ là) loại cơ sở". Nếu câu trả lời là có, đối tượng phải kế thừa. Nếu không, thì có lẽ không nên. Nếu tôi có máy khoan của mình, các ngôn ngữ sẽ cung cấp một từ khóa để chỉ "lớp này" và cung cấp một phương tiện để xác định một lớp sẽ hoạt động giống như một lớp khác, nhưng không thể thay thế cho nó (một lớp như vậy sẽ có tất cả " tham chiếu lớp "thay thế bằng chính nó).
supercat

22
@Alexey - vấn đề là 'Tôi có thể chuyển một máy bay Cessna cho tất cả các khách hàng mong đợi một chiếc máy bay mà không làm họ ngạc nhiên không?'. Nếu có, thì rất có thể bạn muốn thừa kế.
Gishu

9
Tôi thực sự đang vật lộn để nghĩ về bất kỳ ví dụ nào mà sự kế thừa sẽ là câu trả lời của tôi, tôi thường tìm thấy sự tổng hợp, thành phần và giao diện dẫn đến các giải pháp thanh lịch hơn. Nhiều ví dụ trên có thể được giải thích tốt hơn bằng cách sử dụng các phương pháp đó ...
Stuart Wakefield

414

Hãy nghĩ về ngăn chặn như là có một mối quan hệ. Một chiếc xe "có" động cơ, một người "có" tên, v.v.

Hãy nghĩ về thừa kế như một mối quan hệ. Một chiếc xe "là một" phương tiện, một người "là một" động vật có vú, v.v.

Tôi không có tín dụng cho phương pháp này. Tôi lấy nó trực tiếp từ phiên bản thứ hai của Code Complete bởi Steve McConnell , mục 6.3 .


107
Đây không phải lúc nào cũng là một cách tiếp cận hoàn hảo, nó chỉ đơn giản là một hướng dẫn tốt. Nguyên tắc thay thế Liskov chính xác hơn nhiều (thất bại ít hơn).
Bill K

41
"Xe của tôi có một chiếc xe." Nếu bạn coi đó là một câu riêng biệt, không phải trong một bối cảnh lập trình, điều đó hoàn toàn không có ý nghĩa. Và đó là toàn bộ quan điểm của kỹ thuật này. Nếu nó có vẻ khó xử, có lẽ là sai.
Nick Zalutskiy

36
@Nick Chắc chắn, nhưng "Xe của tôi có XeBehavior" có ý nghĩa hơn (tôi đoán lớp "Xe" của bạn có thể được đặt tên là "XeBehavior"). Vì vậy, bạn không thể căn cứ vào quyết định của mình về việc "so sánh" với "so sánh", bạn phải sử dụng LSP hoặc bạn sẽ phạm sai lầm
Tristan

35
Thay vì "là một" nghĩ về "hành xử như thế nào." Kế thừa là về kế thừa hành vi, không chỉ là ngữ nghĩa.
ybakos

4
@ybakos "Hành vi như" có thể đạt được thông qua các giao diện mà không cần kế thừa. Từ Wikipedia : "Việc triển khai thành phần trên thừa kế thường bắt đầu bằng việc tạo ra các giao diện khác nhau đại diện cho các hành vi mà hệ thống phải thể hiện ... Do đó, các hành vi hệ thống được thực hiện mà không cần kế thừa."
DavidRR

210

Nếu bạn hiểu sự khác biệt, nó dễ giải thích hơn.

Mã thủ tục

Một ví dụ về điều này là PHP mà không sử dụng các lớp (đặc biệt là trước PHP5). Tất cả logic được mã hóa trong một tập hợp các chức năng. Bạn có thể bao gồm các tệp khác chứa các hàm trợ giúp, v.v. và tiến hành logic nghiệp vụ của bạn bằng cách truyền dữ liệu xung quanh trong các hàm. Điều này có thể rất khó quản lý khi ứng dụng phát triển. PHP5 cố gắng khắc phục điều này bằng cách cung cấp nhiều thiết kế hướng đối tượng hơn.

Di sản

Điều này khuyến khích việc sử dụng các lớp học. Kế thừa là một trong ba nguyên lý của thiết kế OO (kế thừa, đa hình, đóng gói).

class Person {
   String Title;
   String Name;
   Int Age
}

class Employee : Person {
   Int Salary;
   String Title;
}

Đây là sự kế thừa trong công việc. Nhân viên "là" Người hoặc thừa kế từ Người. Tất cả các mối quan hệ thừa kế là mối quan hệ "là-a". Nhân viên cũng phủ bóng thuộc tính Tiêu đề từ Người, nghĩa là Nhân viên. Tiêu đề sẽ trả lại Tiêu đề cho Nhân viên chứ không phải Người.

Thành phần

Thành phần được ưa chuộng hơn thừa kế. Nói một cách đơn giản, bạn sẽ có:

class Person {
   String Title;
   String Name;
   Int Age;

   public Person(String title, String name, String age) {
      this.Title = title;
      this.Name = name;
      this.Age = age;
   }

}

class Employee {
   Int Salary;
   private Person person;

   public Employee(Person p, Int salary) {
       this.person = p;
       this.Salary = salary;
   }
}

Person johnny = new Person ("Mr.", "John", 25);
Employee john = new Employee (johnny, 50000);

Thành phần thường là mối quan hệ "có" hoặc "sử dụng". Ở đây, lớp Nhân viên có một Người. Nó không kế thừa từ Person mà thay vào đó, đối tượng Person được truyền cho nó, đó là lý do tại sao nó "có" Person.

Thành phần trên kế thừa

Bây giờ nói rằng bạn muốn tạo một loại Trình quản lý để bạn kết thúc với:

class Manager : Person, Employee {
   ...
}

Ví dụ này sẽ hoạt động tốt, tuy nhiên, nếu cả Người và Nhân viên đều khai báo Titlethì sao? Người quản lý nên trả lại "Người quản lý hoạt động" hay "Ông"? Theo thành phần mơ hồ này được xử lý tốt hơn:

Class Manager {
   public string Title;
   public Manager(Person p, Employee e)
   {
      this.Title = e.Title;
   }
}

Đối tượng Manager được cấu thành như một nhân viên và một người. Hành vi Tiêu đề được lấy từ nhân viên. Thành phần rõ ràng này loại bỏ sự mơ hồ trong số những thứ khác và bạn sẽ gặp ít lỗi hơn.


6
Đối với sự kế thừa: Không có sự mơ hồ. Bạn đang thực hiện lớp Manager dựa trên yêu cầu. Vì vậy, bạn sẽ trả về "Trình quản lý hoạt động" nếu đó là những gì bạn yêu cầu đã chỉ định, nếu không bạn sẽ chỉ sử dụng triển khai của lớp cơ sở. Ngoài ra, bạn có thể biến Person thành một lớp trừu tượng và do đó đảm bảo các lớp dòng xuống thực hiện một thuộc tính Title.
Raj Rao

68
Điều quan trọng cần nhớ là người ta có thể nói "Thành phần trên sự kế thừa" nhưng điều đó không có nghĩa là "Thành phần luôn vượt quá sự kế thừa". "Is a" có nghĩa là sự kế thừa và dẫn đến việc sử dụng lại mã. Nhân viên là một người (Nhân viên không có một người).
Raj Rao

36
Ví dụ khó hiểu.Employee là một người, vì vậy nó nên sử dụng tính kế thừa. Bạn không nên sử dụng thành phần cho ví dụ này, vì đó là mối quan hệ sai trong mô hình miền, ngay cả khi về mặt kỹ thuật bạn có thể khai báo nó trong mã.
Michael Freidgeim

15
Tôi không đồng ý với ví dụ này. Một nhân viên là một người, đó là một trường hợp sách giáo khoa về sử dụng quyền thừa kế hợp lý. Tôi cũng nghĩ rằng "vấn đề" việc xác định lại trường Tiêu đề không có ý nghĩa gì. Thực tế là Employee.Title bóng tối Person.Title là một dấu hiệu của lập trình kém. Rốt cuộc, là "Mr." và "Người quản lý hoạt động" thực sự đề cập đến cùng một khía cạnh của một người (chữ thường)? Tôi sẽ đổi tên Employee.Title và do đó có thể tham chiếu các thuộc tính Title và JobTitle của một nhân viên, cả hai đều có ý nghĩa trong cuộc sống thực. Hơn nữa, không có lý do gì cho Người quản lý (tiếp tục ...)
Radon Rosborough

9
(... Tiếp tục) để thừa kế từ cả Người và Nhân viên - sau tất cả, Nhân viên đã thừa kế từ Người. Trong các mô hình phức tạp hơn, trong đó một người có thể là Người quản lý và Đại lý, đúng là có thể sử dụng nhiều kế thừa (một cách cẩn thận!), Nhưng trong nhiều môi trường sẽ có lớp Vai trò trừu tượng mà Người quản lý (chứa Nhân viên Người đó quản lý) và Đại lý (có Hợp đồng và các thông tin khác) kế thừa. Sau đó, một Nhân viên là - Một người có nhiều Vai trò. Vì vậy, cả thành phần và kế thừa được sử dụng đúng.
Radon Rosborough

142

Với tất cả những lợi ích không thể phủ nhận được cung cấp bởi thừa kế, đây là một số nhược điểm của nó.

Nhược điểm của Kế thừa:

  1. Bạn không thể thay đổi việc triển khai được kế thừa từ các siêu lớp trong thời gian chạy (rõ ràng vì tính kế thừa được xác định tại thời gian biên dịch).
  2. Kế thừa hiển thị một lớp con để biết chi tiết về việc triển khai lớp cha của nó, đó là lý do tại sao người ta thường nói rằng kế thừa phá vỡ sự đóng gói (theo nghĩa là bạn thực sự cần phải tập trung vào các giao diện không thực hiện, do đó, việc sử dụng lại lớp phụ không phải luôn được ưu tiên).
  3. Sự kết hợp chặt chẽ được cung cấp bởi sự kế thừa làm cho việc triển khai một lớp con rất gắn kết với việc thực hiện một siêu lớp mà bất kỳ thay đổi nào trong việc thực hiện cha mẹ sẽ buộc lớp con thay đổi.
  4. Tái sử dụng quá mức bằng cách phân lớp phụ có thể làm cho ngăn xếp thừa kế rất sâu và cũng rất khó hiểu.

Mặt khác Thành phần đối tượng được xác định trong thời gian chạy thông qua các đối tượng có được các tham chiếu đến các đối tượng khác. Trong trường hợp như vậy, các đối tượng này sẽ không bao giờ có thể truy cập dữ liệu được bảo vệ của nhau (không bị phá vỡ đóng gói) và sẽ buộc phải tôn trọng giao diện của nhau. Và trong trường hợp này cũng vậy, sự phụ thuộc thực hiện sẽ ít hơn rất nhiều so với trường hợp thừa kế.


5
Theo ý kiến ​​của tôi, đây là một trong những câu trả lời tốt hơn - tôi sẽ nói thêm rằng cố gắng nghĩ lại các vấn đề của bạn về mặt sáng tác, theo kinh nghiệm của tôi, có xu hướng dẫn đến các lớp nhỏ hơn, đơn giản hơn, khép kín hơn, dễ sử dụng hơn , với phạm vi trách nhiệm rõ ràng hơn, nhỏ hơn, tập trung hơn. Thông thường, điều này có nghĩa là ít cần những thứ như tiêm phụ thuộc hoặc chế nhạo (trong các thử nghiệm) vì các thành phần nhỏ hơn thường có thể tự đứng vững. Chỉ là kinh nghiệm của tôi. YMMV :-)
mindplay.dk

3
Đoạn cuối trong bài này thực sự đã nhấp cho tôi. Cảm ơn bạn.
Salx

87

Một lý do khác, rất thực tế, để thích sáng tác hơn kế thừa phải làm với mô hình miền của bạn và ánh xạ nó tới cơ sở dữ liệu quan hệ. Thật sự rất khó để ánh xạ sự kế thừa cho mô hình SQL (bạn kết thúc với tất cả các cách giải quyết khó khăn, như tạo các cột không luôn được sử dụng, sử dụng các khung nhìn, v.v.). Một số ORML cố gắng giải quyết vấn đề này, nhưng nó luôn trở nên phức tạp nhanh chóng. Thành phần có thể dễ dàng được mô hình hóa thông qua mối quan hệ khóa ngoài giữa hai bảng, nhưng kế thừa khó hơn nhiều.


81

Mặc dù nói ngắn gọn là tôi đồng ý với "Thích thành phần hơn thừa kế", nhưng đối với tôi, nó thường giống như "thích khoai tây hơn coca-cola". Có nơi để thừa kế và nơi để sáng tác. Bạn cần hiểu sự khác biệt, sau đó câu hỏi này sẽ biến mất. Điều thực sự có ý nghĩa với tôi là "nếu bạn định sử dụng tính kế thừa - hãy nghĩ lại, rất có thể bạn cần sáng tác".

Bạn nên thích khoai tây hơn coca cola khi bạn muốn ăn, và coca cola hơn khoai tây khi bạn muốn uống.

Tạo một lớp con có ý nghĩa nhiều hơn là một cách thuận tiện để gọi các phương thức siêu lớp. Bạn nên sử dụng tính kế thừa khi lớp con "is-a" siêu lớp cả về cấu trúc và chức năng, khi nó có thể được sử dụng như siêu lớp và bạn sẽ sử dụng lớp đó. Nếu nó không phải là trường hợp - nó không phải là thừa kế, nhưng một cái gì đó khác. Thành phần là khi các đối tượng của bạn bao gồm một đối tượng khác, hoặc có một số mối quan hệ với chúng.

Vì vậy, đối với tôi có vẻ như nếu ai đó không biết nếu anh ta cần thừa kế hoặc thành phần, vấn đề thực sự là anh ta không biết mình muốn uống hay ăn. Hãy suy nghĩ về miền vấn đề của bạn nhiều hơn, hiểu nó tốt hơn.


5
Công cụ phù hợp cho công việc phù hợp. Một cái búa có thể tốt hơn trong việc đập mọi thứ hơn một chiếc cờ lê, nhưng điều đó không có nghĩa là người ta nên xem một chiếc cờ lê là "một cái búa kém hơn". Kế thừa có thể hữu ích khi những thứ được thêm vào lớp con là cần thiết để đối tượng hoạt động như một đối tượng siêu lớp. Ví dụ, hãy xem xét một lớp cơ sở InternalCombustionEnginevới một lớp dẫn xuất GasolineEngine. Loại thứ hai thêm vào những thứ như bugi, thứ mà lớp cơ sở thiếu, nhưng sử dụng thứ đó như một thứ InternalCombustionEnginesẽ khiến bugi được sử dụng.
supercat

61

Kế thừa là khá hấp dẫn đặc biệt là đến từ đất thủ tục và nó thường trông thanh lịch. Ý tôi là tất cả những gì tôi cần làm là thêm một chút chức năng này vào một số lớp khác, phải không? Chà, một trong những vấn đề là

thừa kế có lẽ là hình thức khớp nối tồi tệ nhất mà bạn có thể có

Lớp cơ sở của bạn phá vỡ đóng gói bằng cách hiển thị chi tiết triển khai cho các lớp con dưới dạng các thành viên được bảo vệ. Điều này làm cho hệ thống của bạn cứng nhắc và dễ vỡ. Tuy nhiên, lỗ hổng bi thảm hơn là lớp con mới mang theo tất cả hành lý và quan điểm của chuỗi thừa kế.

Bài báo, Kế thừa là Ác ma: Thất bại sử thi của DataAnnotationsModelBinder , đi qua một ví dụ về điều này trong C #. Nó cho thấy việc sử dụng tính kế thừa khi thành phần nên được sử dụng và làm thế nào nó có thể được tái cấu trúc.


Kế thừa không phải là tốt hay xấu, đó chỉ là một trường hợp đặc biệt của Thành phần. Trong đó, thực sự, lớp con đang thực hiện một chức năng tương tự như siêu lớp. Nếu lớp con được đề xuất của bạn không triển khai lại mà chỉ sử dụng chức năng của siêu lớp, thì bạn đã sử dụng Kế thừa không chính xác. Đó là sai lầm của lập trình viên, không phải là sự phản ánh về Kế thừa.
iPherian

42

Trong Java hoặc C #, một đối tượng không thể thay đổi kiểu của nó một khi nó đã được khởi tạo.

Vì vậy, nếu đối tượng của bạn cần xuất hiện dưới dạng một đối tượng khác hoặc hành xử khác nhau tùy thuộc vào trạng thái hoặc điều kiện của đối tượng, thì hãy sử dụng Thành phần : Tham khảo Trạng tháiChiến lược Mẫu thiết kế .

Nếu đối tượng cần phải cùng loại, sau đó sử dụng Kế thừa hoặc thực hiện giao diện.


10
+1 Tôi đã tìm thấy ngày càng ít sự kế thừa đó hoạt động trong hầu hết các tình huống. Tôi rất thích giao diện được chia sẻ / kế thừa và thành phần của các đối tượng .... hoặc nó được gọi là tổng hợp? Đừng hỏi tôi, tôi đã có bằng EE !!
kenny

Tôi tin rằng đây là kịch bản phổ biến nhất khi áp dụng "thành phần trên thừa kế" vì cả hai có thể phù hợp về mặt lý thuyết. Ví dụ, trong một hệ thống tiếp thị, bạn có thể có khái niệm về một Client. Sau đó, một khái niệm mới về một PreferredClientcửa sổ bật lên sau này. Có nên PreferredClientthừa kế Client? Một khách hàng ưa thích 'là một' khách hàng sau đó, không? Chà, không nhanh như vậy ... như bạn đã nói các đối tượng không thể thay đổi lớp của chúng khi chạy. Làm thế nào bạn sẽ mô hình hóa các client.makePreferred()hoạt động? Có lẽ câu trả lời nằm ở việc sử dụng sáng tác với một khái niệm còn thiếu, Accountcó lẽ?
plalx

Thay vì có các loại Clientlớp khác nhau , có lẽ chỉ có một lớp gói gọn khái niệm về một lớp Accountcó thể là StandardAccounthoặc PreferredAccount...
plalx

40

Không tìm thấy câu trả lời thỏa đáng ở đây, vì vậy tôi đã viết một câu trả lời mới.

Để hiểu lý do tại sao " thích thành phần hơn thừa kế", trước tiên chúng ta cần lấy lại giả định được bỏ qua trong thành ngữ rút gọn này.

Có hai lợi ích của việc thừa kế: phân nhóm và phân lớp

  1. Subtyping có nghĩa là tuân thủ chữ ký loại (giao diện), tức là một bộ API và người ta có thể ghi đè lên một phần của chữ ký để đạt được tính đa hình của kiểu con.

  2. Phân lớp có nghĩa là tái sử dụng ngầm các triển khai phương thức.

Với hai lợi ích có hai mục đích khác nhau để thực hiện kế thừa: định hướng phân nhóm và định hướng tái sử dụng mã.

Nếu tái sử dụng mã là mục đích duy nhất , thì việc phân lớp có thể cung cấp nhiều hơn những gì anh ta cần, tức là một số phương thức công khai của lớp cha mẹ không có ý nghĩa nhiều đối với lớp con. Trong trường hợp này, thay vì ủng hộ thành phần hơn thừa kế, thành phần được yêu cầu . Đây cũng là nơi mà khái niệm "is-a" vs. "has-a" xuất phát.

Vì vậy, chỉ khi phân nhóm được định hướng, nghĩa là sử dụng lớp mới sau này theo cách đa hình, chúng ta mới phải đối mặt với vấn đề chọn kế thừa hoặc thành phần. Đây là giả định được bỏ qua trong thành ngữ rút gọn đang thảo luận.

Để phân loại là tuân thủ chữ ký loại, điều này có nghĩa là thành phần luôn phải phơi bày không ít số lượng API của loại. Bây giờ giao dịch bắt đầu:

  1. Kế thừa cung cấp việc tái sử dụng mã đơn giản nếu không bị ghi đè, trong khi thành phần phải mã lại mọi API, ngay cả khi đó chỉ là một công việc ủy ​​nhiệm đơn giản.

  2. Kế thừa cung cấp đệ quy mở đơn giản thông qua trang đa hình bên trong this, tức là gọi phương thức ghi đè (hoặc thậm chí loại ) trong một hàm thành viên khác, dù là công khai hay riêng tư (dù không được khuyến khích ). Đệ quy mở có thể được mô phỏng thông qua thành phần , nhưng nó đòi hỏi nỗ lực thêm và có thể không phải lúc nào cũng khả thi (?). Câu trả lời này cho một câu hỏi trùng lặp nói một cái gì đó tương tự.

  3. Kế thừa tiếp xúc với các thành viên được bảo vệ . Điều này phá vỡ sự đóng gói của lớp cha và nếu được sử dụng bởi lớp con, một sự phụ thuộc khác giữa đứa trẻ và cha mẹ của nó được đưa ra.

  4. Thành phần có sự phù hợp của sự đảo ngược của điều khiển và sự phụ thuộc của nó có thể được đưa vào một cách linh hoạt, như được thể hiện trong mẫu trang trímẫu proxy .

  5. Thành phần có lợi ích của lập trình hướng kết hợp , tức là làm việc theo cách giống như mẫu tổng hợp .

  6. Thành phần ngay lập tức sau khi lập trình đến một giao diện .

  7. Thành phần có lợi ích dễ dàng thừa kế nhiều .

Với suy nghĩ về sự đánh đổi ở trên, do đó chúng tôi thích sáng tác hơn kế thừa. Tuy nhiên, đối với các lớp liên quan chặt chẽ, tức là khi sử dụng lại mã ngầm thực sự mang lại lợi ích, hoặc sức mạnh kỳ diệu của đệ quy mở là mong muốn, kế thừa sẽ là lựa chọn.


34

Cá nhân tôi học cách luôn thích sáng tác hơn thừa kế. Không có vấn đề lập trình nào bạn có thể giải quyết với sự kế thừa mà bạn không thể giải quyết bằng thành phần; mặc dù bạn có thể phải sử dụng Giao diện (Java) hoặc Giao thức (Obj-C) trong một số trường hợp. Vì C ++ không biết bất kỳ điều gì như vậy, bạn sẽ phải sử dụng các lớp cơ sở trừu tượng, điều đó có nghĩa là bạn không thể loại bỏ hoàn toàn quyền thừa kế trong C ++.

Thành phần thường logic hơn, nó cung cấp sự trừu tượng tốt hơn, đóng gói tốt hơn, tái sử dụng mã tốt hơn (đặc biệt là trong các dự án rất lớn) và ít có khả năng phá vỡ bất cứ điều gì ở khoảng cách chỉ vì bạn đã thực hiện một thay đổi riêng lẻ ở bất kỳ đâu trong mã của bạn. Nó cũng giúp dễ dàng duy trì " Nguyên tắc trách nhiệm duy nhất ", thường được tóm tắt là " Không bao giờ có nhiều hơn một lý do để một lớp thay đổi. ", Và điều đó có nghĩa là mỗi lớp tồn tại cho một mục đích cụ thể và nó nên chỉ có các phương pháp liên quan trực tiếp đến mục đích của nó. Ngoài ra có một cây thừa kế rất nông giúp việc giữ tổng quan dễ dàng hơn nhiều ngay cả khi dự án của bạn bắt đầu thực sự lớn. Nhiều người nghĩ rằng thừa kế đại diện cho chúng ta thế giới thựckhá tốt, nhưng đó không phải là sự thật. Thế giới thực sử dụng nhiều thành phần hơn là thừa kế. Khá nhiều đối tượng trong thế giới thực mà bạn có thể cầm trên tay đã được cấu thành từ các đối tượng trong thế giới thực nhỏ hơn khác.

Có những nhược điểm của thành phần, mặc dù. Nếu bạn bỏ qua kế thừa hoàn toàn và chỉ tập trung vào thành phần, bạn sẽ nhận thấy rằng bạn thường phải viết một vài dòng mã bổ sung không cần thiết nếu bạn đã sử dụng tính kế thừa. Đôi khi bạn cũng bị buộc phải lặp lại chính mình và điều này vi phạm Nguyên tắc DRY(DRY = Đừng lặp lại chính mình). Ngoài ra thành phần thường yêu cầu ủy quyền và một phương thức chỉ gọi một phương thức khác của đối tượng khác mà không có mã nào khác xung quanh lệnh gọi này. Các "cuộc gọi phương thức kép" như vậy (có thể dễ dàng mở rộng thành các cuộc gọi phương thức ba hoặc bốn lần và thậm chí xa hơn thế) có hiệu suất kém hơn nhiều so với thừa kế, trong đó bạn chỉ cần kế thừa một phương thức của cha mẹ. Gọi một phương thức được kế thừa có thể nhanh như nhau khi gọi một phương thức không được kế thừa hoặc có thể chậm hơn một chút, nhưng thường vẫn nhanh hơn hai lần gọi phương thức liên tiếp.

Bạn có thể nhận thấy rằng hầu hết các ngôn ngữ OO không cho phép nhiều kế thừa. Mặc dù có một vài trường hợp trong đó nhiều kế thừa thực sự có thể mua cho bạn một cái gì đó, nhưng đó là những trường hợp ngoại lệ hơn là quy tắc. Bất cứ khi nào bạn gặp phải một tình huống mà bạn nghĩ rằng "nhiều kế thừa sẽ là một tính năng thực sự tuyệt vời để giải quyết vấn đề này", bạn thường ở một thời điểm mà bạn nên nghĩ lại kế thừa hoàn toàn, vì thậm chí nó có thể cần một vài dòng mã bổ sung , một giải pháp dựa trên thành phần thường sẽ trở thành bằng chứng thanh lịch, linh hoạt và tương lai hơn nhiều.

Kế thừa thực sự là một tính năng thú vị, nhưng tôi e rằng nó đã bị lạm dụng quá mức trong vài năm qua. Mọi người coi di sản là một cây búa có thể đóng đinh tất cả, bất kể đó thực sự là một cái đinh, một cái đinh vít, hoặc có thể là một cái gì đó hoàn toàn khác.


"Nhiều người nghĩ rằng sự kế thừa đại diện cho thế giới thực của chúng ta khá tốt, nhưng đó không phải là sự thật." Rất nhiều điều này! Trái ngược với hầu hết tất cả các hướng dẫn lập trình trên thế giới từ trước đến nay, mô hình hóa các đối tượng trong thế giới thực như chuỗi thừa kế có thể là một ý tưởng tồi trong thời gian dài. Bạn chỉ nên sử dụng quyền thừa kế khi có một mối quan hệ đơn giản, bẩm sinh, đơn giản đến khó tin. Giống như TextFilelà một File.
neonblitzer

25

Nguyên tắc chung của tôi: Trước khi sử dụng tính kế thừa, hãy xem xét nếu thành phần có ý nghĩa hơn.

Lý do: Phân lớp thường có nghĩa là phức tạp và kết nối hơn, nghĩa là khó thay đổi, duy trì và mở rộng quy mô hơn mà không phạm sai lầm.

Một câu trả lời đầy đủ và cụ thể hơn từ Tim Boudreau của Sun:

Các vấn đề phổ biến đối với việc sử dụng thừa kế như tôi thấy đó là:

  • Các hành động vô tội có thể có kết quả không mong muốn - Ví dụ kinh điển về điều này là các cuộc gọi đến các phương thức có thể ghi đè từ hàm tạo của lớp bậc trên, trước khi các trường đối tượng của lớp con đã được khởi tạo. Trong một thế giới hoàn hảo, không ai sẽ làm điều đó. Đây không phải là một thế giới hoàn hảo.
  • Nó đưa ra những cám dỗ đồi trụy cho các lớp con để đưa ra các giả định về thứ tự của các cuộc gọi phương thức và như vậy - các giả định đó có xu hướng không ổn định nếu siêu lớp có thể phát triển theo thời gian. Xem thêm máy nướng bánh mì và nồi cà phê của tôi .
  • Các lớp học trở nên nặng hơn - bạn không nhất thiết phải biết công việc mà siêu lớp của bạn đang làm trong công cụ xây dựng của nó, hoặc nó sẽ sử dụng bao nhiêu bộ nhớ. Vì vậy, việc xây dựng một số vật thể nhẹ vô tội có thể đắt hơn nhiều so với bạn nghĩ và điều này có thể thay đổi theo thời gian nếu siêu lớp tiến hóa
  • Nó khuyến khích một vụ nổ của các lớp con . Tải lớp tốn thời gian, nhiều lớp hơn chi phí bộ nhớ. Đây có thể không phải là vấn đề cho đến khi bạn xử lý một ứng dụng ở quy mô NetBeans, nhưng ở đó, chúng tôi có vấn đề thực sự với, ví dụ, các menu bị chậm vì màn hình đầu tiên của menu kích hoạt tải lớp lớn. Chúng tôi đã sửa lỗi này bằng cách chuyển sang cú pháp khai báo nhiều hơn và các kỹ thuật khác, nhưng cũng tốn thời gian để sửa.
  • Điều này khiến việc thay đổi mọi thứ sau này trở nên khó khăn hơn - nếu bạn tạo một lớp công khai, việc hoán đổi siêu lớp sẽ phá vỡ các lớp con - đó là một lựa chọn mà một khi bạn đã công khai mã, bạn đã kết hôn. Vì vậy, nếu bạn không thay đổi chức năng thực sự cho siêu lớp của mình, bạn sẽ có nhiều tự do hơn để thay đổi mọi thứ sau này nếu bạn sử dụng, thay vì mở rộng thứ bạn cần. Lấy ví dụ, phân lớp JPanel - điều này thường sai; và nếu lớp con được công khai ở đâu đó, bạn sẽ không bao giờ có cơ hội xem lại quyết định đó. Nếu nó được truy cập dưới dạng JComponent getThePanel (), bạn vẫn có thể làm điều đó (gợi ý: hiển thị các mô hình cho các thành phần bên trong dưới dạng API của bạn).
  • Hệ thống phân cấp đối tượng không mở rộng quy mô (hoặc khiến chúng mở rộng quy mô sau khó hơn nhiều so với kế hoạch trước) - đây là vấn đề "quá nhiều lớp" cổ điển. Tôi sẽ đi sâu vào vấn đề này bên dưới và cách mẫu AskTheOracle có thể giải quyết nó (mặc dù nó có thể xúc phạm những người theo chủ nghĩa thuần túy OOP).

...

Tôi sẽ làm gì, nếu bạn cho phép thừa kế, mà bạn có thể dùng với một hạt muối là:

  • Không có trường, bao giờ, ngoại trừ hằng
  • Các phương thức sẽ là trừu tượng hoặc cuối cùng
  • Gọi không có phương thức từ hàm tạo của lớp bậc trên

...

tất cả điều này áp dụng ít hơn cho các dự án nhỏ so với các dự án lớn và ít hơn cho các lớp tư nhân so với các dự án công cộng


25

Tại sao thích thành phần hơn thừa kế?

Xem câu trả lời khác.

Khi nào bạn có thể sử dụng thừa kế?

Người ta thường nói rằng một lớp Barcó thể kế thừa một lớp Fookhi câu sau là đúng:

  1. một quán bar là một foo

Thật không may, bài kiểm tra trên một mình là không đáng tin cậy. Sử dụng như sau thay thế:

  1. một thanh là một foo,
  2. thanh có thể làm mọi thứ mà foos có thể làm.

Các thử nghiệm đảm bảo đầu tiên mà tất cả các thu khí của Foocó ý nghĩa trong Bar(= thuộc tính chia sẻ), trong khi thử nghiệm thứ hai làm cho chắc chắn rằng tất cả các setters của Foocó ý nghĩa trong Bar(chức năng = chia sẻ).

Ví dụ 1: Chó -> Động vật

Một con chó là một con vật VÀ chó có thể làm mọi thứ mà động vật có thể làm (như thở, chết, v.v.). Do đó, lớp Dog có thể kế thừa lớp Animal.

Ví dụ 2: Vòng tròn - / -> Hình elip

Hình tròn là hình elip NHƯNG hình tròn không thể làm mọi thứ mà hình elip có thể làm. Ví dụ: các vòng tròn không thể kéo dài, trong khi các hình elip thì có thể. Do đó, lớp Circle không thể kế thừa lớpEllipse .

Đây được gọi là vấn đề Circle-Ellipse , thực sự không phải là vấn đề, chỉ là một bằng chứng rõ ràng rằng thử nghiệm đầu tiên thôi không đủ để kết luận rằng có thể thừa kế. Đặc biệt, ví dụ này nhấn mạnh rằng các lớp dẫn xuất nên mở rộng chức năng của các lớp cơ sở, không bao giờ hạn chế nó. Mặt khác, lớp cơ sở không thể được sử dụng đa hình.

Khi nào bạn nên sử dụng thừa kế?

Ngay cả khi bạn có thể sử dụng thừa kế không có nghĩa là bạn nên : sử dụng bố cục luôn là một tùy chọn. Kế thừa là một công cụ mạnh mẽ cho phép tái sử dụng mã ngầm và gửi động, nhưng nó đi kèm với một vài nhược điểm, đó là lý do tại sao thành phần thường được ưa thích. Sự đánh đổi giữa thừa kế và thành phần không rõ ràng, và theo tôi được giải thích rõ nhất trong câu trả lời của lcn .

Theo nguyên tắc thông thường, tôi có xu hướng chọn kế thừa so với thành phần khi việc sử dụng đa hình được dự kiến ​​là rất phổ biến, trong trường hợp đó, sức mạnh của công văn động có thể dẫn đến một API thanh lịch và dễ đọc hơn nhiều. Ví dụ: có một lớp đa hình Widgettrong các khung GUI hoặc một lớp đa hìnhNode trong các thư viện XML cho phép có một API dễ đọc và trực quan hơn nhiều so với những gì bạn có với một giải pháp hoàn toàn dựa trên thành phần.

Nguyên tắc thay thế Liskov

Để bạn biết, một phương pháp khác được sử dụng để xác định xem có thể thừa kế hay không được gọi là Nguyên tắc thay thế Liskov :

Các hàm sử dụng các con trỏ hoặc tham chiếu đến các lớp cơ sở phải có thể sử dụng các đối tượng của các lớp dẫn xuất mà không biết nó

Về cơ bản, điều này có nghĩa là sự kế thừa là có thể nếu lớp cơ sở có thể được sử dụng đa hình, mà tôi tin là tương đương với thử nghiệm của chúng tôi "một thanh là một foo và các thanh có thể làm mọi thứ mà foos có thể làm".


Các kịch bản hình tròn-elip và hình chữ nhật vuông là những ví dụ nghèo nàn. Các lớp con luôn phức tạp hơn so với siêu lớp của chúng, vì vậy vấn đề được đặt ra. Vấn đề này được giải quyết bằng cách đảo ngược mối quan hệ. Một hình elip xuất phát từ một hình tròn và một hình chữ nhật xuất phát từ một hình vuông. Thật vô cùng ngớ ngẩn khi sử dụng bố cục trong các tình huống này.
Logic mờ

@FuzzyLogic Đồng ý, nhưng trên thực tế, bài viết của tôi không bao giờ ủng hộ việc sử dụng sáng tác trong trường hợp này. Tôi chỉ nói rằng vấn đề hình elip hình tròn là một ví dụ tuyệt vời về lý do tại sao "is-a" không phải là một thử nghiệm tốt để kết luận rằng Circle nên xuất phát từ Ellipse. Khi chúng tôi kết luận rằng trên thực tế, Circle không nên xuất phát từ Ellipse do vi phạm LSP, thì các tùy chọn có thể là đảo ngược mối quan hệ, hoặc sử dụng thành phần hoặc sử dụng các lớp mẫu hoặc sử dụng một thiết kế phức tạp hơn liên quan đến các lớp bổ sung hoặc các hàm trợ giúp, vv ... và quyết định rõ ràng nên được đưa ra trên cơ sở từng trường hợp.
Boris Dalstein

1
@FuzzyLogic Và nếu bạn tò mò về những gì tôi sẽ ủng hộ cho trường hợp cụ thể của Circle-Ellipse: Tôi sẽ ủng hộ việc không triển khai lớp Circle. Vấn đề với việc đảo ngược mối quan hệ là nó cũng vi phạm LSP: tưởng tượng chức năng computeArea(Circle* c) { return pi * square(c->radius()); }. Nó rõ ràng bị phá vỡ nếu vượt qua một Ellipse (bán kính () thậm chí có nghĩa là gì?). Hình elip không phải là hình tròn và vì thế không nên xuất phát từ hình tròn.
Boris Dalstein

computeArea(Circle *c) { return pi * width * height / 4.0; }Bây giờ nó chung chung.
Logic mờ

2
@FuzzyLogic Tôi không đồng ý: bạn nhận ra rằng điều này có nghĩa là Vòng tròn lớp dự đoán sự tồn tại của lớp Ellipse, và do đó được cung cấp width()height()? Điều gì sẽ xảy ra nếu bây giờ một người dùng thư viện quyết định tạo một lớp khác gọi là "EggShape"? Nó cũng nên xuất phát từ "Circle"? Dĩ nhiên là không. Hình dạng quả trứng không phải là hình tròn và hình elip cũng không phải là hình tròn, vì vậy không ai có thể xuất phát từ Circle vì nó phá vỡ LSP. Các phương thức thực hiện thao tác trên lớp Circle * đưa ra các giả định mạnh mẽ về vòng tròn là gì và phá vỡ các giả định này gần như chắc chắn sẽ dẫn đến lỗi.
Boris Dalstein

19

Kế thừa là rất mạnh, nhưng bạn không thể ép buộc nó (xem: vấn đề hình tròn-hình elip ). Nếu bạn thực sự không thể hoàn toàn chắc chắn về một mối quan hệ phụ "thực sự", thì tốt nhất nên đi theo bố cục.


15

Kế thừa tạo ra một mối quan hệ mạnh mẽ giữa một lớp con và siêu lớp; lớp con phải nhận thức được các chi tiết triển khai của siêu lớp. Tạo siêu lớp khó hơn nhiều, khi bạn phải suy nghĩ về cách nó có thể được mở rộng. Bạn phải ghi lại các bất biến của lớp một cách cẩn thận và nêu rõ các phương thức khác mà các phương thức overridable sử dụng trong nội bộ.

Kế thừa đôi khi hữu ích, nếu hệ thống phân cấp thực sự đại diện cho một mối quan hệ is-a-. Nó liên quan đến Nguyên tắc Đóng mở, trong đó tuyên bố rằng các lớp nên được đóng để sửa đổi nhưng mở để mở rộng. Bằng cách đó bạn có thể có đa hình; để có một phương thức chung liên quan đến siêu kiểu và các phương thức của nó, nhưng thông qua công văn động, phương thức của lớp con được gọi. Điều này rất linh hoạt và giúp tạo ra sự gián tiếp, điều cần thiết trong phần mềm (để biết ít hơn về chi tiết triển khai).

Kế thừa dễ dàng được sử dụng quá mức, và tạo ra sự phức tạp bổ sung, với sự phụ thuộc cứng giữa các lớp. Ngoài ra, việc hiểu những gì xảy ra trong quá trình thực thi chương trình trở nên khá khó khăn do các lớp và lựa chọn động của các cuộc gọi phương thức.

Tôi sẽ đề nghị sử dụng sáng tác như mặc định. Nó mang tính mô đun hơn và mang lại lợi ích của liên kết muộn (bạn có thể thay đổi thành phần một cách linh hoạt). Ngoài ra, dễ dàng hơn để kiểm tra những thứ riêng biệt. Và nếu bạn cần sử dụng một phương thức từ một lớp, bạn không bị buộc phải ở dạng nhất định (Nguyên tắc thay thế Liskov).


3
Đáng lưu ý rằng thừa kế không phải là cách duy nhất để đạt được đa hình. Mẫu trang trí cung cấp sự xuất hiện của đa hình thông qua thành phần.
BitMask777

1
@ BitMask777: Đa hình phụ chỉ là một loại đa hình, một loại khác sẽ là đa hình tham số, bạn không cần thừa kế cho điều đó. Cũng quan trọng hơn: khi nói về thừa kế, người ta có nghĩa là thừa kế giai cấp; .ie bạn có thể có đa hình kiểu con bằng cách có một giao diện chung cho nhiều lớp và bạn không gặp phải các vấn đề về thừa kế.
egaga

2
@engaga: Tôi diễn giải nhận xét của bạn Inheritance is sometimes useful... That way you can have polymorphismlà khó liên kết các khái niệm về tính kế thừa và đa hình (phân nhóm giả định cho bối cảnh). Nhận xét của tôi nhằm chỉ ra những gì bạn làm rõ trong nhận xét của mình: rằng thừa kế không phải là cách duy nhất để thực hiện đa hình và trên thực tế không nhất thiết là yếu tố quyết định khi quyết định giữa thành phần và kế thừa.
BitMask777

15

Giả sử một chiếc máy bay chỉ có hai phần: động cơ và cánh.
Sau đó, có hai cách để thiết kế một lớp máy bay.

Class Aircraft extends Engine{
  var wings;
}

Bây giờ máy bay của bạn có thể bắt đầu với việc có cánh cố định
và thay đổi chúng thành cánh quay khi đang bay. Nó thực chất
là một động cơ có cánh. Nhưng nếu tôi muốn thay đổi
động cơ khi đang bay thì sao?

Lớp cơ sở trưng Enginera một trình biến đổi để thay đổi các
thuộc tính của nó hoặc tôi thiết kế lại Aircraftthành:

Class Aircraft {
  var wings;
  var engine;
}

Bây giờ, tôi có thể thay thế động cơ của mình khi đang bay.


Bài đăng của bạn đưa ra một điểm mà tôi chưa từng xem xét trước đây - để tiếp tục sự tương tự của bạn về các vật thể cơ học có nhiều bộ phận, trên một thứ giống như súng, thường có một phần được đánh dấu bằng một số sê-ri, có số sê-ri được coi là toàn bộ vũ khí (đối với súng ngắn, nó thường là khung). Người ta có thể thay thế tất cả các bộ phận khác và vẫn có cùng một loại súng, nhưng nếu khung bị nứt và cần phải thay thế, kết quả của việc lắp ráp một khung mới với tất cả các bộ phận khác từ súng ban đầu sẽ là một khẩu súng mới. Lưu ý rằng ...
supercat

... thực tế là nhiều bộ phận của một khẩu súng có thể có số sê-ri được đánh dấu trên chúng không có nghĩa là một khẩu súng có thể có nhiều danh tính. Chỉ có số sê-ri trên khung xác định súng; số sê-ri trên bất kỳ bộ phận nào khác xác định khẩu súng mà các bộ phận đó được sản xuất để lắp ráp, có thể không phải là súng mà chúng được lắp ráp tại bất kỳ thời điểm nào.
supercat


7

Khi bạn muốn "sao chép" / Hiển thị API của lớp cơ sở, bạn sử dụng tính kế thừa. Khi bạn chỉ muốn "sao chép" chức năng, hãy sử dụng ủy quyền.

Một ví dụ về điều này: Bạn muốn tạo một Stack ra khỏi danh sách. Stack chỉ có pop, đẩy và nhìn trộm. Bạn không nên sử dụng tính kế thừa cho rằng bạn không muốn Push_back, push_front, removeAt, et al. - loại chức năng trong Stack.


7

Hai cách này có thể sống tốt với nhau và thực sự hỗ trợ lẫn nhau.

Thành phần chỉ là chơi mô-đun: bạn tạo giao diện tương tự như lớp cha, tạo đối tượng mới và ủy quyền các cuộc gọi cho nó. Nếu các đối tượng này không cần biết về nhau, thì nó khá an toàn và dễ sử dụng. Có rất nhiều possibilites ở đây.

Tuy nhiên, nếu lớp cha vì một số lý do cần truy cập các hàm do "lớp con" cung cấp cho lập trình viên thiếu kinh nghiệm thì có thể đó là một nơi tuyệt vời để sử dụng tính kế thừa. Lớp cha chỉ có thể gọi nó là "foo ()" trừu tượng được ghi đè bởi lớp con và sau đó nó có thể cung cấp giá trị cho cơ sở trừu tượng.

Nó có vẻ là một ý tưởng hay, nhưng trong nhiều trường hợp, tốt hơn là chỉ cung cấp cho lớp một đối tượng thực hiện foo () (hoặc thậm chí đặt giá trị được cung cấp foo () theo cách thủ công) hơn là kế thừa lớp mới từ một số lớp cơ sở yêu cầu hàm foo () được chỉ định.

Tại sao?

Bởi vì thừa kế là một cách di chuyển thông tin kém .

Thành phần có một lợi thế thực sự ở đây: mối quan hệ có thể được đảo ngược: "lớp cha" hoặc "nhân viên trừu tượng" có thể tổng hợp bất kỳ đối tượng "con" cụ thể nào thực hiện giao diện nhất định + bất kỳ đứa trẻ nào cũng có thể được đặt bên trong bất kỳ loại cha mẹ nào khác, chấp nhận nó là loại . Và có thể có bất kỳ số lượng đối tượng nào, ví dụ MergeSort hoặc QuickSort có thể sắp xếp bất kỳ danh sách các đối tượng thực hiện So sánh-giao diện trừu tượng. Hoặc nói một cách khác: bất kỳ nhóm đối tượng nào thực hiện "foo ()" và nhóm đối tượng khác có thể sử dụng các đối tượng có "foo ()" đều có thể chơi cùng nhau.

Tôi có thể nghĩ ra ba lý do thực sự để sử dụng thừa kế:

  1. Bạn có nhiều lớp với cùng một giao diện và bạn muốn tiết kiệm thời gian viết chúng
  2. Bạn phải sử dụng cùng một lớp cơ sở cho từng đối tượng
  3. Bạn cần sửa đổi các biến riêng tư, không thể công khai trong mọi trường hợp

Nếu những điều này là đúng, thì có lẽ cần phải sử dụng tính kế thừa.

Không có gì xấu khi sử dụng lý do 1, điều rất tốt là có một giao diện vững chắc trên các đối tượng của bạn. Điều này có thể được thực hiện bằng cách sử dụng thành phần hoặc với sự kế thừa, không có vấn đề gì - nếu giao diện này đơn giản và không thay đổi. Thông thường thừa kế là khá hiệu quả ở đây.

Nếu lý do là số 2, nó có một chút khó khăn. Bạn có thực sự chỉ cần sử dụng cùng một lớp cơ sở? Nói chung, chỉ sử dụng cùng một lớp cơ sở là không đủ, nhưng nó có thể là một yêu cầu của khung của bạn, một sự cân nhắc thiết kế không thể tránh khỏi.

Tuy nhiên, nếu bạn muốn sử dụng các biến riêng tư, trường hợp 3, thì bạn có thể gặp rắc rối. Nếu bạn coi các biến toàn cục là không an toàn, thì bạn nên xem xét sử dụng tính kế thừa để có quyền truy cập vào các biến riêng tư cũng không an toàn . Xin lưu ý các bạn, các biến toàn cục không phải là RẤT NHIỀU - cơ sở dữ liệu về cơ bản là tập hợp lớn các biến toàn cục. Nhưng nếu bạn có thể xử lý nó, thì nó khá ổn.


7

Để giải quyết câu hỏi này từ một góc nhìn khác cho các lập trình viên mới hơn:

Kế thừa thường được dạy sớm khi chúng ta học lập trình hướng đối tượng, vì vậy nó được coi là một giải pháp dễ dàng cho một vấn đề phổ biến.

Tôi có ba lớp mà tất cả đều cần một số chức năng chung. Vì vậy, nếu tôi viết một lớp cơ sở và có tất cả chúng được thừa hưởng từ nó, thì tất cả chúng sẽ có chức năng đó và tôi chỉ cần duy trì nó ở một nơi.

Nghe có vẻ hay, nhưng trong thực tế, nó hầu như không bao giờ hoạt động, vì một trong nhiều lý do:

  • Chúng tôi phát hiện ra rằng có một số chức năng khác mà chúng tôi muốn các lớp của chúng tôi có. Nếu cách chúng ta thêm chức năng cho các lớp là thông qua kế thừa, chúng ta phải quyết định - chúng ta có thêm nó vào lớp cơ sở hiện tại không, mặc dù không phải mọi lớp kế thừa từ nó đều cần chức năng đó? Chúng ta có tạo một lớp cơ sở khác không? Nhưng những gì về các lớp đã kế thừa từ lớp cơ sở khác?
  • Chúng tôi phát hiện ra rằng chỉ một trong các lớp kế thừa từ lớp cơ sở của chúng tôi, chúng tôi muốn lớp cơ sở hoạt động khác đi một chút. Vì vậy, bây giờ chúng tôi quay lại và sửa lại lớp cơ sở của chúng tôi, có thể thêm một số phương thức ảo hoặc thậm chí tệ hơn, một số mã có nội dung: "Nếu tôi được thừa hưởng loại A, hãy làm điều này, nhưng nếu tôi được thừa hưởng loại B, hãy làm điều đó . " Điều đó thật tệ vì nhiều lý do. Một là mỗi khi chúng ta thay đổi lớp cơ sở, chúng ta sẽ thay đổi hiệu quả mọi lớp kế thừa. Vì vậy, chúng tôi thực sự thay đổi lớp A, B, C và D vì chúng tôi cần một hành vi hơi khác trong lớp A. Cẩn thận như chúng tôi nghĩ, chúng tôi có thể phá vỡ một trong những lớp đó vì những lý do không liên quan đến những điều đó các lớp học.
  • Chúng tôi có thể biết lý do tại sao chúng tôi quyết định làm cho tất cả các lớp này kế thừa lẫn nhau, nhưng nó có thể không (có lẽ sẽ không) có ý nghĩa với người khác phải duy trì mã của chúng tôi. Chúng tôi có thể buộc họ vào một lựa chọn khó khăn - tôi có làm điều gì đó thực sự xấu xí và lộn xộn để thực hiện thay đổi tôi cần (xem điểm đạn trước đó) hay tôi chỉ viết lại một loạt điều này.

Cuối cùng, chúng tôi buộc mã của chúng tôi trong một số nút thắt khó khăn và không nhận được bất kỳ lợi ích nào từ nó ngoại trừ việc chúng tôi phải nói, "Thật tuyệt, tôi đã học về thừa kế và bây giờ tôi đã sử dụng nó." Điều đó không có nghĩa là hạ thấp bởi vì tất cả chúng ta đã làm điều đó. Nhưng tất cả chúng tôi đã làm điều đó bởi vì không ai bảo chúng tôi không làm điều đó.

Ngay khi ai đó giải thích "thành phần ưu tiên hơn thừa kế" với tôi, tôi đã nghĩ lại mỗi lần tôi cố gắng chia sẻ chức năng giữa các lớp bằng cách sử dụng tính kế thừa và nhận ra rằng hầu hết thời gian nó không thực sự hoạt động tốt.

Thuốc giải độc là Nguyên tắc Trách nhiệm duy nhất . Hãy nghĩ về nó như một sự ràng buộc. Lớp tôi phải làm một việc. Tôi phải có khả năng đặt cho lớp của mình một cái tên bằng cách nào đó mô tả một điều mà nó làm. (Có ngoại lệ cho tất cả mọi thứ, nhưng quy tắc tuyệt đối đôi khi tốt hơn khi chúng ta học.) Theo sau tôi không thể viết một lớp cơ sở được gọi ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses. Bất kỳ chức năng riêng biệt nào tôi cần phải có trong lớp riêng của nó, và sau đó các lớp khác cần chức năng đó có thể phụ thuộc vào lớp đó, không phải kế thừa từ nó.

Có nguy cơ đơn giản hóa, đó là thành phần - kết hợp nhiều lớp để làm việc cùng nhau. Và một khi chúng ta hình thành thói quen đó, chúng ta thấy rằng nó linh hoạt hơn, có thể duy trì và kiểm tra được hơn là sử dụng tính kế thừa.


Các lớp không thể sử dụng nhiều lớp cơ sở không phải là sự phản ánh kém về Kế thừa mà là sự phản ánh kém về sự thiếu khả năng của một ngôn ngữ cụ thể.
iPherian

Trong thời gian kể từ khi viết câu trả lời này, tôi đã đọc bài đăng này từ "Chú Bob", nơi giải quyết vấn đề thiếu khả năng đó. Tôi chưa bao giờ sử dụng một ngôn ngữ cho phép nhiều kế thừa. Nhưng nhìn lại, câu hỏi được gắn thẻ "bất khả tri ngôn ngữ" và câu trả lời của tôi giả sử C #. Tôi cần mở rộng tầm nhìn của mình.
Scott Hannen

6

Ngoài việc là một / có một sự cân nhắc, người ta cũng phải xem xét "độ sâu" của sự kế thừa mà đối tượng của bạn phải trải qua. Bất cứ điều gì vượt quá năm hoặc sáu cấp độ thừa kế sâu có thể gây ra các sự cố bất ngờ về quyền anh và quyền anh / unboxing, và trong những trường hợp đó, có thể là khôn ngoan khi soạn thảo đối tượng của bạn.


6

Khi bạn có một là-một mối quan hệ giữa hai lớp (ví dụ con chó là một loài chó), bạn đi cho thừa kế.

Mặt khác, khi bạn có một hoặc một số mối quan hệ tính từ giữa hai lớp (sinh viên có các khóa học) hoặc (các khóa học giáo viên), bạn đã chọn sáng tác.


Bạn nói thừa kế và thừa kế. bạn không có nghĩa là thừa kế và thành phần?
trevorKirkby 30/12/13

Không, bạn không. Bạn cũng có thể xác định một giao diện chó và để mọi con chó thực hiện nó và bạn sẽ có thêm mã RẮN.
markus

5

Một cách đơn giản để hiểu điều này sẽ là sử dụng tính kế thừa khi bạn cần một đối tượng của lớp có cùng giao diện với lớp cha của nó, do đó nó có thể được coi là một đối tượng của lớp cha (phát sóng) . Hơn nữa, các lệnh gọi hàm trên một đối tượng lớp dẫn xuất sẽ vẫn giống nhau ở mọi nơi trong mã, nhưng phương thức cụ thể để gọi sẽ được xác định trong thời gian chạy (tức là việc thực hiện cấp thấp khác nhau, giao diện cấp cao vẫn giữ nguyên).

Thành phần nên được sử dụng khi bạn không cần lớp mới có cùng giao diện, tức là bạn muốn che giấu các khía cạnh nhất định của việc triển khai lớp mà người dùng của lớp đó không cần biết. Vì vậy, thành phần theo cách hỗ trợ đóng gói (nghĩa là che giấu việc thực hiện) trong khi kế thừa có nghĩa là hỗ trợ trừu tượng hóa (nghĩa là cung cấp một biểu diễn đơn giản hóa của một cái gì đó, trong trường hợp này là cùng một giao diện cho một loạt các loại với các phần bên trong khác nhau).


+1 để đề cập đến giao diện. Tôi sử dụng cách tiếp cận này thường xuyên để ẩn các lớp hiện có và làm cho lớp mới của tôi có thể kiểm tra đơn vị chính xác bằng cách loại bỏ đối tượng được sử dụng để sáng tác. Điều này đòi hỏi chủ sở hữu của đối tượng mới phải chuyển nó vào lớp cha ứng cử viên thay thế.
Kell


4

Tôi đồng ý với @Pavel, khi anh ta nói, có những nơi để sáng tác và có những nơi để thừa kế.

Tôi nghĩ rằng nên sử dụng tính kế thừa nếu câu trả lời của bạn là một câu khẳng định cho bất kỳ câu hỏi nào trong số này.

  • Là lớp học của bạn trong một cấu trúc được hưởng lợi từ đa hình? Ví dụ: nếu bạn có một lớp Shape, khai báo một phương thức gọi là draw (), thì rõ ràng chúng ta cần các lớp Circle và Square là các lớp con của Shape, để các lớp khách của chúng phụ thuộc vào Shape chứ không phụ thuộc vào các lớp con cụ thể.
  • Lớp của bạn có cần sử dụng lại bất kỳ tương tác cấp cao nào được định nghĩa trong lớp khác không? Các phương pháp mẫu mẫu thiết kế sẽ không thể thực hiện mà không cần thừa kế. Tôi tin rằng tất cả các khung mở rộng sử dụng mô hình này.

Tuy nhiên, nếu ý định của bạn hoàn toàn là sử dụng lại mã, thì thành phần rất có thể là một lựa chọn thiết kế tốt hơn.


4

Kế thừa là một máy móc rất mạnh mẽ để tái sử dụng mã. Nhưng cần phải được sử dụng đúng cách. Tôi sẽ nói rằng sự kế thừa được sử dụng một cách chính xác nếu lớp con cũng là một kiểu con của lớp cha. Như đã đề cập ở trên, Nguyên tắc thay thế Liskov là điểm mấu chốt ở đây.

Phân lớp không giống như phân nhóm. Bạn có thể tạo các lớp con không phải là kiểu con (và đây là lúc bạn nên sử dụng bố cục). Để hiểu thế nào là một kiểu con, hãy bắt đầu đưa ra lời giải thích về loại là gì.

Khi chúng ta nói rằng số 5 là số nguyên kiểu, chúng ta nói rằng 5 thuộc về một tập hợp các giá trị có thể (ví dụ: xem các giá trị có thể có cho các kiểu nguyên thủy Java). Chúng tôi cũng tuyên bố rằng có một tập hợp các phương thức hợp lệ tôi có thể thực hiện trên giá trị như phép cộng và phép trừ. Và cuối cùng, chúng tôi tuyên bố rằng có một tập các thuộc tính luôn được thỏa mãn, ví dụ, nếu tôi thêm các giá trị 3 và 5, tôi sẽ nhận được 8 kết quả.

Để đưa ra một ví dụ khác, hãy nghĩ về các kiểu dữ liệu trừu tượng, Tập hợp số nguyên và Danh sách số nguyên, các giá trị họ có thể giữ được giới hạn ở các số nguyên. Cả hai đều hỗ trợ một tập hợp các phương thức, như add (newValue) và size (). Và cả hai đều có các thuộc tính khác nhau (bất biến lớp), Bộ không cho phép trùng lặp trong khi Danh sách cho phép trùng lặp (tất nhiên có các thuộc tính khác mà cả hai đều thỏa mãn).

Subtype cũng là một loại, có mối quan hệ với loại khác, được gọi là kiểu cha (hoặc siêu kiểu). Kiểu con phải đáp ứng các tính năng (giá trị, phương thức và thuộc tính) của kiểu cha. Mối quan hệ có nghĩa là trong bất kỳ bối cảnh nào mà siêu kiểu được mong đợi, nó có thể được thay thế bằng một kiểu con, mà không ảnh hưởng đến hành vi của việc thực thi. Chúng ta hãy đi xem một số mã để minh họa những gì tôi đang nói. Giả sử tôi viết Danh sách các số nguyên (bằng một số loại ngôn ngữ giả):

class List {
  data = new Array();

  Integer size() {
    return data.length;
  }

  add(Integer anInteger) {
    data[data.length] = anInteger;
  }
}

Sau đó, tôi viết Tập hợp số nguyên dưới dạng lớp con của Danh sách số nguyên:

class Set, inheriting from: List {
  add(Integer anInteger) {
     if (data.notContains(anInteger)) {
       super.add(anInteger);
     }
  }
}

Lớp Tập hợp số nguyên của chúng tôi là một lớp con của Danh sách số nguyên, nhưng không phải là một kiểu con, do nó không thỏa mãn tất cả các tính năng của lớp Danh sách. Các giá trị và chữ ký của các phương thức được thỏa mãn nhưng các thuộc tính thì không. Hành vi của phương thức add (Integer) đã được thay đổi rõ ràng, không bảo toàn các thuộc tính của kiểu cha. Suy nghĩ từ quan điểm của khách hàng của các lớp học của bạn. Họ có thể nhận được một Bộ số nguyên trong đó Danh sách các số nguyên được mong đợi. Khách hàng có thể muốn thêm một giá trị và nhận giá trị đó được thêm vào Danh sách ngay cả khi giá trị đó đã tồn tại trong Danh sách. Nhưng cô ấy sẽ không có được hành vi đó nếu giá trị tồn tại. Một bất ngờ lớn cho cô ấy!

Đây là một ví dụ cổ điển về việc sử dụng thừa kế không đúng cách. Sử dụng thành phần trong trường hợp này.

(một đoạn từ: sử dụng thừa kế đúng cách ).


3

Một quy tắc ngón tay cái mà tôi đã nghe là sự kế thừa nên được sử dụng khi mối quan hệ và thành phần "is-a" của nó khi nó là "has-a". Ngay cả với điều đó tôi cảm thấy rằng bạn nên luôn luôn nghiêng về bố cục vì nó giúp loại bỏ rất nhiều sự phức tạp.


2

Thành phần v / s Kế thừa là một chủ đề rộng. Không có câu trả lời thực sự cho những gì tốt hơn vì tôi nghĩ tất cả phụ thuộc vào thiết kế của hệ thống.

Nói chung loại mối quan hệ giữa các đối tượng cung cấp thông tin tốt hơn để chọn một trong số họ.

Nếu loại quan hệ là quan hệ "IS-A" thì Kế thừa là cách tiếp cận tốt hơn. mặt khác, loại quan hệ là quan hệ "HAS-A" thì thành phần sẽ tiếp cận tốt hơn.

Nó hoàn toàn phụ thuộc vào mối quan hệ thực thể.


2

Mặc dù Thành phần được ưa thích, tôi muốn nêu bật ưu điểm của Kế thừa và nhược điểm của Thành phần .

Ưu điểm của thừa kế:

  1. Nó thiết lập một mối quan hệ " IS A" hợp lý . Nếu xexe tải hai loại xe (lớp cơ sở), lớp trẻ LÀ MỘT lớp cơ sở.

    I E

    Xe là phương tiện

    Xe tải là một phương tiện

  2. Với tính kế thừa, bạn có thể xác định / sửa đổi / mở rộng khả năng

    1. Lớp cơ sở không cung cấp triển khai và lớp con phải ghi đè phương thức hoàn chỉnh (trừu tượng) => Bạn có thể thực hiện hợp đồng
    2. Lớp cơ sở cung cấp triển khai mặc định và lớp phụ có thể thay đổi hành vi => Bạn có thể xác định lại hợp đồng
    3. Lớp phụ thêm phần mở rộng để triển khai lớp cơ sở bằng cách gọi super.methodName () làm câu lệnh đầu tiên => Bạn có thể gia hạn hợp đồng
    4. Lớp cơ sở xác định cấu trúc của thuật toán và lớp con sẽ ghi đè lên một phần của thuật toán => Bạn có thể triển khai Template_method mà không thay đổi trong khung xương của lớp cơ sở

Nhược điểm của thành phần:

  1. Trong kế thừa, lớp con có thể gọi trực tiếp phương thức lớp cơ sở mặc dù nó không triển khai phương thức lớp cơ sở vì có quan hệ IS . Nếu bạn sử dụng thành phần, bạn phải thêm các phương thức trong lớp container để hiển thị API lớp chứa

ví dụ: Nếu Xe chứa Xe và nếu bạn phải lấy giá Xe , đã được xác định trong Xe , mã của bạn sẽ như thế này

class Vehicle{
     protected double getPrice(){
          // return price
     }
} 

class Car{
     Vehicle vehicle;
     protected double getPrice(){
          return vehicle.getPrice();
     }
} 

Tôi nghĩ rằng nó không trả lời câu hỏi
almanegra 15/03/2017

Bạn có thể xem lại câu hỏi của OP. Tôi đã giải quyết: Có sự đánh đổi nào cho mỗi cách tiếp cận?
Ravindra babu

Như bạn đã đề cập, bạn chỉ nói về "ưu điểm của Kế thừa và nhược điểm của Thành phần", chứ không phải là sự đánh đổi cho phương pháp
MACHI

ưu và nhược điểm cung cấp sự đánh đổi vì ưu điểm của thừa kế là nhược điểm của thành phần và nhược điểm của thành phần là ưu điểm của thừa kế.
Ravindra babu

1

Như nhiều người đã nói, trước tiên tôi sẽ bắt đầu với tấm séc - liệu có tồn tại mối quan hệ "là-một" hay không. Nếu nó tồn tại tôi thường kiểm tra như sau:

Cho dù lớp cơ sở có thể được khởi tạo. Đó là, liệu lớp cơ sở có thể không trừu tượng. Nếu nó có thể không trừu tượng, tôi thường thích sáng tác

Ví dụ 1. Kế toán là một nhân viên. Nhưng tôi sẽ không sử dụng tính kế thừa vì một đối tượng Nhân viên có thể được khởi tạo.

Ví dụ 2. Sách là một SellItem. Một SellItem không thể được khởi tạo - đó là khái niệm trừu tượng. Do đó tôi sẽ sử dụng inheritacne. SellItem là một lớp cơ sở trừu tượng (hoặc giao diện trong C #)

Bạn nghĩ gì về phương pháp này?

Ngoài ra, tôi hỗ trợ @anon trả lời trong Tại sao nên sử dụng tính kế thừa?

Lý do chính cho việc sử dụng thừa kế không phải là một hình thức sáng tác - đó là vì vậy bạn có thể có hành vi đa hình. Nếu bạn không cần đa hình, có lẽ bạn không nên sử dụng tính kế thừa.

@MatthieuM. nói trong /software/12439/code-smell-inherribution-abuse/12448#comment303759_12448

Vấn đề với sự kế thừa là nó có thể được sử dụng cho hai mục đích trực giao:

giao diện (cho đa hình)

thực hiện (để sử dụng lại mã)

TÀI LIỆU THAM KHẢO

  1. Thiết kế lớp nào tốt hơn?
  2. Kế thừa so với tập hợp

1
Tôi không chắc tại sao 'lớp cơ sở là trừu tượng?' số liệu vào cuộc thảo luận .. LSP: tất cả các chức năng hoạt động trên Chó có hoạt động không nếu các đối tượng Poodle được truyền vào? Nếu có, thì Poodle có thể được thay thế cho Dog và do đó có thể thừa hưởng từ Dog.
Gishu

@Gishu Cảm ơn. Tôi chắc chắn sẽ xem xét LSP. Nhưng trước đó, bạn có thể vui lòng cung cấp một "ví dụ trong đó kế thừa là phù hợp trong đó lớp cơ sở không thể trừu tượng". Những gì tôi nghĩ là, kế thừa chỉ được áp dụng nếu lớp cơ sở là trừu tượng. Nếu lớp cơ sở cần được khởi tạo riêng, không đi kế thừa. Đó là, mặc dù Kế toán là một Nhân viên, không sử dụng thừa kế.
LCJ

1
đã đọc WCF gần đây. Một ví dụ trong khung .net là SyncizationContext (cơ sở + có thể được khởi tạo) mà hàng đợi hoạt động trên một luồng ThreadPool. Các phái sinh bao gồm WinFormsSyncContext (xếp hàng vào UI Thread) và DispatcherSyncContext (xếp hàng vào WPF Dispatcher)
Gishu

@Gishu Cảm ơn. Tuy nhiên, sẽ hữu ích hơn nếu bạn có thể cung cấp một kịch bản dựa trên miền Ngân hàng, miền HR, miền Bán lẻ hoặc bất kỳ miền phổ biến nào khác.
LCJ

1
Lấy làm tiếc. Tôi không quen thuộc với các tên miền đó .. Một ví dụ khác nếu cái trước đó quá khó hiểu là lớp Control trong Winforms / WPF. Kiểm soát cơ sở / chung có thể được khởi tạo. Các dẫn xuất bao gồm Listboxes, Textboxes, v.v ... Bây giờ tôi nghĩ về nó, mẫu Designator Design là một ví dụ hay IMHO và cũng hữu ích. Trình trang trí xuất phát từ đối tượng không trừu tượng mà nó muốn bọc / trang trí.
Gishu

1

Tôi thấy không ai đề cập đến vấn đề kim cương , có thể phát sinh với sự kế thừa.

Trong nháy mắt, nếu các lớp B và C kế thừa A và cả hai phương thức ghi đè X và một lớp thứ tư D, kế thừa từ cả B và C, và không ghi đè X, thì nên sử dụng XD nào?

Wikipedia cung cấp một cái nhìn tổng quan tốt đẹp về chủ đề đang được thảo luận trong câu hỏi này.


1
D kế thừa B và C chứ không phải A. Nếu vậy, thì nó sẽ sử dụng triển khai X trong lớp A.
Fabricio

1
@fovenio: cảm ơn, tôi đã chỉnh sửa văn bản. Nhân tiện, kịch bản như vậy không thể xảy ra trong các ngôn ngữ không cho phép thừa kế nhiều lớp, tôi có đúng không?
Veverke

vâng, bạn đúng .. và tôi chưa bao giờ làm việc với một người cho phép thừa kế nhiều lần (theo ví dụ trong bài toán kim cương) ..
Fabricio
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.