Apa yang dimaksud penulis dengan melemparkan referensi antarmuka ke implementasi apa pun?

17

Saat ini saya sedang dalam proses mencoba menguasai C #, jadi saya membaca Adaptive Code via C # oleh Gary McLean Hall .

Dia menulis tentang pola dan anti-pola. Di bagian implementasi versus antarmuka ia menulis yang berikut ini:

Pengembang yang baru dengan konsep pemrograman untuk antarmuka sering mengalami kesulitan melepaskan apa yang ada di balik antarmuka.

Pada waktu kompilasi, setiap klien dari suatu antarmuka harus tidak tahu implementasi dari antarmuka yang digunakannya. Pengetahuan tersebut dapat mengarah pada asumsi yang salah yang memasangkan klien ke implementasi antarmuka yang spesifik.

Bayangkan contoh umum di mana kelas perlu menyimpan catatan dalam penyimpanan persisten. Untuk melakukannya, itu benar mendelegasikan ke antarmuka, yang menyembunyikan rincian mekanisme penyimpanan persisten yang digunakan. Namun, tidak benar untuk membuat asumsi tentang implementasi antarmuka mana yang digunakan pada saat run time. Misalnya, casting referensi antarmuka untuk implementasi apa pun selalu merupakan ide yang buruk.

Mungkin kendala bahasa, atau kurangnya pengalaman saya, tapi saya tidak mengerti apa artinya itu. Inilah yang saya mengerti:

Saya punya proyek waktu luang yang menyenangkan untuk berlatih C #. Di sana saya punya kelas:

public class SomeClass...

Kelas ini digunakan di banyak tempat. Saat belajar C #, saya membaca bahwa lebih baik untuk abstrak dengan antarmuka, jadi saya membuat yang berikut ini

public interface ISomeClass <- Here I made a "contract" of all the public methods and properties SomeClass needs to have.

public class SomeClass : ISomeClass <- Same as before. All implementation here.

Jadi saya pergi ke semua referensi kelas dan menggantinya dengan ISomeClass.

Kecuali dalam konstruksi, di mana saya menulis:

ISomeClass myClass = new SomeClass();

Apakah saya mengerti dengan benar bahwa ini salah? Jika ya, mengapa demikian, dan apa yang harus saya lakukan?

Marshall
sumber
25
Tidak ada dalam contoh Anda adalah Anda melemparkan objek tipe antarmuka ke tipe implementasi. Anda menetapkan sesuatu dari tipe implementasi ke variabel antarmuka, yang sangat bagus dan benar.
Caleth
1
Apa maksud Anda "dalam konstruktor tempat saya menulis ISomeClass myClass = new SomeClass();? Jika Anda benar-benar bermaksud itu, itu adalah rekursi dalam konstruktor, mungkin bukan yang Anda inginkan. Mudah-mudahan maksud Anda dalam" konstruksi "yaitu alokasi, mungkin, tetapi tidak dalam konstruktor itu sendiri, kan ?
Erik Eidt
@Erik: Ya. Dalam konstruksi. Anda benar. Akan mengoreksi pertanyaan. Terima kasih
Marshall
Fakta menyenangkan: F # memiliki cerita yang lebih baik daripada C # dalam hal itu - itu menghilangkan implementasi antarmuka implisit, jadi setiap kali Anda ingin memanggil metode antarmuka, Anda perlu dikirim ke jenis antarmuka. Ini membuatnya sangat jelas kapan dan bagaimana Anda menggunakan antarmuka dalam kode Anda, dan membuat pemrograman ke antarmuka lebih tertanam dalam bahasa.
scrwtp
3
Ini agak di luar topik, tapi saya pikir penulis salah mendiagnosis masalah yang dimiliki orang-orang yang baru mengenal konsep ini. Menurut pendapat saya, masalahnya adalah bahwa orang yang baru mengenal konsep tersebut tidak tahu bagaimana membuat antarmuka yang baik. Sangat mudah untuk membuat antarmuka yang terlalu spesifik yang tidak benar-benar memberikan generalisasi (yang mungkin terjadi dengan ISomeClass), tetapi juga mudah untuk membuat antarmuka yang terlalu umum yang tidak mungkin untuk menulis kode yang berguna pada saat itu satu-satunya pilihan adalah untuk memikirkan kembali antarmuka dan menulis ulang kode atau downcast.
Derek Elkins meninggalkan SE

Jawaban:

37

Mengabstraksi kelas Anda ke dalam antarmuka adalah sesuatu yang harus Anda pertimbangkan jika dan hanya jika Anda berniat untuk menulis implementasi lain dari antarmuka tersebut atau kemungkinan kuat melakukannya di masa depan.

Jadi mungkin SomeClassdan ISomeClassmerupakan contoh yang buruk, karena itu akan seperti memiliki OracleObjectSerializerkelas dan IOracleObjectSerializerantarmuka.

Contoh yang lebih akurat adalah sesuatu seperti OracleObjectSerializerdan a IObjectSerializer. Satu-satunya tempat di program Anda di mana Anda peduli implementasi yang akan digunakan adalah ketika instance dibuat. Kadang-kadang ini dipisahkan lebih lanjut dengan menggunakan pola pabrik.

Di tempat lain dalam program Anda harus menggunakan IObjectSerializertidak peduli cara kerjanya. Mari kita mengira sekarang bahwa Anda juga memiliki SQLServerObjectSerializerimplementasi di samping OracleObjectSerializer. Sekarang anggaplah Anda perlu mengatur beberapa properti khusus untuk diatur dan metode itu hanya ada di OracleObjectSerializer dan bukan SQLServerObjectSerializer.

Ada dua cara untuk melakukannya: cara yang salah dan pendekatan prinsip substitusi Liskov .

Cara yang salah

Cara yang salah, dan contoh yang dirujuk dalam buku Anda, akan mengambil contoh IObjectSerializerdan melemparkannya ke OracleObjectSerializerdan kemudian memanggil metode yang setPropertytersedia hanya pada OracleObjectSerializer. Ini buruk karena meskipun Anda mungkin tahu sebuah contoh untuk menjadi OracleObjectSerializer, Anda memperkenalkan titik lain dalam program Anda di mana Anda ingin tahu apa implementasi itu. Ketika implementasi itu berubah, dan mungkin akan cepat atau lambat jika Anda memiliki beberapa implementasi, skenario kasus terbaik, Anda perlu menemukan semua tempat ini dan membuat penyesuaian yang benar. Skenario kasus terburuk, Anda melemparkan IObjectSerializercontoh ke OracleObjectSerializerdan Anda menerima kegagalan runtime dalam produksi.

Pendekatan Prinsip Pergantian Liskov

Liskov mengatakan bahwa Anda seharusnya tidak perlu metode seperti setPropertydi kelas implementasi seperti dalam kasus saya OracleObjectSerializerjika dilakukan dengan benar. Jika Anda kelas abstrak OracleObjectSerializeruntuk IObjectSerializer, Anda harus mencakup semua metode yang diperlukan untuk menggunakan kelas itu, dan jika Anda tidak bisa, maka ada sesuatu yang salah dengan abstraksi Anda (mencoba untuk membuat Dogpekerjaan kelas sebagai IPersonimplementasi misalnya).

Pendekatan yang benar adalah menyediakan setPropertymetode untuk IObjectSerializer. Metode serupa di SQLServerObjectSerializeridealnya akan bekerja melalui setPropertymetode ini . Lebih baik lagi, Anda membakukan nama properti melalui Enummana setiap implementasi menerjemahkan enum yang setara dengan terminologi basis datanya sendiri.

Sederhananya, menggunakan ISomeClasshanya setengah dari itu. Anda seharusnya tidak perlu membuangnya di luar metode yang bertanggung jawab atas pembuatannya. Melakukannya hampir pasti merupakan kesalahan desain yang serius.

Neil
sumber
1
Sepertinya saya bahwa, jika Anda akan dilemparkan IObjectSerializerke OracleObjectSerializerkarena Anda “tahu” bahwa ini adalah apa itu, maka Anda harus jujur dengan diri sendiri (dan yang lebih penting, dengan orang lain yang mungkin menjaga kode ini, yang mungkin termasuk masa depan Anda diri), dan gunakan OracleObjectSerializersepanjang jalan dari tempat itu dibuat ke tempat itu digunakan. Ini menjadikannya sangat umum dan jelas bahwa Anda memperkenalkan ketergantungan pada implementasi tertentu — dan pekerjaan serta keburukan yang terlibat dalam melakukan itu menjadi, dengan sendirinya, isyarat kuat bahwa ada sesuatu yang salah.
KRyan
(Dan, jika karena alasan tertentu Anda benar - benar harus bergantung pada implementasi tertentu, menjadi jauh lebih jelas bahwa ini adalah apa yang Anda lakukan dan bahwa Anda melakukannya dengan maksud dan tujuan. Ini "seharusnya" tidak pernah terjadi tentu saja, dan 99% dari waktu sepertinya itu terjadi sebenarnya tidak dan Anda harus memperbaiki hal-hal, tetapi tidak ada yang 100% pasti atau cara yang seharusnya terjadi.)
KRyan
@Ryan Benar-benar. Abstraksi seharusnya hanya digunakan jika Anda membutuhkannya. Menggunakan abstraksi ketika tidak diperlukan hanya akan membuat kode sedikit lebih sulit untuk dipahami.
Neil
29

Jawaban yang diterima benar dan sangat berguna, tetapi saya ingin membahas secara singkat baris kode yang Anda tanyakan:

ISomeClass myClass = new SomeClass();

Secara umum, ini tidak buruk. Hal yang harus dihindari sebisa mungkin adalah melakukan ini:

void someMethod(ISomeClass interface){
    SomeClass cast = (SomeClass)interface;
}

Di mana kode Anda disediakan antarmuka secara eksternal, tetapi secara internal melemparkannya ke implementasi tertentu, "Karena saya tahu itu hanya akan menjadi implementasi itu". Bahkan jika itu ternyata benar, dengan menggunakan antarmuka dan melakukan casting untuk implementasi, Anda secara sukarela menyerahkan keamanan tipe nyata hanya agar Anda dapat berpura-pura menggunakan abstraksi. Jika ada orang lain yang mengerjakan kode nanti dan melihat metode yang menerima parameter antarmuka, maka mereka akan menganggap bahwa setiap implementasi antarmuka itu merupakan opsi yang sah untuk dilewatkan. Bahkan mungkin Anda sendiri yang sedikit ketinggalan baris setelah Anda lupa bahwa metode tertentu terletak pada parameter apa yang dibutuhkan. Jika Anda pernah merasa perlu untuk melemparkan dari antarmuka ke implementasi tertentu, maka baik antarmuka, implementasi, atau kode yang merujuknya dirancang secara tidak benar dan harus diubah. Misalnya, jika metode ini hanya berfungsi ketika argumen yang dikirimkan adalah kelas tertentu, maka parameter hanya menerima kelas itu.

Sekarang, melihat kembali ke panggilan konstruktor Anda

ISomeClass myClass = new SomeClass();

masalah dari casting tidak benar-benar berlaku. Semua ini tampaknya tidak terpapar secara eksternal, jadi tidak ada risiko khusus yang terkait dengannya. Pada dasarnya, baris kode ini sendiri adalah detail implementasi yang antarmuka dirancang untuk abstrak untuk memulai, sehingga pengamat eksternal akan melihatnya bekerja dengan cara yang sama terlepas dari apa yang mereka lakukan. Namun, ini juga tidak MENGGUNAKAN apa pun dari keberadaan antarmuka. Anda myClassmemiliki tipe ISomeClass, tetapi tidak memiliki alasan apa pun karena selalu diberikan implementasi khusus,SomeClass. Ada beberapa potensi keuntungan kecil, seperti bisa menukar implementasi dalam kode dengan hanya mengubah panggilan konstruktor, atau menugaskan kembali variabel itu nanti untuk implementasi yang berbeda, tetapi kecuali ada tempat lain yang memerlukan variabel diketik ke antarmuka daripada implementasi pola ini membuat kode Anda terlihat seperti antarmuka hanya digunakan oleh hafalan, bukan karena pemahaman sebenarnya tentang manfaat antarmuka.

Kamil Drakari
sumber
1
Apache Math melakukan ini dengan Cartesian3D Source, baris 286 , yang bisa sangat menjengkelkan.
J_F_B_M
1
Ini adalah jawaban yang benar-benar benar yang menjawab pertanyaan awal.
Benjamin Gruenbaum
2

Saya pikir lebih mudah untuk menunjukkan kode Anda dengan contoh yang buruk:

public interface ISomeClass
{
    void DoThing();
}

public class SomeClass : ISomeClass
{
    public void DoThing()
    {
       // Mine for BitCoin
    }

}

public class AnotherClass : ISomeClass
{
    public void DoThing()
    {
        // Mine for oil
    }
    public Decimal Depth;
 }

 void main()
 {
     ISomeClass task = new SomeClass();

     task.DoThing(); //  This is good

     Console.WriteLine("Depth = {0}", ((AnotherClass)task).Depth); <-- The task object will not have this field
 }

Masalahnya adalah ketika Anda awalnya menulis kode, mungkin hanya ada satu implementasi dari antarmuka itu, sehingga casting akan tetap bekerja, hanya saja di masa depan, Anda dapat mengimplementasikan kelas lain dan kemudian (seperti contoh saya tunjukkan), Anda coba dan akses data yang tidak ada di objek yang Anda gunakan.

Neil
sumber
Kenapa halo, tuan. Adakah yang pernah mengatakan betapa tampannya dirimu?
Neil
2

Hanya untuk kejelasan, mari kita tentukan casting.

Casting secara paksa mengubah sesuatu dari satu jenis ke jenis lainnya. Contoh umum adalah casting nomor floating point ke tipe integer. Konversi tertentu dapat ditentukan saat casting tetapi defaultnya adalah dengan menafsirkan ulang bit.

Berikut adalah contoh casting dari halaman dokumen Microsoft ini .

// Create a new derived type.  
Giraffe g = new Giraffe();  

// Implicit conversion to base type is safe.  
Animal a = g;  

// Explicit conversion is required to cast back  
// to derived type. Note: This will compile but will  
// throw an exception at run time if the right-side  
// object is not in fact a Giraffe.  
Giraffe g2 = (Giraffe) a;  

Anda bisa melakukan hal yang sama dan melemparkan sesuatu yang mengimplementasikan antarmuka ke implementasi tertentu dari antarmuka itu, tetapi Anda seharusnya tidak melakukannya karena itu akan menghasilkan kesalahan atau perilaku tak terduga jika implementasi yang berbeda dari yang Anda harapkan digunakan.

Ryan1729
sumber
1
"Casting adalah mengubah sesuatu dari satu tipe ke tipe lainnya." - Tidak. Casting secara eksplisit mengubah sesuatu dari satu tipe ke tipe lainnya. (Secara khusus, "cast" adalah nama untuk sintaks yang digunakan untuk menentukan konversi itu.) Konversi implisit tidak dilemparkan. "Konversi spesifik dapat ditentukan saat casting tetapi defaultnya adalah dengan menafsirkan ulang bit." -- Tentu tidak. Ada banyak konversi, baik implisit maupun eksplisit, yang melibatkan perubahan signifikan pada pola bit.
hvd
@ hvd Saya sudah melakukan koreksi sekarang tentang kejujuran casting. Ketika saya mengatakan defaultnya adalah dengan menafsirkan kembali bit, saya mencoba untuk menyatakan bahwa jika Anda membuat tipe Anda sendiri, maka dalam kasus-kasus di mana para pemain secara otomatis ditentukan, ketika Anda melemparkannya ke jenis lain, bit-bit tersebut akan ditafsirkan kembali. . Dalam Animal/ Giraffecontoh di atas, jika Anda melakukan Animal a = (Animal)g;bit akan ditafsirkan kembali, (setiap data spesifik Giraffe akan ditafsirkan sebagai "bukan bagian dari objek ini").
Ryan1729
Terlepas dari apa yang dikatakan hvd, orang sangat sering menggunakan istilah "pemeran" dalam referensi untuk konversi tersirat; lihat misalnya https://www.google.com/search?q="implicit+cast"&tbm=bks . Secara teknis saya pikir lebih tepat untuk mencadangkan istilah "pemeran" untuk konversi eksplisit, selama Anda tidak bingung ketika orang lain menggunakannya secara berbeda.
ruakh
0

5 sen saya:

Semua contoh itu OK, tetapi itu bukan contoh dunia nyata dan tidak menunjukkan niat dunia nyata.

Saya tidak tahu C # jadi saya akan memberikan contoh abstrak (campuran antara Java dan C ++). Semoga tidak apa-apa.

Misalkan Anda memiliki antarmuka iList:

interface iList<Key,Value>{
   bool add(Key k, Value v);
   bool remove(Element e);
   Value get(Key k);
}

Sekarang anggaplah ada banyak implementasi:

  • DynamicArrayList - menggunakan array datar, cepat untuk menyisipkan dan menghapus di akhir.
  • LinkedList - menggunakan daftar ditautkan ganda, cepat untuk menyisipkan di depan dan akhir.
  • AVLTreeList - menggunakan AVL Tree, cepat untuk melakukan segalanya, tetapi menggunakan banyak memori
  • SkipList - menggunakan SkipList, cepat untuk melakukan segalanya, lebih lambat dari AVL Tree, tetapi menggunakan lebih sedikit memori.
  • HashList - menggunakan HashTable

Orang dapat memikirkan banyak implementasi yang berbeda.

Sekarang anggaplah kita memiliki kode berikut:

uint begin_size = 1000;
iList list = new DynamicArrayList(begin_size);

Itu jelas menunjukkan niat kami yang ingin kami gunakan iList. Tentu kami tidak lagi dapat melakukan DynamicArrayListoperasi tertentu, tetapi kami membutuhkan iList.

Pertimbangkan kode berikut:

iList list = factory.getList();

Sekarang kita bahkan tidak tahu apa implementasinya. Contoh terakhir ini sering digunakan dalam pemrosesan gambar ketika Anda memuat beberapa file dari disk dan Anda tidak memerlukan jenis file (gif, jpeg, png, bmp ...), tetapi yang Anda inginkan hanyalah melakukan manipulasi gambar (flip, skala, simpan sebagai png di akhir).

Nick
sumber
0

Anda memiliki antarmuka ISomeClass, dan objek myObject yang Anda tidak tahu apa-apa dari kode Anda kecuali bahwa itu dinyatakan untuk mengimplementasikan ISomeClass.

Anda memiliki SomeClass kelas, yang Anda ketahui mengimplementasikan antarmuka ISomeClass. Anda tahu karena itu dinyatakan untuk mengimplementasikan ISomeClass, atau Anda menerapkannya sendiri untuk mengimplementasikan ISomeClass.

Apa yang salah dengan casting myClass ke SomeClass? Dua hal salah. Satu, Anda tidak benar-benar tahu bahwa myClass adalah sesuatu yang dapat dikonversi ke SomeClass (turunan SomeClass atau subkelas SomeClass), sehingga para pemainnya bisa salah. Dua, Anda tidak harus melakukan ini. Anda harus bekerja dengan myClass yang dideklarasikan sebagai iSomeClass, dan menggunakan metode ISomeClass.

Titik di mana Anda mendapatkan objek SomeClass adalah ketika metode antarmuka dipanggil. Pada titik tertentu Anda memanggil myClass.myMethod (), yang dideklarasikan di antarmuka, tetapi memiliki implementasi di SomeClass, dan tentu saja mungkin di banyak kelas lain yang menerapkan ISomeClass. Jika panggilan berakhir di kode SomeClass.myMethod Anda, maka Anda tahu bahwa diri adalah turunan dari SomeClass, dan pada saat itu benar-benar baik dan benar-benar benar untuk menggunakannya sebagai objek SomeClass. Tentu saja jika itu adalah instance dari OtherClass dan bukan SomeClass, maka Anda tidak akan sampai pada kode SomeClass.

gnasher729
sumber