Tại sao các giao diện hữu ích hơn các siêu lớp trong việc đạt được khớp nối lỏng lẻo?


15

( Đối với mục đích của câu hỏi này, khi tôi nói 'giao diện', ý tôi là cấu trúc ngôn ngữinterface chứ không phải là 'giao diện' theo nghĩa khác của từ này, tức là các phương thức công khai mà một lớp cung cấp cho thế giới bên ngoài để giao tiếp và thao túng nó. )

Khớp nối lỏng lẻo có thể đạt được bằng cách có một đối tượng phụ thuộc vào sự trừu tượng thay vì một loại cụ thể.

Điều này cho phép khớp nối lỏng lẻo vì hai lý do chính: 1- trừu tượng ít có khả năng thay đổi hơn các loại cụ thể, điều đó có nghĩa là mã phụ thuộc ít có khả năng bị phá vỡ. 2- các loại bê tông khác nhau có thể được sử dụng trong thời gian chạy, bởi vì tất cả chúng đều phù hợp với sự trừu tượng. Các loại bê tông mới cũng có thể được thêm vào sau mà không cần thay đổi mã phụ thuộc hiện có.

Ví dụ, hãy xem xét một lớp Carvà hai lớp con VolvoMazda.

Nếu mã của bạn phụ thuộc vào a Car, nó có thể sử dụng a Volvohoặc a Mazdatrong thời gian chạy. Ngoài ra sau này trên các lớp con bổ sung có thể được thêm vào mà không cần thay đổi mã phụ thuộc.

Ngoài ra, Car- đó là một sự trừu tượng - ít có khả năng thay đổi hơn Volvohoặc Mazda. Ô tô nói chung là giống nhau trong một thời gian khá lâu, nhưng Volvos và Mazdas có nhiều khả năng thay đổi. Tức là trừu tượng ổn định hơn các loại bê tông.

Tất cả điều này là để cho thấy rằng tôi hiểu khớp nối lỏng lẻo là gì và làm thế nào đạt được bằng cách phụ thuộc vào sự trừu tượng và không phụ thuộc vào bê tông hóa. (Nếu tôi viết một cái gì đó không chính xác xin vui lòng nói như vậy).

Điều tôi không hiểu là đây:

Trừu tượng có thể là siêu lớp hoặc giao diện.

Nếu vậy, tại sao các giao diện được ca ngợi đặc biệt cho khả năng cho phép khớp nối lỏng lẻo? Tôi không thấy nó khác với việc sử dụng siêu lớp như thế nào.

Sự khác biệt duy nhất tôi thấy là: 1- Các giao diện không bị giới hạn bởi tính kế thừa duy nhất, nhưng điều đó không liên quan nhiều đến chủ đề khớp nối lỏng lẻo. 2- Các giao diện là 'trừu tượng' hơn vì chúng không có logic thực hiện nào cả. Nhưng tôi vẫn không hiểu tại sao điều đó lại tạo ra sự khác biệt lớn như vậy.

Hãy giải thích cho tôi tại sao các giao diện được cho là tuyệt vời trong việc cho phép khớp nối lỏng lẻo, trong khi các siêu lớp đơn giản thì không.


3
Hầu hết các ngôn ngữ (ví dụ Java, C #) có giao diện của người dùng, chỉ hỗ trợ kế thừa duy nhất. Vì mỗi lớp chỉ có thể có một siêu lớp ngay lập tức, các siêu lớp (trừu tượng) quá giới hạn để một đối tượng hỗ trợ nhiều trừu tượng. Kiểm tra các đặc điểm (ví dụ: Scala hoặc Perl's Roles ) để biết cách thay thế hiện đại, điều này cũng tránh được vấn đề kim cương trên đường sắt với nhiều sự kế thừa.
amon

@amon Vì vậy, bạn đang nói rằng lợi thế của giao diện so với các lớp trừu tượng khi cố gắng đạt được khớp nối lỏng lẻo là chúng không bị giới hạn bởi sự kế thừa duy nhất?
Aviv Cohn

Không, tôi có nghĩa là tốn kém về trình biên dịch có nhiều việc phải làm khi nó xử lý một lớp trừu tượng , nhưng điều này có thể bị bỏ qua.
pasty

2
Có vẻ như @amon đang đi đúng hướng, tôi đã tìm thấy bài đăng này nơi người ta nói rằng: interfaces are essential for single-inheritance languages like Java and C# because that's the only way in which you can aggregate different behaviors into a single class(điều này cho tôi so sánh với C ++, trong đó giao diện chỉ là các lớp với các hàm ảo thuần túy).
pasty

Xin vui lòng cho biết ai nói siêu xe là xấu.
Tulains Córdova

Câu trả lời:


11

Thuật ngữ: Tôi sẽ đề cập đến cấu trúc ngôn ngữ interfacenhư giao diện và giao diện của một loại hoặc đối tượng là bề mặt (vì thiếu một thuật ngữ tốt hơn).

Khớp nối lỏng lẻo có thể đạt được bằng cách có một đối tượng phụ thuộc vào sự trừu tượng thay vì một loại cụ thể.

Chính xác.

Điều này cho phép ghép lỏng vì hai lý do chính: 1 - trừu tượng ít có khả năng thay đổi hơn các loại cụ thể, điều đó có nghĩa là mã phụ thuộc ít có khả năng bị phá vỡ. 2 - các loại bê tông khác nhau có thể được sử dụng trong thời gian chạy, bởi vì tất cả chúng đều phù hợp với sự trừu tượng. Các loại bê tông mới cũng có thể được thêm vào sau mà không cần thay đổi mã phụ thuộc hiện có.

Không hoàn toàn chính xác. Các ngôn ngữ hiện tại thường không dự đoán rằng một sự trừu tượng sẽ thay đổi (mặc dù có một số mẫu thiết kế để xử lý điều đó). Tách biệt cụ thể khỏi những điều chung chung trừu tượng. Điều này thường được thực hiện bởi một số lớp trừu tượng . Lớp này có thể được thay đổi thành một số chi tiết cụ thể khác mà không vi phạm mã xây dựng dựa trên sự trừu tượng hóa này - đạt được khớp nối lỏng lẻo. Ví dụ không phải OOP: Một sortthói quen có thể được thay đổi từ Quicksort trong phiên bản 1 sang Tim Sắp xếp trong phiên bản 2. Mã chỉ phụ thuộc vào kết quả được sắp xếp (nghĩa là được xây dựng dựa trên sự sorttrừu tượng hóa) do đó được tách rời khỏi triển khai sắp xếp thực tế.

Những gì tôi gọi là bề mặt ở trên là phần chung của một sự trừu tượng. Bây giờ xảy ra trong OOP rằng một đối tượng đôi khi phải hỗ trợ nhiều trừu tượng. Một ví dụ không hoàn toàn tối ưu: Java java.util.LinkedListhỗ trợ cả Listgiao diện về giao diện trừu tượng có trật tự, bộ sưu tập có thể lập chỉ mục, và hỗ trợ Queuegiao diện (nói một cách thô thiển) là về sự trừu tượng hóa của FIFO.

Làm thế nào một đối tượng có thể hỗ trợ nhiều trừu tượng?

C ++ không có giao diện, nhưng nó có nhiều kế thừa, phương thức ảo và các lớp trừu tượng. Một sự trừu tượng hóa sau đó có thể được định nghĩa là một lớp trừu tượng (nghĩa là một lớp không thể được khởi tạo ngay lập tức) mà khai báo, nhưng không định nghĩa các phương thức ảo. Các lớp thực hiện các chi tiết cụ thể của một sự trừu tượng sau đó có thể kế thừa từ lớp trừu tượng đó và thực hiện các phương thức ảo cần thiết.

Vấn đề ở đây là việc thừa kế nhiều lần có thể dẫn đến vấn đề kim cương , trong đó thứ tự các lớp được tìm kiếm để thực hiện phương thức (thứ tự giải quyết phương thức MRO) có thể dẫn đến mâu thuẫn trên băng. Có hai câu trả lời cho điều này:

  1. Xác định một trật tự lành mạnh và từ chối các đơn đặt hàng không thể được tuyến tính hóa hợp lý. Các C3 MRO là khá hợp lý và hoạt động tốt. Nó được xuất bản năm 1996.

  2. Đi theo con đường dễ dàng và từ chối nhiều kế thừa trong suốt.

Java đã chọn tùy chọn thứ hai và chọn kế thừa hành vi đơn. Tuy nhiên, chúng ta vẫn cần khả năng của một đối tượng để hỗ trợ nhiều trừu tượng. Do đó, các giao diện phải được sử dụng không hỗ trợ các định nghĩa phương thức, chỉ khai báo.

Kết quả là MRO là hiển nhiên (chỉ cần nhìn vào từng siêu lớp theo thứ tự) và đối tượng của chúng ta có thể có nhiều bề mặt cho bất kỳ số lượng trừu tượng nào.

Điều này hóa ra là không thỏa đáng, bởi vì thường thì một chút hành vi là một phần của bề mặt. Hãy xem xét một Comparablegiao diện:

interface Comparable<T> {
    public int cmp(T that);
    public boolean lt(T that);  // less than
    public boolean le(T that);  // less than or equal
    public boolean eq(T that);  // equal
    public boolean ne(T that);  // not equal
    public boolean ge(T that);  // greater than or equal
    public boolean gt(T that);  // greater than
}

Điều này rất thân thiện với người dùng (một API đẹp với nhiều phương thức thuận tiện), nhưng tẻ nhạt để thực hiện. Chúng tôi muốn giao diện chỉ bao gồm cmpvà tự động thực hiện các phương thức khác theo phương thức được yêu cầu đó. Mixins , nhưng quan trọng hơn là Đặc điểm [ 1 ], [ 2 ] giải quyết vấn đề này mà không rơi vào bẫy của nhiều kế thừa.

Điều này được thực hiện bằng cách định nghĩa một thành phần tính trạng để các tính trạng không thực sự tham gia vào MRO - thay vào đó các phương thức được xác định được đưa vào lớp triển khai.

Các Comparablegiao diện có thể được thể hiện bằng Scala như

trait Comparable[T] {
    def cmp(that: T): Int
    def lt(that: T): Boolean = this.cmp(that) <  0
    def le(that: T): Boolean = this.cmp(that) <= 0
    ...
}

Khi một lớp sau đó sử dụng đặc điểm đó, các phương thức khác sẽ được thêm vào định nghĩa lớp:

// "extends" isn't different from Java's "implements" in this case
case class Inty(val x: Int) extends Comparable[Inty] {
    override def cmp(that: Inty) = this.x - that.x
    // lt etc. get added automatically
}

Vì vậy, Inty(4) cmp Inty(6)sẽ -2Inty(4) lt Inty(6)sẽ được true.

Nhiều ngôn ngữ có một số hỗ trợ cho các đặc điểm và bất kỳ ngôn ngữ nào có Giao thức Metaobject Giao thức (MOP) có thể có các đặc điểm được thêm vào đó. Bản cập nhật Java 8 gần đây đã thêm các phương thức mặc định tương tự như các đặc điểm (các phương thức trong giao diện có thể có các triển khai dự phòng để tùy chọn triển khai các lớp để thực hiện các phương thức này).

Thật không may, đặc điểm là một phát minh khá gần đây (2002), và do đó khá hiếm trong các ngôn ngữ chính lớn hơn.


Câu trả lời tốt, nhưng tôi sẽ thêm rằng các ngôn ngữ thừa kế đơn có thể làm mờ nhiều kế thừa bằng cách sử dụng các giao diện với thành phần.

4

Điều tôi không hiểu là đây:

Trừu tượng có thể là siêu lớp hoặc giao diện.

Nếu vậy, tại sao các giao diện được ca ngợi đặc biệt cho khả năng cho phép khớp nối lỏng lẻo? Tôi không thấy nó khác với việc sử dụng siêu lớp như thế nào.

Đầu tiên, phân nhóm và trừu tượng là hai điều khác nhau. Subtyping chỉ có nghĩa là tôi có thể thay thế các giá trị của một loại cho các giá trị của loại khác - không loại nào cần phải trừu tượng.

Quan trọng hơn, các lớp con có sự phụ thuộc trực tiếp vào các chi tiết triển khai của siêu lớp của chúng. Đó là loại khớp nối mạnh nhất. Trong thực tế, nếu lớp cơ sở không được thiết kế có tính kế thừa, thì những thay đổi đối với lớp cơ sở không thay đổi hành vi của nó vẫn có thể phá vỡ các lớp con và không có cách nào để biết tiên nghiệm nếu xảy ra vỡ. Điều này được gọi là vấn đề lớp cơ sở mong manh .

Việc triển khai một giao diện không kết nối bạn với bất cứ điều gì ngoại trừ chính giao diện đó không chứa hành vi.


Cảm ơn đã trả lời. Để xem tôi có hiểu không: khi bạn muốn một đối tượng tên A phụ thuộc vào một sự trừu tượng có tên B thay vì triển khai cụ thể của sự trừu tượng đó có tên C, thì B thường là một giao diện được triển khai bởi C, thay vì siêu lớp được mở rộng bởi C. Điều này là do: C phân lớp B kết hợp chặt chẽ C thành B. Nếu B thay đổi - C thay đổi. Tuy nhiên, C triển khai B (B là một giao diện) không kết hợp B đến C: B chỉ là một danh sách các phương thức C phải thực hiện, do đó không có khớp nối chặt chẽ. Tuy nhiên, liên quan đến đối tượng A (người phụ thuộc), việc B là lớp hay giao diện không thành vấn đề.
Aviv Cohn

Chính xác? ..... .....
Manila Cohn

Tại sao bạn lại coi một giao diện được ghép nối với bất cứ thứ gì?
Michael Shaw

Tôi nghĩ rằng câu trả lời này đóng đinh trên đầu. Tôi sử dụng C ++ khá nhiều và như đã nêu trong một trong những câu trả lời khác, C ++ không hoàn toàn có giao diện nhưng bạn giả mạo nó bằng cách sử dụng siêu lớp với tất cả các phương thức còn lại là "thuần ảo" (tức là do trẻ em thực hiện). Vấn đề là, thật dễ dàng để tạo các lớp cơ sở LÀM cái gì đó cùng với chức năng được ủy quyền. Trong nhiều, rất nhiều trường hợp, tôi và đồng nghiệp nhận thấy rằng bằng cách đó, một trường hợp sử dụng mới xuất hiện và làm mất hiệu lực của chức năng chia sẻ đó. Nếu có chức năng chia sẻ cần thiết, thật dễ dàng để tạo một lớp trợ giúp.
J Trana

@Prog Dòng suy nghĩ của bạn chủ yếu là chính xác, nhưng một lần nữa, sự trừu tượng và phân nhóm là hai điều riêng biệt. Khi bạn nói rằng you want an object named A to depend on an abstraction named B instead of a concrete implementation of that abstraction named Cbạn cho rằng các lớp không bằng cách nào đó trừu tượng. Một sự trừu tượng hóa là bất cứ điều gì che giấu các chi tiết triển khai, vì vậy một lớp với các trường riêng cũng trừu tượng như một giao diện với cùng các phương thức chung.
Doval

1

Có sự kết hợp giữa các lớp cha mẹ và con, vì đứa trẻ phụ thuộc vào cha mẹ.

Giả sử chúng ta có một lớp A và lớp B thừa hưởng từ nó. Nếu chúng ta vào lớp A và thay đổi mọi thứ, lớp B cũng bị thay đổi.

Giả sử chúng ta có một giao diện I và lớp B thực hiện nó. Nếu chúng ta thay đổi giao diện I, thì mặc dù lớp B có thể không thực hiện được nữa, nhưng lớp B không thay đổi.


Tôi tò mò liệu các downvoters có lý do hay chỉ là có một ngày tồi tệ.
Michael Shaw

1
Tôi không downvote, nhưng tôi nghĩ nó có thể phải làm với câu đầu tiên. Các lớp con được kết hợp với các lớp cha, không phải là cách khác. Cha mẹ không cần biết gì về đứa trẻ, nhưng đứa trẻ cần có kiến ​​thức sâu sắc về cha mẹ.

@JohnGaughan: Cảm ơn đã phản hồi. Chỉnh sửa cho rõ ràng.
Michael Shaw
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.