Menerapkan antarmuka saat Anda tidak membutuhkan salah satu properti

31

Cukup mudah. Saya menerapkan antarmuka, tetapi ada satu properti yang tidak perlu untuk kelas ini dan, pada kenyataannya, tidak boleh digunakan. Ide awal saya adalah melakukan sesuatu seperti:

int IFoo.Bar
{
    get { raise new NotImplementedException(); }
}

Saya kira tidak ada yang salah dengan ini, tapi itu tidak terasa "benar". Adakah orang lain yang pernah mengalami situasi serupa sebelumnya? Jika demikian, bagaimana Anda mendekatinya?

Chris Pratt
sumber
1
Saya samar-samar ingat ada beberapa kelas semi-umum digunakan dalam C # yang mengimplementasikan antarmuka tetapi secara eksplisit menyatakan dalam dokumentasi bahwa metode tertentu tidak diimplementasikan. Saya akan mencoba melihat apakah saya dapat menemukannya.
Mage Xy
Saya pasti akan tertarik untuk melihatnya, jika Anda dapat menemukannya.
Chris Pratt
23
Saya dapat menunjukkan beberapa kasus ini di perpustakaan .NET - DAN MEREKA SEMUA DIAKUI SEBAGAI KESALAHAN YANG BURUK . Ini adalah pelanggaran ikonik dan umum dari Prinsip Pergantian Liskov - alasan untuk tidak melanggar LSP dapat ditemukan dalam jawaban saya di sini
Jimmy Hoffa
3
Apakah Anda harus mengimplementasikan antarmuka spesifik ini, atau dapatkah Anda memperkenalkan antarmuka super dan menggunakannya?
Christopher Schultz
6
"satu properti yang tidak perlu untuk kelas ini" - Apakah diperlukan bagian dari antarmuka, tergantung pada klien antarmuka, bukan pelaksana. Jika suatu kelas tidak dapat mengimplementasikan anggota antarmuka secara wajar, maka kelas tersebut tidak sesuai untuk antarmuka tersebut. Ini mungkin berarti bahwa antarmuka dirancang dengan buruk - mungkin mencoba melakukan terlalu banyak - tetapi itu tidak membantu kelas.
Sebastian Redl

Jawaban:

51

Ini adalah contoh klasik tentang bagaimana orang memutuskan untuk melanggar Prinsip Subtitusi Liskov. Saya sangat tidak menyarankannya tetapi akan mendorong kemungkinan solusi yang berbeda:

  1. Mungkin kelas yang Anda tulis tidak menyediakan fungsionalitas yang ditentukan antarmuka jika tidak menggunakan semua anggota antarmuka.
  2. Atau, antarmuka itu dapat melakukan banyak hal dan dapat dipisahkan berdasarkan Prinsip Pemisahan Antarmuka.

Jika yang pertama adalah kasus untuk Anda, hanya saja jangan mengimplementasikan antarmuka pada kelas itu. Anggap saja seperti soket listrik di mana lubang arde tidak perlu sehingga tidak benar - benar menempel ke arde. Anda tidak memasukkan apa pun dengan ground dan bukan masalah besar! Tapi begitu Anda menggunakan sesuatu yang membutuhkan tanah - Anda bisa mengalami kegagalan yang spektakuler. Lebih baik tidak meninju lubang tanah palsu. Jadi, jika kelas Anda tidak benar - benar melakukan apa yang diinginkan antarmuka, jangan mengimplementasikan antarmuka.


Berikut adalah beberapa bit cepat dari wikipedia:

Prinsip Substitusi Liskov dapat dengan mudah dirumuskan sebagai, "Jangan memperkuat pra-kondisi, dan jangan melemahkan pasca-kondisi".

Secara lebih formal, prinsip substitusi Liskov (LSP) adalah definisi khusus dari hubungan subtyping, yang disebut subtyping perilaku (kuat), yang awalnya diperkenalkan oleh Barbara Liskov dalam sebuah pidato konferensi konferensi tahun 1987 yang berjudul Abstraksi dan hirarki data. Ini adalah hubungan semantik daripada hanya sintaksis karena bermaksud untuk menjamin interoperabilitas semantik dari jenis dalam suatu hierarki , [...]

Untuk interoperabilitas semantik dan substitusi antara implementasi berbeda dari kontrak yang sama - Anda membutuhkan mereka semua untuk berkomitmen pada perilaku yang sama.


Prinsip Segregasi Antarmuka berbicara pada gagasan bahwa antarmuka harus dipisahkan menjadi set yang kohesif sehingga Anda tidak memerlukan antarmuka yang melakukan banyak hal yang berbeda ketika Anda hanya menginginkan satu fasilitas. Pikirkan lagi antarmuka soket listrik, itu bisa memiliki termostat juga, tetapi itu akan membuat lebih sulit untuk memasang soket listrik dan mungkin membuatnya lebih sulit untuk digunakan untuk keperluan non-pemanasan. Seperti soket listrik dengan termostat, antarmuka besar sulit diimplementasikan dan sulit digunakan.

Prinsip pemisahan antarmuka (ISP) menyatakan bahwa tidak ada klien yang harus dipaksa untuk bergantung pada metode yang tidak digunakannya. [1] ISP membagi antarmuka yang sangat besar menjadi yang lebih kecil dan lebih spesifik sehingga klien hanya perlu tahu tentang metode yang menarik bagi mereka.

Jimmy Hoffa
sumber
Benar. Ini mungkin mengapa itu terasa tidak benar bagiku, sejak awal. Terkadang Anda hanya perlu pengingat lembut bahwa Anda melakukan sesuatu yang bodoh. Terima kasih.
Chris Pratt
@ ChrisPratt itu adalah kesalahan yang sangat umum untuk dibuat, itu sebabnya formalisme bisa berharga - mengklasifikasikan bau kode membantu untuk lebih cepat mengidentifikasi mereka dan mengingat solusi yang digunakan sebelumnya.
Jimmy Hoffa
4

Terlihat baik bagi saya, jika ini adalah situasi Anda.

Namun, menurut saya antarmuka Anda (atau menggunakannya) rusak jika kelas turunan tidak benar-benar mengimplementasikan semua itu. Pertimbangkan untuk memisahkan antarmuka itu.

Penafian: Ini membutuhkan beberapa pewarisan untuk melakukannya dengan benar, dan saya tidak tahu apakah C # mendukungnya.

Lightness Races dengan Monica
sumber
6
C # tidak mendukung multiple inheritance of class, tetapi ia mendukungnya untuk antarmuka.
mgw854
2
Saya pikir kamu benar. Lagipula antarmuka adalah kontrak, dan bahkan jika saya tahu kelas khusus ini tidak akan digunakan dengan cara yang merusak apa pun yang menggunakan antarmuka jika properti ini dinonaktifkan, itu tidak jelas.
Chris Pratt
@ ChrisPratt: Ya.
Lightness Races with Monica
4

Saya telah menemukan situasi ini. Bahkan seperti yang ditunjukkan di tempat lain BCL memiliki contoh seperti itu ... Saya akan mencoba memberikan contoh yang lebih baik dan memberikan beberapa alasan:

Ketika Anda memiliki antarmuka yang sudah dikirim yang Anda simpan untuk alasan kompatibilitas dan ...

  • Antarmuka berisi anggota yang usang atau rusak. Misalnya BlockingCollection<T>.ICollection.SyncRoot(antara lain) sementara ICollection.SyncRoottidak usang, itu akan melempar NotSupportedException.

  • Antarmuka berisi anggota yang didokumentasikan sebagai opsional, dan implementasi dapat membuang pengecualian. Misalnya pada MSDN tentang IEnumerator.Resethal itu mengatakan:

Metode Reset disediakan untuk interoperabilitas COM. Itu tidak perlu diimplementasikan; sebagai gantinya, pelaksana dapat dengan mudah melempar NotSupportedException.

  • Dengan kesalahan desain antarmuka, seharusnya lebih dari satu antarmuka di tempat pertama. Ini adalah pola umum dalam BCL untuk mengimplementasikan versi Read Only dari wadah NotSupportedException. Saya telah melakukannya sendiri, inilah yang diharapkan sekarang ... Saya ICollection<T>.IsReadOnlykembali truesehingga Anda dapat memberi tahu mereka appart. Desain yang benar seharusnya memiliki versi antarmuka yang Dapat Dibaca, dan kemudian antarmuka penuh mewarisi dari itu.

  • Tidak ada antarmuka yang lebih baik untuk digunakan. Sebagai contoh, saya memiliki kelas yang memungkinkan Anda mengakses item dengan indeks, memeriksa apakah itu berisi item dan pada indeks apa, itu memiliki beberapa ukuran, Anda dapat menyalinnya ke sebuah array ... sepertinya pekerjaan IList<T>tetapi kelas saya memiliki ukuran tetap, dan tidak mendukung menambahkan atau menghapus, sehingga berfungsi lebih seperti sebuah array daripada daftar. Tapi tidak ada IArray<T>di BCL .

  • Antarmuka milik API yang porting ke beberapa platform, dan dalam implementasi platform tertentu beberapa bagian tidak didukung. Idealnya akan ada beberapa cara untuk mendeteksinya, sehingga kode portabel yang menggunakan API seperti itu dapat memutuskan apa pun untuk memanggil bagian-bagian itu ... tetapi jika Anda memanggilnya, itu benar-benar tepat untuk didapatkan NotSupportedException. Ini terutama benar, jika ini adalah port ke platform baru yang tidak diramalkan dalam desain aslinya.


Juga pertimbangkan mengapa itu tidak didukung?

Terkadang InvalidOperationExceptionmerupakan pilihan yang lebih baik. Sebagai contoh, satu lagi cara untuk menambahkan polimorfisme dalam suatu kelas adalah dengan memiliki berbagai implementasi antarmuka internal dan kode Anda memilih yang mana untuk instantiate tergantung pada parameter yang diberikan dalam konstruktor kelas. [Ini khususnya berguna jika Anda tahu bahwa set opsi sudah diperbaiki dan Anda tidak ingin mengizinkan kelas pihak ketiga diperkenalkan dengan injeksi dependensi.] Saya telah melakukan ini untuk mendukung ThreadLocal karena implementasi pelacakan dan non-pelacakan adalah appart terlalu jauh, dan apa yang ThreadLocal.Valuesmelempar pada implementasi non-pelacakan?InvalidOperationExceptionbahkan, itu tidak tergantung pada keadaan objek. Dalam hal ini saya memperkenalkan kelas sendiri, dan saya tahu bahwa metode ini harus diimplementasikan dengan hanya melempar pengecualian.

Terkadang nilai default masuk akal. Misalnya pada yang ICollection<T>.IsReadOnlydisebutkan di atas, masuk akal untuk mengembalikan 'benar' atau 'salah' tergantung kasusnya. Jadi ... apa arti semantiknya IFoo.Bar? mungkin ada beberapa nilai default yang masuk akal untuk kembali.


Tambahan: jika Anda mengendalikan antarmuka (dan Anda tidak perlu tetap menggunakannya untuk kompatibilitas) seharusnya tidak ada kasus di mana Anda harus melempar NotSupportedException. Meskipun, Anda mungkin harus membagi antarmuka menjadi dua atau lebih antarmuka yang lebih kecil agar sesuai untuk kasus Anda, yang dapat menyebabkan "polusi" dalam situasi ekstrem.

Theraot
sumber
0

Adakah orang lain yang pernah mengalami situasi serupa sebelumnya?

Ya, desainer perpustakaan .Net melakukannya. Kelas Kamus melakukan apa yang Anda lakukan. Menggunakan implementasi eksplisit untuk menyembunyikan * beberapa metode IDictionary secara efektif . Ini dijelaskan lebih baik di sini , tetapi untuk meringkas, untuk menggunakan Kamus's Add, CopyTo, atau Hapus metode yang mengambil KeyValuePairs, Anda harus terlebih dahulu melemparkan objek ke IDictionary.

* Ini bukan "menyembunyikan" metode dalam arti ketat dari kata "sembunyikan" seperti yang digunakan Microsoft . Tapi saya tidak tahu istilah yang lebih baik dalam hal ini.

pengguna2023861
sumber
... yang mengerikan.
Lightness Races with Monica
Mengapa downvote?
user2023861
2
Mungkin karena Anda tidak menjawab pertanyaan itu. Ish. Semacam.
Lightness Races with Monica
@LightnessRacesinOrbit, saya memperbarui jawaban saya untuk membuatnya lebih jelas. Saya harap ini membantu OP.
user2023861
2
Sepertinya itu menjawab pertanyaan kepada saya. Fakta bahwa itu mungkin ide yang baik atau buruk tampaknya tidak berada dalam ruang lingkup pertanyaan, tetapi mungkin masih berguna untuk mengindikasikan jawaban.
Ellesedil
-6

Anda selalu bisa menerapkan dan mengembalikan nilai jinak atau nilai default. Bagaimanapun itu hanya sebuah properti. Itu bisa mengembalikan 0 (properti default int), atau nilai apa pun yang masuk akal dalam implementasi Anda (int.MinValue, int.MaxValue, 1, 42, dll ...)

//We don't officially implement this property
int IFoo.Bar
{
     { get; }
}

Melempar pengecualian sepertinya bentuk yang buruk.

Jon Raynor
sumber
2
Mengapa? Bagaimana mengembalikan data yang salah lebih baik?
Lightness Races with Monica
1
Ini memecah LSP: lihat jawaban Jimmy Hoffa untuk penjelasan mengapa.
5
Jika tidak ada kemungkinan nilai pengembalian yang benar, lebih baik untuk melemparkan pengecualian daripada mengembalikan nilai yang salah. Apa pun yang Anda lakukan, program Anda akan mengalami kegagalan fungsi jika secara tidak sengaja memanggil properti ini. Tetapi jika Anda melemparkan pengecualian, maka akan jelas mengapa itu tidak berfungsi.
Tanner Swett
Saya tidak tahu bagaimana nilainya akan salah saat Anda yang mengimplementasikan antarmuka! Ini implementasi Anda, bisa apa pun yang Anda inginkan.
Jon Raynor
Bagaimana jika nilai yang benar tidak diketahui pada waktu kompilasi?
Andy