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ế this
con trỏ được gọi bởi các hàm ảo bằng this
con 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 doThings
thay đổ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 WayOfDoingUsefulThings
và getStuff()
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ó: dealWithStuff
mất một WayOfDoingUsefulThings
chỉ để 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 delegation
từ 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
- Cấm thừa kế hoặc thiết kế cho nó
- 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.