Mari kita asumsikan saya memiliki dua kelas yang terlihat seperti ini (blok kode pertama dan masalah umum terkait dengan C #):
class A
{
public int IntProperty { get; set; }
}
class B
{
public int IntProperty { get; set; }
}
Kelas-kelas ini tidak dapat diubah dengan cara apa pun (mereka adalah bagian dari majelis pihak ke-3). Oleh karena itu, saya tidak dapat membuat mereka mengimplementasikan antarmuka yang sama, atau mewarisi kelas yang sama yang kemudian akan berisi IntProperty.
Saya ingin menerapkan beberapa logika pada IntProperty
properti kedua kelas, dan di C ++ saya bisa menggunakan kelas template untuk melakukannya dengan mudah:
template <class T>
class LogicToBeApplied
{
public:
void T CreateElement();
};
template <class T>
T LogicToBeApplied<T>::CreateElement()
{
T retVal;
retVal.IntProperty = 50;
return retVal;
}
Dan kemudian saya bisa melakukan sesuatu seperti ini:
LogicToBeApplied<ClassA> classALogic;
LogicToBeApplied<ClassB> classBLogic;
ClassA classAElement = classALogic.CreateElement();
ClassB classBElement = classBLogic.CreateElement();
Dengan begitu saya bisa membuat satu kelas pabrik generik yang dapat digunakan untuk ClassA dan ClassB.
Namun, dalam C #, saya harus menulis dua kelas dengan dua where
klausa yang berbeda walaupun kode untuk logika persis sama:
public class LogicAToBeApplied<T> where T : ClassA, new()
{
public T CreateElement()
{
T retVal = new T();
retVal.IntProperty = 50;
return retVal;
}
}
public class LogicBToBeApplied<T> where T : ClassB, new()
{
public T CreateElement()
{
T retVal = new T();
retVal.IntProperty = 50;
return retVal;
}
}
Saya tahu bahwa jika saya ingin memiliki kelas yang berbeda dalam where
klausa, mereka harus terkait, yaitu mewarisi kelas yang sama, jika saya ingin menerapkan kode yang sama kepada mereka dalam pengertian yang saya jelaskan di atas. Hanya saja sangat menyebalkan memiliki dua metode yang benar-benar identik. Saya juga tidak ingin menggunakan refleksi karena masalah kinerja.
Adakah yang bisa menyarankan beberapa pendekatan di mana ini dapat ditulis dengan cara yang lebih elegan?
Jawaban:
Tambahkan antarmuka proxy (kadang-kadang disebut adaptor , kadang-kadang dengan perbedaan kecil), terapkan
LogicToBeApplied
dalam hal proxy, lalu tambahkan cara untuk membuat instance proxy ini dari dua lambdas: satu untuk properti dapatkan dan satu untuk set.Sekarang, setiap kali Anda harus lulus dalam IProxy tetapi memiliki turunan dari kelas pihak ketiga, Anda bisa memasukkan beberapa lambda:
Selain itu, Anda dapat menulis bantuan sederhana untuk membuat instance LamdaProxy dari instance A atau B. Mereka bahkan dapat menjadi metode ekstensi untuk memberi Anda gaya "fasih":
Dan sekarang konstruksi proxy terlihat seperti ini:
Sedangkan untuk pabrik Anda, saya akan melihat apakah Anda dapat mengubahnya menjadi metode pabrik "utama" yang menerima IProxy dan melakukan semua logika di dalamnya dan metode lain yang baru saja melewati
new A().Proxied()
ataunew B().Proxied()
:Tidak ada cara untuk melakukan hal yang sama dengan kode C ++ Anda di C # karena templat C ++ bergantung pada pengetikan struktural . Selama dua kelas memiliki nama metode dan tanda tangan yang sama, di C ++ Anda dapat memanggil metode itu secara umum pada keduanya. C # memiliki pengetikan nominal - nama kelas atau antarmuka adalah bagian dari tipenya. Oleh karena itu, kelas-kelas
A
danB
tidak dapat diperlakukan sama dalam kapasitas apa pun kecuali hubungan eksplisit "adalah" didefinisikan melalui warisan atau implementasi antarmuka.Jika boilerplate menerapkan metode ini per kelas terlalu banyak, Anda dapat menulis fungsi yang mengambil objek dan secara reflektif membangun
LambdaProxy
dengan mencari nama properti tertentu:Ini gagal parah ketika diberikan objek dengan tipe yang salah; refleksi secara inheren memperkenalkan kemungkinan kegagalan sistem tipe C # tidak dapat mencegah. Untungnya Anda dapat menghindari refleksi sampai beban pemeliharaan para pembantu menjadi terlalu besar karena Anda tidak diharuskan untuk memodifikasi antarmuka IProxy atau implementasi LambdaProxy untuk menambahkan gula reflektif.
Sebagian alasan mengapa ini berhasil
LambdaProxy
adalah "generik secara maksimal"; itu dapat mengadaptasi nilai apa pun yang mengimplementasikan "semangat" dari kontrak IProxy karena implementasi LambdaProxy sepenuhnya ditentukan oleh fungsi pengambil dan penyetel yang diberikan. Ini bahkan berfungsi jika kelas memiliki nama yang berbeda untuk properti, atau tipe berbeda yang secara masuk akal dan aman direpresentasikan sebagaiint
, atau jika ada beberapa cara untuk memetakan konsep yangProperty
seharusnya mewakili fitur lain dari kelas. Tipuan yang diberikan oleh fungsi memberi Anda fleksibilitas maksimal.sumber
ReflectiveProxier
, bisakah Anda membangun proxy menggunakandynamic
kata kunci? Menurut saya Anda akan memiliki masalah mendasar yang sama (yaitu kesalahan yang hanya tertangkap saat runtime), tetapi sintaks dan rawatan akan jauh lebih sederhana.Berikut adalah garis besar cara menggunakan adaptor tanpa mewarisi dari A dan / atau B, dengan kemungkinan menggunakannya untuk objek A dan B yang ada:
Saya biasanya lebih suka adaptor objek semacam ini daripada proxy kelas, mereka menghindari masalah buruk yang dapat Anda hadapi dengan warisan. Sebagai contoh, solusi ini akan bekerja bahkan jika A dan B adalah kelas yang disegel.
sumber
new int Property
? Anda tidak membayangi apa pun.Anda dapat beradaptasi
ClassA
danClassB
melalui antarmuka umum. Dengan cara ini kode AndaLogicAToBeApplied
tetap sama. Tidak jauh berbeda dari apa yang Anda miliki.sumber
A
,B
tipe ke antarmuka umum. Keuntungan besar adalah kita tidak perlu menduplikasi logika umum. Kerugiannya adalah bahwa logika sekarang instantiates wrapper / proxy bukan jenis yang sebenarnya.LogicToBeApplied
memiliki kompleksitas tertentu dan tidak boleh diulangi di dua tempat dalam basis kode dalam keadaan apa pun. Maka kode boilerplate tambahan sering diabaikan.Versi C ++ hanya berfungsi karena templatnya menggunakan "static duck typing" - apa pun yang dikompilasi selama jenisnya memberikan nama yang benar. Ini lebih seperti sistem makro. Sistem generik C # dan bahasa lain bekerja sangat berbeda.
Jawaban devnull dan Doc Brown menunjukkan bagaimana pola adaptor dapat digunakan untuk menjaga algoritma Anda tetap umum, dan masih beroperasi pada tipe arbitrer ... dengan beberapa batasan. Khususnya, Anda sekarang membuat jenis yang berbeda dari yang Anda inginkan.
Dengan sedikit tipu daya, dimungkinkan untuk menggunakan tipe yang tepat tanpa perubahan apa pun. Namun, sekarang kita perlu mengekstraksi semua interaksi dengan tipe target ke antarmuka yang terpisah. Di sini, interaksi ini adalah penugasan konstruksi dan properti:
Dalam interpretasi OOP, ini akan menjadi contoh dari pola strategi , meskipun dicampur dengan obat generik.
Kami kemudian dapat menulis ulang logika Anda untuk menggunakan interaksi ini:
Definisi interaksi akan terlihat seperti:
Kerugian besar dari pendekatan ini adalah bahwa programmer perlu menulis dan melewatkan contoh interaksi ketika memanggil logika. Ini cukup mirip dengan solusi berbasis pola-adaptor, tetapi sedikit lebih umum.
Dalam pengalaman saya, ini adalah yang terdekat dengan fungsi templat yang Anda dapat gunakan dalam bahasa lain. Teknik serupa digunakan di Haskell, Scala, Go, dan Rust untuk mengimplementasikan antarmuka di luar definisi tipe. Namun, dalam bahasa ini kompiler masuk dan memilih contoh interaksi yang benar secara implisit sehingga Anda tidak benar-benar melihat argumen tambahan. Ini juga mirip dengan metode ekstensi C #, tetapi tidak terbatas pada metode statis.
sumber
Jika Anda benar-benar ingin berhati-hati terhadap angin, Anda dapat menggunakan "dinamis" untuk membuat kompilator menangani semua kekotoran refleksi untuk Anda. Ini akan menghasilkan kesalahan runtime jika Anda melewatkan objek ke SetSomeProperty yang tidak memiliki properti bernama SomeProperty.
sumber
Jawaban lain mengidentifikasi masalah dengan benar dan memberikan solusi yang bisa diterapkan. C # tidak (umumnya) mendukung "mengetik bebek" ("Jika berjalan seperti bebek ..."), jadi tidak ada cara untuk memaksa Anda
ClassA
danClassB
dipertukarkan jika mereka tidak dirancang seperti itu.Namun, jika Anda sudah bersedia menerima risiko kesalahan runtime, maka ada jawaban yang lebih mudah daripada menggunakan Refleksi.
C # memiliki
dynamic
kata kunci yang sempurna untuk situasi seperti ini. Ia memberitahu kompiler "Saya tidak akan tahu apa jenis ini sampai runtime (dan mungkin bahkan tidak), jadi izinkan saya untuk melakukan apa saja untuk itu".Dengan itu, Anda dapat membangun fungsi yang Anda inginkan:
Perhatikan penggunaan
static
kata kunci juga. Itu memungkinkan Anda menggunakan ini sebagai:Tidak ada implikasi kinerja gambar besar menggunakan
dynamic
, cara ada hit satu kali (dan menambah kompleksitas) menggunakan Refleksi. Pertama kali kode Anda mengenai panggilan dinamis dengan jenis tertentu akan memiliki sedikit overhead , tetapi panggilan berulang akan sama cepatnya dengan kode standar. Namun, Anda akan mendapatkanRuntimeBinderException
jika Anda mencoba mengirimkan sesuatu yang tidak memiliki properti itu, dan tidak ada cara yang baik untuk memeriksanya sebelumnya. Anda mungkin ingin secara khusus menangani kesalahan itu dengan cara yang bermanfaat.sumber
Anda dapat menggunakan refleksi untuk mengeluarkan properti dengan nama.
Jelas Anda berisiko kesalahan runtime dengan metode ini. Itulah yang C # berusaha menghentikan Anda lakukan.
Saya memang membaca di suatu tempat bahwa versi C # yang akan datang memungkinkan Anda untuk melewatkan objek sebagai antarmuka yang tidak mereka warisi tetapi cocok. Yang juga akan menyelesaikan masalah Anda.
(Saya akan mencoba dan menggali artikel)
Metode lain, meskipun saya tidak yakin ini akan menyelamatkan Anda kode apa pun, akan menjadi subkelas A dan B dan juga mewarisi Antarmuka dengan IntProperty.
sumber
Saya hanya ingin menggunakan
implicit operator
konversi bersama dengan pendekatan delegasi / lambda dari jawaban Jack.A
danB
seperti yang diasumsikan:Maka mudah untuk mendapatkan sintaks yang bagus dengan konversi yang ditentukan pengguna secara implisit (tidak perlu metode ekstensi atau yang serupa):
Ilustrasi penggunaan:
The
Initialize
Metode menunjukkan bagaimana Anda dapat bekerja denganAdapter
tanpa peduli apakah itu adalahA
atauB
atau sesuatu yang lain. Invokasi dariInitialize
metode ini menunjukkan bahwa kita tidak memerlukan gips (yang terlihat).AsProxy()
atau sejenisnya untuk mengolah betonA
atauB
sebagaiAdapter
.Pertimbangkan jika Anda ingin melempar
ArgumentNullException
konversi yang ditentukan pengguna jika argumen yang diteruskan adalah referensi nol, atau tidak.sumber