Tại sao sự kế thừa và đa hình được sử dụng rộng rãi như vậy?


18

Càng tìm hiểu về các mô hình lập trình khác nhau, chẳng hạn như lập trình chức năng, tôi càng bắt đầu đặt câu hỏi về sự khôn ngoan của các khái niệm OOP như kế thừa và đa hình. Lần đầu tiên tôi học về sự kế thừa và đa hình ở trường, và tại thời điểm đa hình có vẻ như là một cách tuyệt vời để viết mã chung cho phép dễ dàng mở rộng.

Nhưng khi đối mặt với việc gõ vịt (cả động và tĩnh) và các tính năng chức năng như các hàm bậc cao hơn, tôi đã bắt đầu xem tính kế thừa và đa hình là áp đặt một hạn chế không cần thiết dựa trên tập hợp các mối quan hệ mỏng manh giữa các đối tượng. Ý tưởng chung đằng sau tính đa hình là bạn viết một hàm một lần và sau đó bạn có thể thêm chức năng mới vào chương trình của mình mà không thay đổi hàm ban đầu - tất cả những gì bạn cần làm là tạo một lớp dẫn xuất khác thực hiện các phương thức cần thiết.

Nhưng điều này đơn giản hơn rất nhiều để đạt được thông qua việc gõ vịt, cho dù đó là ngôn ngữ động như Python hay ngôn ngữ tĩnh như C ++.

Ví dụ, hãy xem xét hàm Python sau, theo sau là tương đương C ++ tĩnh của nó:

def foo(obj):
   obj.doSomething()

template <class Obj>
void foo(Obj& obj)
{
   obj.doSomething();
}

Tương đương OOP sẽ giống như mã Java sau:

public void foo(DoSomethingable obj)
{
  obj.doSomething();
}

Tất nhiên, sự khác biệt chính là phiên bản Java yêu cầu tạo giao diện hoặc phân cấp thừa kế trước khi nó hoạt động. Do đó, phiên bản Java liên quan đến nhiều công việc hơn và kém linh hoạt hơn. Ngoài ra, tôi thấy rằng hầu hết các hệ thống phân cấp thừa kế trong thế giới thực có phần không ổn định. Tất cả chúng ta đều đã thấy các ví dụ về Hình dạng và Động vật, nhưng trong thế giới thực, khi các yêu cầu kinh doanh thay đổi và các tính năng mới được thêm vào, thật khó để thực hiện bất kỳ công việc nào trước khi bạn cần thực sự kéo dài mối quan hệ "là" các lớp con, hoặc cách khác sửa sang lại / cấu trúc lại cấu trúc phân cấp của bạn để bao gồm các lớp cơ sở hoặc giao diện tiếp theo để đáp ứng các yêu cầu mới. Với cách gõ vịt, bạn không cần phải lo lắng về việc mô hình hóa bất cứ điều gì - bạn chỉ cần lo lắng về chức năng bạn cần.

Tuy nhiên, tính kế thừa và tính đa hình rất phổ biến đến nỗi tôi nghi ngờ sẽ không có gì quá đáng khi gọi chúng là chiến lược chi phối cho khả năng mở rộng và tái sử dụng mã. Vậy tại sao sự kế thừa và đa hình lại thành công rực rỡ như vậy? Tôi có đang xem xét một số lợi thế nghiêm trọng mà tính kế thừa / đa hình có được khi gõ vịt không?


Tôi không phải là chuyên gia về Python nên tôi phải hỏi: điều gì sẽ xảy ra nếu objkhông có doSomethingphương pháp? Là một ngoại lệ được nêu ra? Không có gì xảy ra?
Thất vọngWithFormsDesigner

6
@Frustrated: Một ngoại lệ rất rõ ràng, cụ thể được nêu ra.
dsimcha

11
Không phải vịt gõ chỉ là đa hình được thực hiện đến mười một? Tôi đoán là bạn không nản lòng với OOP nhưng với những hiện thân được gõ tĩnh. Tôi có thể hiểu điều đó, thực sự, nhưng điều đó không làm cho OOP trở thành một ý tưởng tồi.

3
Lần đầu tiên đọc "rộng rãi" là "cuồng nhiệt" ...

Câu trả lời:


22

Tôi hầu như đồng ý với bạn, nhưng để giải trí, tôi sẽ chơi Devil's Advocate. Các giao diện rõ ràng cung cấp một nơi duy nhất để tìm kiếm một hợp đồng rõ ràng, được chỉ định chính thức , cho bạn biết một loại được cho là phải làm gì. Điều này có thể quan trọng khi bạn không phải là nhà phát triển duy nhất trong một dự án.

Hơn nữa, các giao diện rõ ràng này có thể được thực hiện hiệu quả hơn so với gõ vịt. Một cuộc gọi chức năng ảo hầu như không có nhiều chi phí hơn một cuộc gọi chức năng thông thường, ngoại trừ việc nó không thể được nội tuyến. Gõ vịt có chi phí đáng kể. Kiểu gõ cấu trúc kiểu C ++ (sử dụng các mẫu) có thể tạo ra một lượng lớn tệp tin đối tượng (vì mỗi lần khởi tạo là độc lập ở cấp tệp đối tượng) và không hoạt động khi bạn cần đa hình khi chạy, không biên dịch thời gian.

Điểm mấu chốt: Tôi đồng ý rằng tính kế thừa và đa hình kiểu Java có thể là PITA và các lựa chọn thay thế nên được sử dụng thường xuyên hơn, nhưng nó vẫn có những ưu điểm của nó.


29

Kế thừa và đa hình được sử dụng rộng rãi vì chúng hoạt động , đối với một số loại vấn đề lập trình.

Không phải là chúng được dạy rộng rãi ở trường, mà ngược lại: chúng được dạy rộng rãi ở trường bởi vì mọi người (còn gọi là thị trường) thấy rằng chúng hoạt động tốt hơn các công cụ cũ, và vì vậy các trường bắt đầu dạy chúng. [Giai thoại: khi tôi lần đầu tiên học OOP, việc tìm một trường đại học dạy bất kỳ ngôn ngữ OOP nào là vô cùng khó khăn. Mười năm sau, thật khó để tìm thấy một trường đại học không dạy ngôn ngữ OOP.]

Bạn đã nói:

Ý tưởng chung đằng sau tính đa hình là bạn viết một hàm một lần và sau đó bạn có thể thêm chức năng mới vào chương trình của mình mà không thay đổi hàm ban đầu - tất cả những gì bạn cần làm là tạo một lớp dẫn xuất khác thực hiện các phương thức cần thiết

Tôi nói:

Không, không phải

Những gì bạn mô tả không phải là đa hình, mà là sự kế thừa. Không có gì ngạc nhiên khi bạn gặp vấn đề OOP! ;-)

Sao lưu một bước: đa hình là một lợi ích của việc truyền thông điệp; nó đơn giản có nghĩa là mỗi đối tượng được tự do trả lời một tin nhắn theo cách riêng của nó.

Vì vậy, ... Duck Typing (hay đúng hơn, cho phép) đa hình

Ý chính của câu hỏi của bạn dường như không phải là bạn không hiểu OOP hay bạn không thích nó, mà là bạn không thích xác định giao diện . Điều đó tốt, và miễn là bạn cẩn thận mọi thứ sẽ hoạt động tốt. Hạn chế là nếu bạn đã mắc lỗi - ví dụ bỏ qua một phương thức - bạn sẽ không phát hiện ra cho đến khi hết thời gian.

Đó là một điều tĩnh so với động, đó là một cuộc thảo luận lâu đời như Lisp, và không giới hạn ở OOP.


2
+1 cho "gõ vịt đa hình". Sự đa hình và sự kế thừa đã bị xiềng xích trong tiềm thức quá lâu, và việc gõ tĩnh nghiêm ngặt là lý do thực sự duy nhất mà họ từng phải có.
cHao

+1. Tôi nghĩ rằng sự phân biệt tĩnh và động nên nhiều hơn một ghi chú bên lề - đó là cốt lõi của sự phân biệt giữa kiểu gõ vịt và đa hình kiểu java.
Sava B.

8

Tôi đã bắt đầu xem tính kế thừa và đa hình là áp đặt một hạn chế không cần thiết dựa trên một tập hợp các mối quan hệ mong manh giữa các đối tượng.

Tại sao?

Kế thừa (có hoặc không có gõ vịt) đảm bảo tái sử dụng một tính năng phổ biến. Nếu nó phổ biến, bạn có thể đảm bảo rằng nó được sử dụng lại một cách nhất quán trong các lớp con.

Đó là tất cả. Không có "hạn chế không cần thiết". Đó là một sự đơn giản hóa.

Đa hình, tương tự, là "gõ vịt" nghĩa là gì. Phương pháp tương tự. Rất nhiều lớp với giao diện giống hệt nhau, nhưng cách triển khai khác nhau.

Đó không phải là một hạn chế. Đó là một sự đơn giản hóa.


7

Kế thừa bị lạm dụng, nhưng vịt cũng vậy. Cả hai có thể và làm dẫn đến vấn đề.

Với kiểu gõ mạnh, bạn sẽ có được nhiều "bài kiểm tra đơn vị" được thực hiện tại thời điểm biên dịch. Với gõ vịt bạn thường phải viết chúng.


3
Nhận xét của bạn về thử nghiệm chỉ áp dụng cho gõ vịt năng động. C ++ - kiểu gõ tĩnh tĩnh (có mẫu) cung cấp cho bạn các lỗi khi biên dịch. (Mặc dù, các lỗi mẫu C ++ được cấp là khá khó để giải mã, nhưng đó hoàn toàn là một vấn đề khác.)
Channel72

2

Những điều tốt về học hỏi những điều học là bạn làm tìm hiểu họ. Điều không tốt là bạn có thể chấp nhận chúng một cách quá giáo điều, không hiểu khi nào chúng hữu ích và khi nào không.

Sau đó, nếu bạn đã học nó một cách giáo điều, sau này bạn có thể "nổi loạn" giống như giáo điều theo hướng khác. Điều đó cũng không tốt.

Như với bất kỳ ý tưởng nào như vậy, tốt nhất là thực hiện một cách tiếp cận thực tế. Phát triển sự hiểu biết về nơi phù hợp và nơi họ không. Và bỏ qua tất cả những cách họ đã được bán quá mức.


2

Có, gõ tĩnh và giao diện là hạn chế. Nhưng tất cả mọi thứ kể từ khi lập trình có cấu trúc được phát minh (tức là "goto coi có hại") đã khiến chúng ta bị hạn chế. Chú Bob có một điều tuyệt vời về điều này trong blog video của mình .

Bây giờ, người ta có thể lập luận rằng sự co thắt là xấu, nhưng mặt khác, nó mang lại trật tự, sự kiểm soát và sự quen thuộc cho một chủ đề rất phức tạp khác.

Hạn chế thư giãn bằng cách (giới thiệu lại) giới thiệu kiểu gõ động và thậm chí truy cập bộ nhớ trực tiếp khái niệm rất mạnh mẽ, nhưng nó cũng có thể khiến nhiều lập trình viên khó đối phó hơn. Đặc biệt là các lập trình viên thường dựa vào trình biên dịch và gõ an toàn cho phần lớn công việc của họ.


1

Kế thừa là mối quan hệ rất mạnh mẽ giữa hai lớp. Tôi không nghĩ Java có gì mạnh hơn. Do đó, bạn chỉ nên sử dụng nó khi bạn có ý nghĩa. Thừa kế công khai là mối quan hệ "is-a", không phải là "thường là-a". Nó thực sự, thực sự dễ dàng để lạm dụng thừa kế và kết thúc với một mớ hỗn độn. Trong nhiều trường hợp, tính kế thừa được sử dụng để thể hiện "has-a" hoặc "takes-function-from-a" và điều đó thường được thực hiện tốt hơn bởi thành phần.

Đa hình là hệ quả trực tiếp của mối quan hệ "is-a". Nếu Derogen kế thừa từ Base, thì mọi Derogen "là-a" Base, và do đó bạn có thể sử dụng Derogen bất cứ nơi nào bạn sử dụng Base. Nếu điều này không có ý nghĩa trong hệ thống phân cấp thừa kế, thì hệ thống phân cấp đó là sai và có lẽ có quá nhiều sự kế thừa đang diễn ra.

Gõ vịt là một tính năng gọn gàng, nhưng trình biên dịch sẽ không cảnh báo bạn nếu bạn sử dụng sai. Nếu bạn không muốn phải xử lý các trường hợp ngoại lệ vào thời gian chạy, bạn cần đảm bảo với bản thân rằng bạn đang nhận được kết quả đúng mỗi lần. Có thể dễ dàng hơn chỉ để xác định một hệ thống phân cấp thừa kế tĩnh.

Tôi không phải là một fan hâm mộ thực sự của kiểu gõ tĩnh (tôi coi nó thường là một hình thức tối ưu hóa sớm), nhưng nó loại bỏ một số lớp lỗi và nhiều người cho rằng những lớp đó rất đáng để loại bỏ.

Nếu bạn thích gõ động và gõ vịt tốt hơn gõ tĩnh và phân cấp thừa kế được xác định, điều đó tốt. Tuy nhiên, cách Java có lợi thế của nó.


0

Tôi đã nhận thấy rằng tôi càng sử dụng các lần đóng cửa của C # thì tôi càng ít thực hiện OOP truyền thống. Kế thừa được sử dụng là cách duy nhất để dễ dàng chia sẻ việc thực hiện, vì vậy tôi nghĩ rằng nó thường được sử dụng quá mức và giới hạn quan niệm đã bị đẩy đi quá xa.

Mặc dù bạn thường có thể sử dụng các bao đóng để thực hiện hầu hết những gì bạn làm với thừa kế, nhưng nó cũng có thể trở nên xấu xí.

Về cơ bản đó là một tình huống công cụ phù hợp cho công việc: OOP truyền thống có thể hoạt động thực sự tốt khi bạn có một mô hình phù hợp với nó, và đóng cửa có thể hoạt động thực sự tốt khi bạn không.


-1

Sự thật nằm ở đâu đó ở giữa. Tôi thích cách ngôn ngữ C # 4.0 được gõ tĩnh hỗ trợ "gõ vịt" theo từ khóa "động".


-1

Kế thừa, ngay cả khi lý do từ quan điểm của FP, là một khái niệm tuyệt vời; nó không chỉ tiết kiệm rất nhiều thời gian, nó còn có ý nghĩa đối với mối quan hệ giữa các đối tượng nhất định trong chương trình của bạn.

public class Animal {
    public virtual string Sound () {
        return "Some Sound";
    }
}

public class Dog : Animal {
    public override string Sound () {
        return "Woof";
    }
}

public class Cat : Animal {
    public override string Sound () {
        return "Mew";
    }
}

public class GoldenRetriever : Dog {

}

Ở đây, lớp học GoldenRetrievergiống Soundnhư Dogcảm ơn miễn phí đối với thừa kế.

Tôi sẽ viết ví dụ tương tự với cấp độ Haskell của tôi để bạn thấy sự khác biệt

data Animal = Animal | Dog | Cat | GoldenRetriever

sound :: Animal -> String
sound Animal = "Some Sound"
sound Dog = "Woof"
sound Cat = "Mew"
sound GoldenRetriever = "Woof"

Ở đây bạn không thoát phải chỉ định soundcho GoldenRetriever. Điều dễ nhất nói chung sẽ là

sound GoldenRetriever = sound Dog

nhưng chỉ cần tưởng tượng nếu bạn có 20 chức năng! Nếu có một chuyên gia Haskell xin vui lòng chỉ cho chúng tôi một cách dễ dàng hơn.

Điều đó nói rằng, sẽ rất tuyệt khi có sự khớp và thừa kế mẫu cùng một lúc, trong đó một hàm sẽ mặc định cho lớp cơ sở nếu cá thể hiện tại không có triển khai.


5
Tôi thề, Animallà hướng dẫn chống OOP.
Telastyn

@Telastyn Tôi đã học được ví dụ từ Derek Banas trên Youtube. Giáo viên tuyệt vời. Theo tôi nó rất dễ hình dung và lý do về. Bạn có thể tự do chỉnh sửa bài viết với một ví dụ tốt hơn.
Cristian Garcia

điều này dường như không thêm bất cứ điều gì đáng kể vào 10 câu trả lời trước
gnat

@gnat Trong khi "chất" đã có sẵn, một người mới làm quen với chủ đề này có thể tìm thấy một ví dụ dễ nắm bắt hơn.
Cristian Garcia

2
Động vật và hình dạng là những ứng dụng thực sự xấu của đa hình vì một số lý do đã được thảo luận về quảng cáo trên trang web này. Về cơ bản, nói một con mèo "là một" động vật là vô nghĩa, vì không có "động vật" cụ thể. Những gì bạn thực sự có nghĩa là mô hình là hành vi, không phải bản sắc. Xác định giao diện "CanSpeak" có thể được triển khai dưới dạng meo hoặc vỏ cây có ý nghĩa. Xác định giao diện "HasArea" mà hình vuông và hình tròn có thể thực hiện có ý nghĩa, nhưng không phải là Hình dạng chung.
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.