Mengapa antarmuka lebih bermanfaat daripada superclasses dalam mencapai kopling longgar?

15

( Untuk keperluan pertanyaan ini, ketika saya mengatakan 'antarmuka' yang saya maksud adalah konstruk bahasainterface , dan bukan 'antarmuka' dalam arti kata lain, yaitu metode publik yang ditawarkan kelas kepada dunia luar untuk berkomunikasi dengan dan memanipulasi itu. )

Kopling longgar dapat dicapai dengan memiliki objek bergantung pada abstraksi bukan jenis beton.

Hal ini memungkinkan untuk lepas kopling karena dua alasan utama: 1- abstraksi lebih kecil kemungkinannya untuk berubah daripada jenis beton, yang berarti kode dependen lebih kecil kemungkinannya untuk dipatahkan. 2- jenis beton yang berbeda dapat digunakan saat runtime, karena mereka semua sesuai dengan abstraksi. Jenis beton baru juga dapat ditambahkan nanti tanpa perlu mengubah kode dependen yang ada.

Sebagai contoh, pertimbangkan kelas Cardan dua subclass Volvodan Mazda.

Jika kode Anda bergantung pada a Car, ia bisa menggunakan a Volvoatau a Mazdaselama runtime. Juga nanti subkelas tambahan dapat ditambahkan tanpa perlu mengubah kode dependen.

Juga, Car- yang merupakan abstraksi - kecil kemungkinannya berubah daripada Volvoatau Mazda. Mobil pada umumnya sama untuk beberapa waktu, tetapi Volvo dan Mazda jauh lebih mungkin untuk berubah. Yaitu abstraksi lebih stabil daripada jenis beton.

Semua ini untuk menunjukkan bahwa saya memahami apa itu kopling longgar dan bagaimana hal itu dicapai dengan bergantung pada abstraksi dan bukan pada konkretsi. (Jika saya menulis sesuatu yang tidak akurat, tolong katakan demikian).

Yang tidak saya mengerti adalah ini:

Abstraksi dapat berupa superclasses atau antarmuka.

Jika demikian, mengapa antarmuka secara khusus dipuji karena kemampuan mereka untuk memungkinkan kopling longgar? Saya tidak melihat perbedaannya dengan menggunakan superclass.

Satu-satunya perbedaan yang saya lihat adalah: 1- Antarmuka tidak dibatasi oleh pewarisan tunggal, tapi itu tidak ada hubungannya dengan topik kopling longgar. 2- Antarmuka lebih 'abstrak' karena mereka tidak memiliki logika implementasi sama sekali. Tapi tetap saja, saya tidak mengerti mengapa itu membuat perbedaan besar.

Tolong jelaskan kepada saya mengapa antarmuka dikatakan hebat dalam memungkinkan kopling longgar, sedangkan superclasses sederhana tidak.

Aviv Cohn
sumber
3
Sebagian besar bahasa (mis. Java, C #) yang memiliki "antarmuka" hanya mendukung pewarisan tunggal. Karena setiap kelas hanya dapat memiliki satu superclass langsung, superclasses (abstrak) terlalu terbatas agar satu objek dapat mendukung banyak abstraksi. Periksa sifat-sifat (misalnya Peran Scala atau Perl ) untuk alternatif modern yang juga menghindari " masalah berlian " dengan pewarisan berganda.
amon
@amon Jadi Anda mengatakan bahwa keuntungan dari antarmuka lebih dari kelas abstrak ketika mencoba mencapai kopling longgar adalah mereka tidak dibatasi oleh pewarisan tunggal?
Aviv Cohn
Tidak, maksud saya mahal dalam hal kompiler lebih harus dilakukan ketika menangani kelas abstrak , tetapi ini mungkin bisa diabaikan.
pucat
2
Sepertinya @amon berada di jalur yang benar, saya menemukan posting ini di mana dikatakan bahwa: 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(yang membuat saya perbandingan dengan C ++, di mana antarmuka hanya kelas dengan fungsi virtual murni).
pucat
Tolong beritahu siapa bilang superclasses itu buruk.
Tulains Córdova

Jawaban:

11

Terminologi: Saya akan merujuk pada konstruksi bahasa interfacesebagai antarmuka , dan antarmuka jenis atau objek sebagai permukaan (karena tidak ada istilah yang lebih baik).

Kopling longgar dapat dicapai dengan memiliki objek bergantung pada abstraksi bukan jenis beton.

Benar.

Hal ini memungkinkan untuk kopling longgar karena dua alasan utama: 1 - abstraksi lebih kecil kemungkinannya untuk berubah daripada jenis beton, yang berarti kode dependen lebih kecil kemungkinannya untuk dipatahkan. 2 - tipe beton yang berbeda dapat digunakan saat runtime, karena semuanya sesuai dengan abstraksi. Jenis beton baru juga dapat ditambahkan nanti tanpa perlu mengubah kode dependen yang ada.

Tidak sepenuhnya benar. Bahasa saat ini umumnya tidak mengantisipasi bahwa abstraksi akan berubah (walaupun ada beberapa pola desain untuk mengatasinya). Memisahkan spesifik dari hal - hal umum adalah abstraksi. Ini biasanya dilakukan oleh beberapa lapisan abstraksi . Lapisan ini dapat diubah ke beberapa spesifik lain tanpa melanggar kode yang dibangun di atas abstraksi ini - kopling longgar tercapai. Contoh Non-OOP: Suatu sortrutin dapat diubah dari Quicksort di versi 1 ke Tim Sort di versi 2. Kode yang hanya tergantung pada hasil yang diurutkan (yaitu dibangun berdasarkan sortabstraksi) karena itu dipisahkan dari implementasi penyortiran yang sebenarnya.

Apa yang saya sebut permukaan di atas adalah bagian umum dari abstraksi. Sekarang terjadi di OOP bahwa satu objek terkadang harus mendukung banyak abstraksi. Contoh yang tidak terlalu optimal: Java java.util.LinkedListmendukung kedua Listantarmuka yaitu tentang abstraksi "teratur, dapat diindeks koleksi", dan mendukung Queueantarmuka yang (secara kasar) adalah tentang abstraksi "FIFO".

Bagaimana suatu objek dapat mendukung banyak abstraksi?

C ++ tidak memiliki antarmuka, tetapi memiliki banyak pewarisan, metode virtual, dan kelas abstrak. Abstraksi kemudian dapat didefinisikan sebagai kelas abstrak (yaitu kelas yang tidak dapat segera dipakai) yang menyatakan, tetapi tidak mendefinisikan metode virtual. Kelas yang mengimplementasikan spesifik abstraksi kemudian dapat mewarisi dari kelas abstrak dan mengimplementasikan metode virtual yang diperlukan.

Masalahnya di sini adalah bahwa pewarisan berganda dapat menyebabkan masalah intan , di mana urutan di mana kelas dicari untuk implementasi metode (MRO: metode resolusi urutan) dapat menyebabkan "kontradiksi". Ada dua tanggapan untuk ini:

  1. Tetapkan perintah yang waras dan tolak perintah yang tidak bisa diluruskan dengan bijaksana. The C3 MRO cukup masuk akal dan bekerja dengan baik. Itu diterbitkan tahun 1996.

  2. Ambil rute yang mudah dan tolak beberapa warisan di seluruh.

Java mengambil opsi yang terakhir dan memilih warisan perilaku tunggal. Namun, kita masih membutuhkan kemampuan suatu objek untuk mendukung banyak abstraksi. Oleh karena itu, antarmuka harus digunakan yang tidak mendukung definisi metode, hanya deklarasi.

Hasilnya adalah bahwa MRO jelas (lihat saja setiap superclass secara berurutan), dan bahwa objek kita dapat memiliki beberapa permukaan untuk sejumlah abstraksi.

Ini ternyata agak tidak memuaskan, karena cukup sering sedikit perilaku adalah bagian dari permukaan. Pertimbangkan sebuah Comparableantarmuka:

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
}

Ini sangat user-friendly (API yang bagus dengan banyak metode yang mudah), tetapi membosankan untuk diimplementasikan. Kami ingin antarmuka hanya menyertakan cmp, dan menerapkan metode lain secara otomatis dalam hal satu metode yang diperlukan. Mixin , tetapi yang lebih penting Ciri-ciri [ 1 ], [ 2 ] menyelesaikan masalah ini tanpa jatuh ke dalam perangkap pewarisan berganda.

Ini dilakukan dengan mendefinisikan komposisi sifat sehingga sifat-sifat tersebut tidak benar-benar berakhir mengambil bagian dalam MRO - sebagai gantinya metode yang didefinisikan disusun ke dalam kelas pelaksana.

The Comparableantarmuka dapat dinyatakan dalam Scala sebagai

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
    ...
}

Ketika sebuah kelas kemudian menggunakan sifat itu, metode lain ditambahkan ke definisi kelas:

// "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
}

Begitu Inty(4) cmp Inty(6)juga akan -2dan Inty(4) lt Inty(6)akan terjadi true.

Banyak bahasa memiliki beberapa dukungan untuk sifat-sifat, dan bahasa apa pun yang memiliki "Metaobject Protocol (MOP)" dapat memiliki sifat ditambahkan ke dalamnya. Pembaruan Java 8 baru-baru ini menambahkan metode default yang mirip dengan ciri-ciri (metode dalam antarmuka dapat memiliki implementasi fallback sehingga opsional untuk menerapkan kelas untuk mengimplementasikan metode ini).

Sayangnya, sifat-sifat adalah penemuan yang cukup baru (2002), dan dengan demikian cukup langka dalam bahasa arus utama yang lebih besar.

amon
sumber
Jawaban yang bagus, tetapi saya ingin menambahkan bahwa bahasa pewarisan tunggal dapat memperdaya banyak pewarisan menggunakan antarmuka dengan komposisi.
4

Yang tidak saya mengerti adalah ini:

Abstraksi dapat berupa superclasses atau antarmuka.

Jika demikian, mengapa antarmuka secara khusus dipuji karena kemampuan mereka untuk memungkinkan kopling longgar? Saya tidak melihat perbedaannya dengan menggunakan superclass.

Pertama, subtyping dan abstraksi adalah dua hal yang berbeda. Subtipe hanya berarti bahwa saya dapat mengganti nilai satu jenis dengan nilai dari jenis lain - kedua jenis tersebut tidak perlu abstrak.

Lebih penting lagi, subclass memiliki ketergantungan langsung pada detail implementasi superclass mereka. Itu jenis kopling terkuat yang ada. Faktanya, jika kelas dasar tidak dirancang dengan mempertimbangkan warisan, perubahan ke kelas dasar yang tidak mengubah perilakunya masih dapat memecahkan subclass, dan tidak ada cara untuk mengetahui apriori jika kerusakan akan terjadi. Ini dikenal sebagai masalah kelas dasar yang rapuh .

Menerapkan antarmuka tidak membuat Anda berpasangan dengan apa pun kecuali antarmuka itu sendiri, yang tidak mengandung perilaku.

Doval
sumber
Terimakasih telah menjawab. Untuk melihat apakah saya mengerti: ketika Anda ingin objek bernama A bergantung pada abstraksi bernama B, bukan implementasi konkret dari abstraksi bernama C itu, seringkali lebih baik bagi B untuk menjadi antarmuka yang diimplementasikan oleh C, alih-alih sebuah superclass yang diperluas oleh C. Ini karena: C mensubklasifikasi B dengan pasangan yang erat C ke B. Jika B berubah - C berubah. Namun C mengimplementasikan B (B menjadi antarmuka) tidak pasangan B ke C: B hanya daftar metode C harus menerapkan, sehingga tidak ada kopling ketat. Namun mengenai objek A (dependen), tidak masalah apakah B adalah kelas atau antarmuka.
Aviv Cohn
Benar? ..... ....
Aviv Cohn
Mengapa Anda menganggap antarmuka untuk digabungkan dengan apa pun?
Michael Shaw
Saya pikir jawaban ini tepat di kepala. Saya menggunakan C ++ sedikit, dan seperti yang dinyatakan dalam salah satu jawaban lain, C ++ tidak cukup memiliki antarmuka tetapi Anda memalsukannya dengan menggunakan superclasses dengan semua metode yang tersisa sebagai "virtual murni" (yaitu diimplementasikan oleh anak-anak). Intinya adalah, mudah untuk membuat kelas dasar yang MELAKUKAN sesuatu bersama dengan fungsionalitas yang didelegasikan. Dalam banyak, banyak, banyak kasus, saya dan rekan kerja saya menemukan bahwa dengan melakukan itu use case baru ikut serta dan membatalkan beberapa fungsionalitas yang dibagikan. Jika ada fungsi bersama yang dibutuhkan, cukup mudah untuk membuat kelas pembantu.
J Trana
@Prog Garis pemikiran Anda sebagian besar benar, tetapi sekali lagi, abstraksi dan subtipe adalah dua hal yang terpisah. Ketika Anda mengatakan you want an object named A to depend on an abstraction named B instead of a concrete implementation of that abstraction named CAnda berasumsi bahwa kelas entah bagaimana tidak abstrak. Abstraksi adalah segala sesuatu yang menyembunyikan detail implementasi, sehingga kelas dengan bidang pribadi sama abstraknya dengan antarmuka dengan metode publik yang sama.
Doval
1

Ada sambungan antara kelas induk dan anak, karena anak bergantung pada orangtua.

Katakanlah kita memiliki kelas A, dan kelas B mewarisi darinya. Jika kita masuk ke kelas A dan mengubah banyak hal, kelas B juga berubah.

Katakanlah kita memiliki antarmuka I, dan kelas B mengimplementasikannya. Jika kita mengubah antarmuka I, maka meskipun kelas B mungkin tidak mengimplementasikannya lagi, kelas B tidak berubah.

Michael Shaw
sumber
Saya ingin tahu apakah downvoters punya alasan, atau hanya mengalami hari yang buruk.
Michael Shaw
1
Saya tidak downvote, tapi saya pikir itu mungkin ada hubungannya dengan kalimat pertama. Kelas anak digabungkan ke kelas orang tua, bukan sebaliknya. Orang tua tidak perlu tahu apa-apa tentang anak itu, tetapi anak itu membutuhkan pengetahuan yang mendalam tentang orang tua.
@JohnGaughan: Terima kasih atas umpan baliknya. Diedit untuk kejelasan.
Michael Shaw