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?
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?
Câu trả lời:
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 .
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.
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?'
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 .
Nếu bạn hiểu sự khác biệt, nó dễ giải thích hơn.
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.
Đ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 đượ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.
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 Title
thì 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.
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:
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ế.
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.
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.
InternalCombustionEngine
vớ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ứ InternalCombustionEngine
sẽ khiến bugi được sử dụng.
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à
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.
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ái và Chiế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.
Client
. Sau đó, một khái niệm mới về một PreferredClient
cửa sổ bật lên sau này. Có nên PreferredClient
thừ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, Account
có lẽ?
Client
lớ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 Account
có thể là StandardAccount
hoặc PreferredAccount
...
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
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.
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:
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.
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ự.
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.
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í và mẫu proxy .
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 .
Thành phần ngay lập tức sau khi lập trình đến một giao diện .
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.
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.
TextFile
là một File
.
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
Xem câu trả lời khác.
Người ta thường nói rằng một lớp Bar
có thể kế thừa một lớp Foo
khi câu sau là đúng:
- 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ế:
- một thanh là một foo, VÀ
- 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 Foo
có ý 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 Foo
có ý 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.
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 Widget
trong 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.
Để 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".
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.
computeArea(Circle *c) { return pi * width * height / 4.0; }
Bây giờ nó chung chung.
width()
và 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.
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.
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).
Inheritance is sometimes useful... That way you can have polymorphism
là 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.
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 Engine
ra 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 Aircraft
thà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ạn cần phải có một cái nhìn tại The Liskov Substitution Nguyên tắc trong Bác Bob RẮN nguyên tắc thiết kế lớp. :)
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.
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ế:
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.
Để 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:
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.
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.
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.
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).
Subtyping là thích hợp và mạnh mẽ hơn trong đó các bất biến có thể được liệt kê , khác sử dụng thành phần chức năng cho khả năng mở rộng.
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.
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.
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 ).
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.
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ể.
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ế:
Nó thiết lập một mối quan hệ " IS A" hợp lý . Nếu xe và xe 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
Với tính kế thừa, bạn có thể xác định / sửa đổi / mở rộng khả năng
Nhược điểm của thành phần:
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();
}
}
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
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.