Apakah antarmuka dianggap 'kosong' jika mewarisi dari antarmuka lain?

12

Antarmuka kosong umumnya menganggap praktik buruk, sejauh yang saya tahu - terutama di mana hal-hal seperti atribut didukung oleh bahasa.

Namun, apakah sebuah antarmuka dianggap 'kosong' jika ia mewarisi dari antarmuka lain?

interface I1 { ... }
interface I2 { ... } //unrelated to I1

interface I3
    : I1, I2
{
    // empty body
}

Apa pun yang mengimplementasikan I3perlu diimplementasikan I1dan I2, dan objek dari kelas berbeda yang mewarisi I3kemudian dapat digunakan secara bergantian (lihat di bawah), jadi apakah itu benar untuk memanggil I3 kosong ? Jika demikian, apa yang akan menjadi cara yang lebih baik untuk arsitektur ini?

// with I3 interface
class A : I3 { ... }
class B : I3 { ... }

class Test {
    void foo() {
        I3 something = new A();
        something = new B();
        something.SomeI1Function();
        something.SomeI2Function();
    }
}

// without I3 interface
class A : I1, I2 { ... }
class B : I1, I2 { ... }

class Test {
    void foo() {
        I1 something = new A();
        something = new B();
        something.SomeI1Function();
        something.SomeI2Function(); // we can't do this without casting first
    }
}
Kai
sumber
1
Tidak dapat menentukan beberapa antarmuka sebagai jenis parameter / bidang, dll. Adalah cacat desain dalam C # /. NET.
CodesInChaos
Saya pikir cara yang lebih baik untuk arsitektur bagian ini harus masuk ke pertanyaan terpisah. Saat ini jawaban yang diterima tidak ada hubungannya dengan judul pertanyaan.
Kapol
@ CodeInChaos Tidak, ini bukan karena sudah ada dua cara untuk melakukan ini. Salah satunya adalah dalam pertanyaan ini, cara lain adalah menentukan parameter tipe generik untuk argumen metode dan menggunakan pembatasan di mana untuk menentukan kedua antarmuka.
Andy
Apakah masuk akal bahwa kelas yang mengimplementasikan I1 dan I2, tetapi bukan I3, seharusnya tidak dapat digunakan oleh foo? (Jika jawabannya "ya", silakan saja!)
user253751
@Andy Sementara saya tidak sepenuhnya setuju dengan klaim CodesInChaos bahwa itu cacat seperti itu, saya tidak berpikir bahwa parameter tipe generik pada metode adalah solusi - itu mendorong tanggung jawab mengetahui jenis yang digunakan dalam implementasi metode ke apa pun memanggil metode, yang terasa salah. Mungkin beberapa sintaks seperti var : I1, I2 something = new A();untuk variabel lokal akan lebih baik (jika kita mengabaikan poin baik yang diangkat dalam jawaban Konrad)
Kai

Jawaban:

27

Apa pun yang mengimplementasikan I3 perlu mengimplementasikan I1 dan I2, dan objek dari kelas yang berbeda yang mewarisi I3 kemudian dapat digunakan secara bergantian (lihat di bawah), jadi apakah benar untuk memanggil I3 kosong? Jika demikian, apa yang akan menjadi cara yang lebih baik untuk arsitektur ini?

"Bundling" I1dan I2menjadi I3penawaran yang bagus (?) Shortcut, atau semacam gula sintaks untuk manapun Anda ingin baik untuk dilaksanakan.

Masalah dengan pendekatan ini adalah bahwa Anda tidak dapat menghentikan pengembang lain dari penerapan I1 dan I2 berdampingan secara eksplisit , tetapi tidak bekerja dua arah - meskipun : I3setara : I1, I2, : I1, I2tidak setara : I3. Bahkan jika mereka identik secara fungsional, kelas yang mendukung keduanya I1dan I2tidak akan terdeteksi sebagai pendukung I3.

Ini berarti Anda menawarkan dua cara untuk mencapai hal yang sama - "manual" dan "manis" (dengan gula sintaksis Anda). Dan itu dapat berdampak buruk pada konsistensi kode. Menawarkan 2 cara berbeda untuk mencapai hal yang sama di sini, di sana, dan di tempat lain menghasilkan 8 kemungkinan kombinasi :)

void foo() {
    I1 something = new A();
    something = new B();
    something.SomeI1Function();
    something.SomeI2Function(); // we can't do this without casting first
}

OKE, kamu tidak bisa. Tapi mungkin itu pertanda baik?

Jika I1dan I2menyiratkan tanggung jawab yang berbeda (jika tidak, tidak perlu memisahkan mereka di tempat pertama), maka mungkin foo()salah untuk mencoba dan somethingmelakukan dua tanggung jawab yang berbeda sekaligus?

Dengan kata lain, bisa jadi itu foo()sendiri melanggar prinsip Tanggung Jawab Tunggal, dan fakta bahwa itu membutuhkan pemeriksaan tipe casting dan runtime untuk menariknya adalah sebuah bendera merah yang mengkhawatirkan kita tentang hal itu.

EDIT:

Jika Anda bersikeras memastikan bahwa foodibutuhkan sesuatu yang mengimplementasikan I1dan I2, Anda dapat meneruskannya sebagai parameter terpisah:

void foo(I1 i1, I2 i2) 

Dan mereka bisa menjadi objek yang sama:

foo(something, something);

Apa foopeduli jika mereka adalah contoh yang sama atau tidak?

Tetapi jika itu benar-benar peduli (mis. Karena mereka tidak berkewarganegaraan), atau Anda hanya berpikir ini terlihat jelek, seseorang dapat menggunakan obat generik sebagai gantinya:

void Foo<T>(T something) where T: I1, I2

Sekarang kendala generik menangani efek yang diinginkan sambil tidak mencemari dunia luar dengan apa pun. Ini adalah C # idiomatis, berdasarkan pada pemeriksaan waktu kompilasi, dan secara keseluruhan terasa lebih tepat.

Saya melihat I3 : I2, I1agak sebagai upaya untuk meniru jenis serikat dalam bahasa yang tidak mendukungnya di luar kotak (C # atau Java tidak, sedangkan jenis serikat ada dalam bahasa fungsional).

Mencoba meniru suatu konstruksi yang tidak terlalu didukung oleh bahasa sering kali kikuk. Demikian pula, di C # Anda dapat menggunakan metode ekstensi dan antarmuka jika Anda ingin meniru mixin . Bisa jadi? Iya. Ide bagus? Saya tidak yakin.

Konrad Morawski
sumber
4

Jika ada kepentingan khusus dalam membuat kelas mengimplementasikan kedua antarmuka, maka saya tidak melihat ada yang salah dengan membuat antarmuka ketiga yang mewarisi dari keduanya. Namun, saya akan berhati-hati untuk tidak jatuh ke dalam perangkap hal-hal rekayasa ulang. Kelas bisa mewarisi dari satu atau yang lain, atau keduanya. Kecuali jika program saya memiliki banyak kelas dalam tiga kasus ini, itu sedikit banyak untuk membuat antarmuka untuk keduanya, dan tentu saja tidak jika kedua antarmuka tidak memiliki hubungan satu sama lain (tidak ada antarmuka IComparableAndSerializable silakan).

Saya mengingatkan Anda bahwa Anda dapat membuat kelas abstrak juga yang mewarisi dari kedua antarmuka, dan Anda dapat menggunakan kelas abstrak itu di tempat I3. Saya pikir itu salah untuk mengatakan bahwa Anda tidak boleh melakukannya, tetapi Anda setidaknya harus memiliki alasan yang bagus.

Neil
sumber
Hah? Anda tentu dapat mengimplementasikan dua antarmuka dalam satu kelas. Anda tidak mewarisi antarmuka, Anda mengimplementasikannya.
Andy
@Andy Di mana saya mengatakan bahwa satu kelas tidak dapat mengimplementasikan dua antarmuka? Untuk apa yang menyangkut penggunaan kata "inherit" di tempat "implement", semantik. Di C ++, inherit akan bekerja dengan baik karena hal yang paling dekat dengan interface adalah kelas abstrak.
Neil
Warisan dan implementasi antarmuka bukanlah konsep yang sama. Kelas-kelas yang mewarisi banyak subclass dapat menyebabkan beberapa masalah aneh, yang tidak terjadi pada penerapan beberapa antarmuka. Dan antarmuka C ++ kurang tidak berarti perbedaannya hilang, itu berarti bahwa C ++ terbatas pada apa yang dapat dilakukannya. Warisan adalah hubungan "is-a", sedangkan antarmuka menentukan "can-do."
Andy
@Andy Aneh masalah yang disebabkan oleh tabrakan antara metode yang diterapkan di kelas tersebut, ya. Namun, itu bukan lagi antarmuka tidak dalam nama atau dalam praktek. Kelas abstrak yang menyatakan tetapi tidak mengimplementasikan metode dalam arti kata kecuali nama, antarmuka. Terminologi berubah antar bahasa, tetapi itu tidak berarti bahwa referensi dan pointer bukan konsep yang sama. Ini poin yang sangat bagus, tetapi jika Anda bersikeras tentang ini, maka kita harus setuju untuk tidak setuju.
Neil
"Terminologi berubah antar bahasa" Tidak, tidak. Terminologi OO konsisten di semua bahasa; Saya tidak pernah mendengar kelas abstrak yang berarti hal lain dalam setiap bahasa OO yang saya gunakan. Ya, Anda dapat memiliki semantik antarmuka dalam C ++, tetapi intinya adalah tidak ada yang mencegah seseorang dari pergi dan menambahkan perilaku ke kelas abstrak dalam bahasa itu dan memecahkan semantik tersebut.
Andy
2

Antarmuka kosong umumnya menganggap praktik buruk

Saya tidak setuju. Baca tentang antarmuka marker .

Sedangkan antarmuka khas menentukan fungsionalitas (dalam bentuk deklarasi metode) yang harus didukung oleh kelas pelaksana, antarmuka penanda tidak perlu melakukannya. Kehadiran antarmuka semacam itu hanya menunjukkan perilaku spesifik pada bagian dari kelas pelaksana.

radarbob
sumber
Saya membayangkan OP menyadari mereka, tetapi mereka sebenarnya agak kontroversial. Sementara beberapa penulis merekomendasikan mereka (mis. Josh Bloch dalam "Java Efektif"), beberapa mengklaim mereka antipernah dan warisan dari masa ketika Jawa belum memiliki anotasi. (Anotasi di Jawa kira-kira setara dengan atribut di .NET). Kesan saya adalah mereka jauh lebih populer di dunia Java daripada .NET.
Konrad Morawski
1
Saya menggunakannya sekali-sekali, tapi rasanya agak lembek - sisi buruknya, jauh di atas kepala saya, adalah Anda tidak dapat membantu fakta bahwa mereka diwariskan, jadi setelah Anda menandai kelas dengan satu, semua anak-anaknya mendapatkan ditandai juga apakah Anda menginginkannya atau tidak. Dan mereka tampaknya mendorong praktik buruk dalam penggunaan instanceofalih-alih polimorfisme
Konrad Morawski