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?
sumber
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 ?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.Jawaban:
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
SomeClass
danISomeClass
merupakan contoh yang buruk, karena itu akan seperti memilikiOracleObjectSerializer
kelas danIOracleObjectSerializer
antarmuka.Contoh yang lebih akurat adalah sesuatu seperti
OracleObjectSerializer
dan aIObjectSerializer
. 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
IObjectSerializer
tidak peduli cara kerjanya. Mari kita mengira sekarang bahwa Anda juga memilikiSQLServerObjectSerializer
implementasi di sampingOracleObjectSerializer
. 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
IObjectSerializer
dan melemparkannya keOracleObjectSerializer
dan kemudian memanggil metode yangsetProperty
tersedia hanya padaOracleObjectSerializer
. Ini buruk karena meskipun Anda mungkin tahu sebuah contoh untuk menjadiOracleObjectSerializer
, 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 melemparkanIObjectSerializer
contoh keOracleObjectSerializer
dan Anda menerima kegagalan runtime dalam produksi.Pendekatan Prinsip Pergantian Liskov
Liskov mengatakan bahwa Anda seharusnya tidak perlu metode seperti
setProperty
di kelas implementasi seperti dalam kasus sayaOracleObjectSerializer
jika dilakukan dengan benar. Jika Anda kelas abstrakOracleObjectSerializer
untukIObjectSerializer
, 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 membuatDog
pekerjaan kelas sebagaiIPerson
implementasi misalnya).Pendekatan yang benar adalah menyediakan
setProperty
metode untukIObjectSerializer
. Metode serupa diSQLServerObjectSerializer
idealnya akan bekerja melaluisetProperty
metode ini . Lebih baik lagi, Anda membakukan nama properti melaluiEnum
mana setiap implementasi menerjemahkan enum yang setara dengan terminologi basis datanya sendiri.Sederhananya, menggunakan
ISomeClass
hanya setengah dari itu. Anda seharusnya tidak perlu membuangnya di luar metode yang bertanggung jawab atas pembuatannya. Melakukannya hampir pasti merupakan kesalahan desain yang serius.sumber
IObjectSerializer
keOracleObjectSerializer
karena 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 gunakanOracleObjectSerializer
sepanjang 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.Jawaban yang diterima benar dan sangat berguna, tetapi saya ingin membahas secara singkat baris kode yang Anda tanyakan:
Secara umum, ini tidak buruk. Hal yang harus dihindari sebisa mungkin adalah melakukan ini:
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
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
myClass
memiliki tipeISomeClass
, 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.sumber
Saya pikir lebih mudah untuk menunjukkan kode Anda dengan contoh yang buruk:
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.
sumber
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 .
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.
sumber
Animal
/Giraffe
contoh di atas, jika Anda melakukanAnimal a = (Animal)g;
bit akan ditafsirkan kembali, (setiap data spesifik Giraffe akan ditafsirkan sebagai "bukan bagian dari objek ini").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
:Sekarang anggaplah ada banyak implementasi:
Orang dapat memikirkan banyak implementasi yang berbeda.
Sekarang anggaplah kita memiliki kode berikut:
Itu jelas menunjukkan niat kami yang ingin kami gunakan
iList
. Tentu kami tidak lagi dapat melakukanDynamicArrayList
operasi tertentu, tetapi kami membutuhkaniList
.Pertimbangkan kode berikut:
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).
sumber
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.
sumber