Kapan tipe pengujian OK?

53

Dengan asumsi sebuah bahasa dengan keamanan tipe bawaan (mis., Bukan JavaScript):

Diberikan metode yang menerima a SuperType, kita tahu bahwa dalam kebanyakan kasus di mana kita mungkin tergoda untuk melakukan pengujian tipe untuk mengambil tindakan:

public void DoSomethingTo(SuperType o) {
  if (o isa SubTypeA) {
    o.doSomethingA()
  } else {
    o.doSomethingB();
  }
}

Kita biasanya, jika tidak selalu, membuat metode tunggal yang dapat ditimpa pada SuperTypedan melakukan ini:

public void DoSomethingTo(SuperType o) {
  o.doSomething();
}

... dimana setiap subtipe diberikan doSomething()implementasinya sendiri . Sisa dari aplikasi kita kemudian dapat dengan tepat tidak mengetahui apakah yang diberikan SuperTypebenar-benar a SubTypeAatau a SubTypeB.

Hebat.

Tapi, kami masih diberi is aoperasi mirip-di sebagian besar, jika tidak semua, jenis bahasa yang aman. Dan itu menunjukkan potensi kebutuhan untuk pengujian tipe eksplisit.

Jadi, dalam situasi apa, jika ada, harus kita atau harus kita melakukan pengujian jenis eksplisit?

Maafkan ketidakhadiran pikiran saya atau kurangnya kreativitas. Saya tahu saya pernah melakukannya sebelumnya; tapi, sejujurnya sudah lama sekali aku tidak bisa mengingat apakah yang kulakukan itu bagus! Dan dalam memori baru-baru ini, saya rasa saya tidak perlu menemukan jenis untuk menguji di luar JavaScript koboi saya.

svidgen
sumber
1
Baca tentang inferensi tipe dan sistem tipe
Basile Starynkevitch
4
Perlu ditunjukkan bahwa baik Java maupun C # tidak memiliki generik dalam versi pertama mereka. Anda harus melakukan cast ke dan dari Object untuk menggunakan kontainer.
Doval
6
"Pengecekan tipe" hampir selalu mengacu pada memeriksa apakah kode mematuhi aturan pengetikan statis bahasa. Apa yang Anda maksud biasanya disebut pengujian tipe .
3
Inilah sebabnya saya suka menulis C ++ dan membiarkan RTTI dimatikan. Ketika seseorang benar-benar tidak dapat menguji jenis objek saat runtime, itu memaksa pengembang untuk mematuhi desain OO yang baik sehubungan dengan pertanyaan yang diajukan di sini.

Jawaban:

48

"Tidak pernah" adalah jawaban kanonik untuk "kapan pengujian tipe baik-baik saja?" Tidak ada cara untuk membuktikan atau membantah ini; itu adalah bagian dari sistem kepercayaan tentang apa yang membuat "desain yang baik" atau "desain berorientasi objek yang baik." Itu juga hokum.

Yang pasti, jika Anda memiliki satu set kelas terintegrasi dan juga lebih dari satu atau dua fungsi yang memerlukan pengujian jenis langsung, Anda mungkin MELAKUKAN SALAH. Yang benar-benar Anda butuhkan adalah metode yang diimplementasikan secara berbeda di dalam SuperTypedan subtipe-nya. Ini adalah bagian tak terpisahkan dari pemrograman berorientasi objek, dan seluruh alasan kelas dan warisan ada.

Dalam hal ini, pengujian tipe secara eksplisit salah bukan karena pengujian tipe secara inheren salah, tetapi karena bahasa tersebut sudah memiliki cara yang bersih, dapat diperluas, idiomatis untuk menyelesaikan diskriminasi tipe, dan Anda tidak menggunakannya. Alih-alih, Anda kembali pada idiom primitif, rapuh, dan tidak dapat diperluas.

Solusi: Gunakan idiom. Seperti yang Anda sarankan, tambahkan metode ke masing-masing kelas, lalu biarkan algoritma pewarisan standar dan pemilihan metode menentukan kasus mana yang berlaku. Atau jika Anda tidak dapat mengubah jenis dasar, subkelas dan tambahkan metode Anda di sana.

Begitu banyak untuk kebijaksanaan konvensional, dan untuk beberapa jawaban. Beberapa kasus di mana pengujian jenis eksplisit masuk akal:

  1. Ini satu kali. Jika Anda memiliki banyak jenis diskriminasi untuk dilakukan, Anda dapat memperluas jenis, atau subkelas. Tapi kamu tidak. Anda hanya memiliki satu atau dua tempat di mana Anda perlu pengujian eksplisit, jadi tidak ada gunanya saat Anda kembali dan bekerja melalui hierarki kelas untuk menambahkan fungsi sebagai metode. Atau tidak sepadan dengan upaya praktis untuk menambahkan jenis generalisasi, pengujian, ulasan desain, dokumentasi, atau atribut lain dari kelas dasar untuk penggunaan yang sederhana dan terbatas. Dalam hal itu, menambahkan fungsi yang melakukan pengujian langsung adalah rasional.

  2. Anda tidak dapat menyesuaikan kelas. Anda berpikir tentang subklasifikasi - tetapi Anda tidak bisa. Banyak kelas di Jawa, misalnya, ditunjuk final. Anda mencoba memasukkan a public class ExtendedSubTypeA extends SubTypeA {...} dan kompiler memberi tahu Anda, tanpa syarat yang tidak pasti, bahwa apa yang Anda lakukan tidak mungkin. Maaf, rahmat dan kecanggihan model berorientasi objek! Seseorang memutuskan Anda tidak dapat memperpanjang tipenya! Sayangnya, banyak perpustakaan standar final, dan membuat kelas finaladalah panduan desain umum. Fungsi end-run adalah yang tersisa untuk Anda.

    BTW, ini tidak terbatas pada bahasa yang diketik secara statis. Bahasa dinamis Python memiliki sejumlah kelas dasar yang, di bawah penutup yang diimplementasikan dalam C, tidak dapat benar-benar dimodifikasi. Seperti Java, itu termasuk sebagian besar tipe standar.

  3. Kode Anda eksternal. Anda berkembang dengan kelas dan objek yang berasal dari berbagai server basis data, mesin middleware, dan basis kode lain yang tidak dapat Anda kontrol atau sesuaikan. Kode Anda hanya konsumen rendah dari objek yang dihasilkan di tempat lain. Bahkan jika Anda bisa membuat subkelas SuperType, Anda tidak akan bisa mendapatkan pustaka yang Anda gunakan untuk menghasilkan objek di dalam subkelas Anda. Mereka akan memberi Anda contoh jenis yang mereka tahu, bukan varian Anda. Ini tidak selalu terjadi ... kadang-kadang mereka dibangun untuk fleksibilitas, dan mereka secara dinamis membuat instance kelas yang Anda beri mereka makan. Atau mereka menyediakan mekanisme untuk mendaftarkan subclass yang Anda ingin pabrik mereka bangun. Parser XML tampaknya sangat baik dalam menyediakan titik masuk seperti itu; lihat misalnya atau lxml dengan Python . Tetapi sebagian besar basis kode tidak menyediakan ekstensi tersebut. Mereka akan mengembalikan kelas yang mereka bangun dan ketahui. Biasanya tidak masuk akal untuk mem-proksi hasil mereka ke dalam hasil khusus Anda hanya agar Anda dapat menggunakan pemilih jenis berorientasi objek murni. Jika Anda akan melakukan diskriminasi jenis, Anda harus melakukannya dengan relatif kasar. Kode pengujian tipe Anda kemudian terlihat cukup sesuai.

  4. Obat-obatan generik / pengiriman banyak orang miskin. Anda ingin menerima berbagai jenis berbeda untuk kode Anda, dan merasa bahwa memiliki array metode yang sangat spesifik jenis tidak anggun. public void add(Object x)tampaknya logis, tapi tidak array addByte, addShort, addInt, addLong, addFloat, addDouble, addBoolean, addChar, dan addStringvarian (untuk beberapa nama). Memiliki fungsi atau metode yang menggunakan tipe super tinggi dan kemudian menentukan apa yang harus dilakukan berdasarkan tipe per tipe - mereka tidak akan memenangkan Anda Penghargaan Kemurnian pada Simposium Desain Booch-Liskov tahunan, tetapi menjatuhkan Penamaan Hongaria akan memberi Anda API yang lebih sederhana. Dalam arti tertentu, Anda is-aatauis-instance-of pengujian mensimulasikan generik atau multi-pengiriman dalam konteks bahasa yang tidak mendukungnya secara asli.

    Dukungan bahasa bawaan untuk generik dan pengetikan bebek mengurangi kebutuhan untuk pengecekan jenis dengan membuat "melakukan sesuatu yang anggun dan sesuai" menjadi lebih mungkin. Pilihan banyak pengiriman / antarmuka yang terlihat dalam bahasa seperti Julia dan Go juga menggantikan pengujian jenis langsung dengan mekanisme bawaan untuk pemilihan "apa yang harus dilakukan" berbasis jenis. Tetapi tidak semua bahasa mendukung ini. Java misalnya umumnya pengiriman tunggal, dan idiomnya tidak super-friendly untuk mengetik bebek.

    Tetapi bahkan dengan semua fitur diskriminasi jenis ini - pewarisan, generik, pengetikan bebek, dan pengiriman banyak - kadang-kadang hanya nyaman untuk memiliki satu rutin, konsolidasi yang membuat fakta bahwa Anda melakukan sesuatu berdasarkan jenis objek jelas dan langsung. Dalam metaprogramming, saya merasa hal itu pada dasarnya tidak dapat dihindari. Apakah kembali ke tipe langsung bertanya merupakan "pragmatisme dalam aksi" atau "coding kotor" akan tergantung pada filosofi dan keyakinan desain Anda.

Jonathan Eunice
sumber
Jika suatu operasi tidak dapat dilakukan pada jenis tertentu, tetapi dapat dilakukan pada dua jenis yang berbeda yang secara implisit dapat dikonversi, kelebihan beban hanya sesuai jika kedua konversi akan menghasilkan hasil yang identik. Kalau tidak, seseorang berakhir dengan perilaku payah seperti di .NET: (1.0).Equals(1.0f)menghasilkan true [argumen dipromosikan ke double], tetapi (1.0f).Equals(1.0)menghasilkan [argumen dipromosikan ke object] salah; di Jawa, Math.round(123456789*1.0)menghasilkan 123456789, tetapi Math.round(123456789*1.0)menghasilkan 123456792 [argumen mempromosikan floatbukan double].
supercat
Ini adalah argumen klasik terhadap tipe casting otomatis / eskalasi. Hasil yang buruk dan paradoks, setidaknya dalam kasus tepi. Saya setuju, tetapi tidak yakin bagaimana Anda bermaksud menghubungkannya dengan jawaban saya.
Jonathan Eunice
Saya merespons poin Anda # 4 yang tampaknya menganjurkan kelebihan beban alih-alih menggunakan metode dengan nama berbeda dengan tipe berbeda.
supercat
2
@supercat Menyalahkan penglihatanku yang buruk, tetapi dua ekspresi dengan Math.roundterlihat identik dengan saya. Apa bedanya?
Lily Chung
2
@IstvanChung: Ups ... yang terakhir seharusnya Math.round(123456789)[menunjukkan apa yang mungkin terjadi jika seseorang menulis ulang Math.round(thing.getPosition() * COUNTS_PER_MIL)untuk mengembalikan nilai posisi yang tidak dinaikkan, tidak menyadari bahwa getPositionmengembalikan suatu intatau long.]
supercat
25

Situasi utama yang pernah saya butuhkan adalah ketika membandingkan dua objek, seperti dalam suatu equals(other)metode, yang mungkin memerlukan algoritma yang berbeda tergantung pada jenis persisnya other. Meski begitu, itu cukup langka.

Situasi lain yang saya alami muncul, sekali lagi sangat jarang, adalah setelah deserialization atau parsing, di mana Anda kadang-kadang membutuhkannya untuk dengan aman dilemparkan ke jenis yang lebih spesifik.

Juga, terkadang Anda hanya perlu meretas untuk memperbaiki kode pihak ketiga yang tidak Anda kontrol. Ini adalah salah satu hal yang tidak benar-benar ingin Anda gunakan secara teratur, tetapi senang ada di sana ketika Anda benar-benar membutuhkannya.

Karl Bielefeldt
sumber
1
Untuk sesaat saya senang dengan kasus deserialisasi, yang salah ingat menggunakannya di sana; tapi, sekarang aku tidak bisa membayangkan bagaimana aku bisa! Saya tahu saya telah melakukan beberapa jenis pencarian aneh untuk itu; tapi saya tidak yakin apakah itu merupakan pengujian tipe . Mungkin serialisasi adalah paralel yang lebih dekat: harus menginterogasi objek untuk tipe konkret mereka.
svidgen
1
Serialisasi dapat dilakukan menggunakan polimorfisme. Ketika deserializing, Anda sering melakukan sesuatu seperti BaseClass base = deserialize(input), karena Anda belum tahu jenisnya, maka Anda lakukan if (base instanceof Derived) derived = (Derived)baseuntuk menyimpannya sebagai jenis turunan yang tepat.
Karl Bielefeldt
1
Kesetaraan masuk akal. Dalam pengalaman saya, metode seperti itu sering memiliki bentuk seperti “jika dua objek ini memiliki tipe beton yang sama, kembalikan apakah semua bidangnya sama; jika tidak, kembalikan false (atau tidak dapat dibandingkan) ”.
Jon Purdy
2
Secara khusus, fakta bahwa Anda menemukan diri Anda tipe pengujian adalah indikator yang baik bahwa perbandingan kesetaraan polimorfik adalah sarang ular beludak.
Steve Jessop
12

Kasus standar (tapi mudah-mudahan jarang) terlihat seperti ini: jika dalam situasi berikut

public void DoSomethingTo(SuperType o) {
  if (o isa SubTypeA) {
    DoSomethingA((SubTypeA) o )
  } else {
    DoSomethingB((SubTypeB) o );
  }
}

fungsi DoSomethingAatau DoSomethingBtidak dapat dengan mudah diimplementasikan sebagai fungsi anggota dari pohon warisan SuperType/ SubTypeA/ SubTypeB. Misalnya, jika

  • subtipe adalah bagian dari perpustakaan yang tidak dapat Anda ubah, atau
  • jika menambahkan kode untuk DoSomethingXXXpustaka itu berarti memperkenalkan ketergantungan terlarang.

Perhatikan ada sering situasi di mana Anda dapat menghindari masalah ini (misalnya, dengan membuat pembungkus atau adaptor untuk SubTypeAdan SubTypeB, atau mencoba untuk mengimplementasikan kembali secara lengkap DoSomethingdalam hal operasi dasar SuperType), tetapi kadang-kadang solusi ini tidak sepadan dengan kerumitan atau membuat hal-hal yang lebih rumit dan kurang dapat dikembangkan daripada melakukan tes tipe eksplisit.

Sebuah contoh dari pekerjaan kemarin saya: Saya punya situasi di mana saya akan memparalelkan pemrosesan daftar objek (tipe SuperType, dengan tepat dua subtipe berbeda, di mana sangat tidak mungkin ada lebih banyak). Versi tak tertandingi berisi dua loop: satu loop untuk objek subtipe A, panggilan DoSomethingA, dan loop kedua untuk objek subtipe B, panggilan DoSomethingB.

Metode "DoSomethingA" dan "DoSomethingB" adalah perhitungan intensif waktu, menggunakan informasi konteks yang tidak tersedia pada ruang lingkup subtipe A dan B. (sehingga tidak masuk akal untuk mengimplementasikannya sebagai fungsi anggota dari subtipe). Dari sudut pandang "loop paralel" yang baru, itu membuat banyak hal lebih mudah dengan berurusan dengan mereka secara seragam, jadi saya mengimplementasikan fungsi yang mirip dengan DoSomethingTodari atas. Namun, melihat implementasi dari "DoSomethingA" dan "DoSomethingB" menunjukkan mereka bekerja sangat berbeda secara internal. Jadi mencoba menerapkan generik "DoSomething" dengan memperluas SuperTypedengan banyak metode abstrak tidak benar-benar akan berhasil, atau akan berarti untuk merancang hal-hal sepenuhnya.

Doc Brown
sumber
2
Apakah Anda dapat menambahkan contoh kecil dan konkret untuk meyakinkan otak saya yang berkabut ini bahwa skenario ini tidak dibuat-buat?
svidgen
Untuk lebih jelasnya, saya tidak mengatakan itu dibuat - buat. Saya curiga saya otak luar biasa hari ini.
svidgen
@viden: ini jauh dari dibuat-buat, sebenarnya saya menghadapi situasi ini hari ini (walaupun saya tidak dapat memposting contoh ini di sini karena mengandung bisnis internal). Dan saya setuju dengan jawaban lain bahwa menggunakan operator "is" harus menjadi pengecualian dan hanya dilakukan dalam kasus yang jarang terjadi.
Doc Brown
Saya pikir hasil edit Anda, yang saya abaikan, memberikan contoh yang baik. ... Apakah ada kasus dimana ini adalah OK bahkan ketika Anda melakukan kontrol SuperTypedan subclass itu?
svidgen
6
Berikut adalah contoh nyata: wadah tingkat atas dalam respons JSON dari layanan web dapat berupa kamus atau larik. Biasanya, Anda memiliki beberapa alat yang mengubah JSON menjadi objek nyata (misalnya NSJSONSerializationdalam Obj-C), tetapi Anda tidak ingin hanya percaya bahwa respons berisi tipe yang Anda harapkan, jadi sebelum menggunakannya Anda memeriksanya (misalnya if ([theResponse isKindOfClass:[NSArray class]])...) .
Caleb
5

Seperti Paman Bob menyebutnya:

When your compiler forgets about the type.

Dalam salah satu episode Clean Coder-nya ia memberi contoh pemanggilan fungsi yang digunakan untuk mengembalikan Employees. Manageradalah sub-tipe dari Employee. Mari kita asumsikan kita memiliki layanan aplikasi yang menerima Managerid dan memanggilnya ke kantor :) Fungsi getEmployeeById()mengembalikan tipe super Employee, tapi saya ingin memeriksa apakah manajer dikembalikan dalam kasus penggunaan ini.

Sebagai contoh:

var manager = employeeRepository.getEmployeeById(empId);
if (!(manager is Manager))
   throw new Exception("Invalid Id specified.");
manager.summon();

Di sini saya sedang memeriksa untuk melihat apakah karyawan yang dikembalikan oleh permintaan sebenarnya adalah seorang manajer (yaitu saya berharap itu menjadi manajer dan jika gagal cepat).

Bukan contoh terbaik, tapi bagaimanapun juga paman Bob.

Memperbarui

Saya memperbarui contoh sebanyak yang saya ingat dari memori.

Songo
sumber
1
Mengapa Managerimplementasi tidak summon()hanya melemparkan pengecualian dalam contoh ini?
svidgen
@viden mungkin CEObisa memanggil Managers.
user253751
@viden, Maka tidak akan jelas apakah employeeRepository.getEmployeeById (empId) diharapkan untuk mengembalikan manajer
Ian
@Ian saya tidak melihat itu sebagai masalah. Jika kode panggilan meminta Employee, itu hanya harus peduli bahwa ia mendapatkan sesuatu yang berperilaku seperti Employee. Jika subclass yang berbeda Employeememiliki izin yang berbeda, tanggung jawab, dll., Apa yang membuat pengujian jenis opsi yang lebih baik daripada sistem izin nyata?
svidgen
3

Kapan pengecekan tipe OK?

Tidak pernah.

  1. Dengan memiliki perilaku yang terkait dengan jenis itu di beberapa fungsi lain, Anda melanggar Prinsip Terbuka Tertutup , karena Anda bebas mengubah perilaku tipe yang ada dengan mengubah isklausa, atau (dalam beberapa bahasa, atau tergantung pada skenario) karena Anda tidak dapat memperpanjang jenis tanpa memodifikasi internal fungsi melakukan ispemeriksaan.
  2. Lebih penting lagi, iscek adalah pertanda kuat bahwa Anda melanggar Prinsip Substitusi Liskov . Apa pun yang bekerja dengan SuperTypeharus benar-benar tidak tahu apa jenis sub ada.
  3. Anda secara implisit mengaitkan beberapa perilaku dengan nama tipe itu. Ini membuat kode Anda lebih sulit untuk dipelihara karena kontrak implisit ini tersebar di seluruh kode dan tidak dijamin akan ditegakkan secara universal dan konsisten seperti anggota kelas yang sebenarnya.

Semua yang dikatakan, iscek bisa kurang buruk daripada alternatif lain. Menempatkan semua fungsi umum ke dalam kelas dasar adalah tangan yang berat dan seringkali menyebabkan masalah yang lebih buruk. Menggunakan satu kelas yang memiliki bendera atau enum untuk "tipe" contohnya adalah ... lebih buruk daripada mengerikan, karena Anda sekarang menyebarkan jenis sistem pengelakan ke semua konsumen.

Singkatnya, Anda harus selalu mempertimbangkan pemeriksaan tipe sebagai bau kode yang kuat. Namun seperti semua panduan, ada saatnya Anda dipaksa untuk memilih di antara pelanggaran pedoman mana yang paling tidak ofensif.

Telastyn
sumber
3
Ada satu peringatan kecil: menerapkan tipe data aljabar dalam bahasa yang tidak mendukungnya. Namun, penggunaan pewarisan dan pengetikan ketik adalah murni detail implementasi yang buruk di sana; maksudnya bukan untuk memperkenalkan subtipe, tetapi untuk mengkategorikan nilai. Saya mengemukakannya hanya karena ADT berguna dan tidak pernah merupakan kualifikasi yang sangat kuat, tetapi sebaliknya saya sepenuhnya setuju; instanceofkebocoran detail implementasi dan memecah abstraksi.
Doval
17
"Tidak pernah" adalah kata yang sangat tidak saya sukai dalam konteks seperti itu, terutama ketika itu bertentangan dengan apa yang Anda tulis di bawah.
Doc Brown
10
Jika dengan "tidak pernah" Anda benar-benar bermaksud "kadang-kadang," maka Anda benar.
Caleb
2
OK berarti diizinkan, dapat diterima, dll., Tetapi belum tentu ideal atau optimal. Kontras dengan tidak OK : jika ada sesuatu yang tidak beres maka Anda tidak boleh melakukannya sama sekali. Seperti yang Anda tunjukkan, kebutuhan untuk memeriksa jenis sesuatu dapat menjadi indikasi masalah yang lebih dalam dalam kode, tetapi ada kalanya itu adalah pilihan yang paling bijaksana, paling tidak buruk, dan dalam situasi seperti itu jelas OK untuk menggunakan alat di Anda pembuangan. (Jika mudah untuk menghindari mereka dalam semua situasi, mereka mungkin tidak akan ada di tempat pertama.) Pertanyaannya adalah mengidentifikasi situasi-situasi itu, dan tidak pernah tidak membantu.
Caleb
2
@supercat: Suatu metode harus menuntut jenis yang paling spesifik yang memenuhi persyaratannya . IEnumerable<T>tidak menjanjikan elemen "terakhir" ada. Jika metode Anda membutuhkan elemen seperti itu, ia harus membutuhkan tipe yang menjamin keberadaannya. Dan subtipe jenis itu kemudian dapat memberikan implementasi efisien dari metode "Terakhir".
cHao
2

Jika Anda mendapat basis kode yang besar (lebih dari 100 ribu baris kode) dan dekat dengan pengiriman atau sedang bekerja di cabang yang nantinya harus digabung, dan oleh karena itu ada banyak biaya / risiko untuk mengubah banyak mengatasi.

Anda kemudian kadang-kadang memiliki opsi refraktor besar sistem, atau "pengujian tipe" sederhana yang dilokalkan. Ini menciptakan hutang teknis yang harus dibayar kembali sesegera mungkin, tetapi seringkali tidak.

(Tidak mungkin menghasilkan contoh, karena kode apa pun yang cukup kecil untuk digunakan sebagai contoh, juga cukup kecil untuk desain yang lebih baik agar terlihat jelas.)

Atau dengan kata lain, ketika tujuannya adalah untuk membayar upah Anda alih - alih mendapatkan "suara" untuk kebersihan desain Anda.


Kasus umum lainnya adalah kode UI, ketika misalnya Anda menunjukkan UI yang berbeda untuk beberapa jenis karyawan, tetapi jelas Anda tidak ingin konsep UI lolos ke semua "domain" kelas Anda.

Anda dapat menggunakan “pengujian tipe” untuk memutuskan versi UI mana yang akan ditampilkan, atau memiliki tabel pencarian mewah yang mengubah dari “kelas domain” ke “kelas UI”. Tabel pencarian hanyalah cara menyembunyikan "pengujian tipe" di satu tempat.

(Kode pembaruan basis data dapat memiliki masalah yang sama dengan kode UI, namun Anda cenderung hanya memiliki satu set kode pembaruan basis data, tetapi Anda dapat memiliki banyak layar berbeda yang harus beradaptasi dengan jenis objek yang ditampilkan.)

Ian
sumber
Pola Pengunjung sering kali merupakan cara yang baik untuk menyelesaikan kasus UI Anda.
Ian Goldby
@IanGoldby, setuju kadang-kadang bisa, namun Anda masih melakukan "pengujian tipe", hanya sedikit disembunyikan.
Ian
Tersembunyi dalam arti yang sama dengan yang disembunyikan ketika Anda memanggil metode virtual biasa? Atau maksud Anda sesuatu yang lain? Pola Pengunjung yang saya gunakan tidak memiliki pernyataan bersyarat yang tergantung pada jenis. Itu semua dilakukan oleh bahasa.
Ian Goldby
@IanGoldby, maksud saya tersembunyi dalam arti bahwa itu dapat membuat kode WPF atau WinForms lebih sulit untuk dipahami. Saya berharap bahwa untuk beberapa UI berbasis web itu akan bekerja dengan sangat baik.
Ian
2

Implementasi LINQ menggunakan banyak jenis pemeriksaan untuk kemungkinan optimasi kinerja, dan kemudian mundur untuk IEnumerable.

Contoh yang paling jelas mungkin adalah metode ElementAt (kutipan kecil dari sumber .NET 4.5):

public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, int index) { 
    IList<TSource> list = source as IList<TSource>;

    if (list != null) return list[index];
    // ... and then an enumerator is created and MoveNext is called index times

Tetapi ada banyak tempat di kelas Enumerable di mana pola yang sama digunakan.

Jadi mungkin mengoptimalkan kinerja untuk subtipe yang umum digunakan adalah penggunaan yang valid. Saya tidak yakin bagaimana ini bisa dirancang lebih baik.

Menno van den Heuvel
sumber
Itu bisa dirancang lebih baik dengan menyediakan cara yang nyaman di mana antarmuka dapat menyediakan implementasi standar, dan kemudian IEnumerable<T>memasukkan banyak metode seperti yang ada di dalamnya List<T>, bersama dengan Featuresproperti yang menunjukkan metode mana yang dapat diharapkan bekerja dengan baik, perlahan, atau tidak sama sekali, serta berbagai asumsi yang dapat dilakukan konsumen dengan aman tentang koleksi (mis. ukuran dan / atau konten yang ada dijamin tidak akan pernah berubah [suatu tipe mungkin mendukung Addsementara masih menjamin bahwa konten yang ada tidak akan berubah]).
supercat
Kecuali dalam skenario di mana jenis mungkin perlu memegang satu dari dua hal yang sama sekali berbeda pada waktu yang berbeda dan persyaratannya jelas saling eksklusif (dan dengan demikian menggunakan bidang terpisah akan menjadi berlebihan), saya biasanya menganggap perlunya mencoba-casting sebagai tanda. bahwa anggota yang seharusnya menjadi bagian dari antarmuka dasar, tidak. Itu bukan untuk mengatakan bahwa kode yang menggunakan antarmuka yang seharusnya menyertakan beberapa anggota tetapi tidak seharusnya menggunakan try-casting untuk mengatasi kelalaian antarmuka dasar, tetapi antarmuka basis penulisan itu harus meminimalkan kebutuhan klien untuk mencoba-coba.
supercat
1

Ada contoh yang sering muncul dalam pengembangan game, khususnya dalam deteksi tabrakan, yang sulit dihadapi tanpa menggunakan beberapa bentuk pengujian tipe.

Asumsikan bahwa semua objek game berasal dari kelas dasar umum GameObject. Setiap objek memiliki bentuk tubuh tabrakan kaku CollisionShapeyang dapat memberikan interface umum (untuk mengatakan posisi query, orientasi, dll) tapi bentuk tabrakan sebenarnya semua akan subclass beton seperti Sphere, Box, ConvexHull, dll menyimpan informasi spesifik untuk jenis objek geometris (lihat di sini untuk contoh nyata)

Sekarang, untuk menguji tabrakan saya perlu menulis fungsi untuk setiap pasangan jenis bentuk tabrakan:

detectCollision(Sphere, Sphere)
detectCollision(Sphere, Box)
detectCollision(Sphere, ConvexHull)
detectCollision(Box, ConvexHull)
...

yang berisi matematika spesifik yang diperlukan untuk melakukan persimpangan dari dua tipe geometris tersebut.

Pada setiap 'centang' loop permainan saya, saya perlu memeriksa pasangan objek untuk tabrakan. Tapi saya hanya memiliki akses ke GameObjectdan sesuai mereka CollisionShape. Jelas saya perlu tahu tipe konkret untuk mengetahui fungsi deteksi tabrakan untuk memanggil. Bahkan pengiriman ganda (yang secara logis tidak berbeda dengan memeriksa jenisnya) dapat membantu di sini *.

Dalam prakteknya dalam situasi ini mesin fisika yang pernah saya lihat (Bullet dan Havok) mengandalkan pengujian tipe dari satu bentuk atau lainnya.

Saya tidak mengatakan ini tentu merupakan solusi yang baik , hanya saja itu mungkin yang terbaik dari sejumlah kecil kemungkinan solusi untuk masalah ini

* Secara teknis itu adalah mungkin untuk menggunakan pengiriman ganda dengan cara menghebohkan dan rumit yang akan memerlukan N (N + 1) / 2 kombinasi (di mana N adalah jumlah jenis bentuk yang Anda miliki) dan hanya akan mengaburkan apa yang Anda benar-benar melakukan yang secara bersamaan mencari tahu jenis dari dua bentuk jadi saya tidak menganggap ini sebagai solusi yang realistis.

Martin
sumber
1

Kadang-kadang Anda tidak ingin menambahkan metode umum ke semua kelas karena itu bukan tanggung jawab mereka untuk melakukan tugas tertentu.

Misalnya Anda ingin menggambar beberapa entitas tetapi tidak ingin menambahkan kode gambar langsung ke mereka (yang masuk akal). Dalam bahasa yang tidak mendukung banyak pengiriman, Anda mungkin berakhir dengan kode berikut:

void DrawEntity(Entity entity) {
    if (entity instanceof Circle) {
        DrawCircle((Circle) entity));
    else if (entity instanceof Rectangle) {
        DrawRectangle((Rectangle) entity));
    } ...
}

Ini menjadi bermasalah ketika kode ini muncul di beberapa tempat dan Anda perlu memodifikasinya di mana saja ketika menambahkan tipe Entitas baru. Jika itu masalahnya maka hal itu dapat dihindari dengan menggunakan pola Pengunjung tetapi kadang-kadang lebih baik untuk menjaga hal-hal sederhana dan tidak memaksakannya. Itulah situasi ketika pengujian tipe OK.

Honza Brabec
sumber
0

Satu-satunya waktu yang saya gunakan adalah kombinasi dengan refleksi. Tetapi meskipun demikian sebagian besar pemeriksaan dinamis, tidak dikodekan ke kelas tertentu (atau hanya dikodekan ke kelas khusus sepertiString atau List).

Maksud saya dengan memeriksa dinamis:

boolean checkType(Type type, Object object) {
    if (object.isOfType(type)) {

    }
}

dan tidak hard-coded

boolean checkIsManaer(Object object) {
    if (object instanceof Manager) {

    }
}
m3th0dman
sumber
0

Pengujian tipe dan tipe casting adalah dua konsep yang sangat terkait erat. Begitu erat hubungannya sehingga saya merasa yakin untuk mengatakan bahwa Anda tidak boleh melakukan tes tipe kecuali niat Anda adalah mengetikkan objek berdasarkan hasil.

Ketika Anda berpikir tentang desain Berorientasi Objek yang ideal, pengujian tipe (dan casting) seharusnya tidak pernah terjadi. Tapi semoga sekarang Anda sudah tahu bahwa pemrograman Berorientasi Objek tidak ideal. Terkadang, terutama dengan kode level lebih rendah, kode tersebut tidak dapat tetap sesuai dengan ideal. Ini adalah kasus dengan ArrayLists di Jawa; karena mereka tidak tahu pada saat run-time kelas apa yang disimpan dalam array, mereka membuatObject[] array dan secara statis melemparkannya ke tipe yang benar.

Telah ditunjukkan bahwa kebutuhan umum untuk mengetik pengujian (dan mengetikkan casting) berasal dari Equalsmetode ini, yang dalam kebanyakan bahasa seharusnya dianggap sederhana.Object . Implementasi harus memiliki beberapa pemeriksaan terperinci untuk membuat jika kedua objek adalah tipe yang sama, yang mengharuskan untuk dapat menguji tipe apa mereka.

Pengujian tipe juga sering muncul dalam refleksi. Seringkali Anda akan memiliki metode yang mengembalikan Object[]atau beberapa array generik lainnya, dan Anda ingin menarik semua Fooobjek untuk alasan apa pun. Ini adalah penggunaan sah untuk pengujian dan casting tipe.

Secara umum, pengujian tipe buruk ketika kode pasangan Anda sia-sia dengan bagaimana implementasi spesifik ditulis. Ini dapat dengan mudah menyebabkan diperlukan tes khusus untuk setiap jenis atau kombinasi jenis, seperti jika Anda ingin menemukan persimpangan garis, persegi panjang, dan lingkaran, dan fungsi persimpangan memiliki algoritma yang berbeda untuk setiap kombinasi. Tujuan Anda adalah untuk menempatkan detail apa pun yang spesifik untuk satu jenis objek di tempat yang sama dengan objek itu, karena itu akan membuatnya lebih mudah untuk mempertahankan dan memperluas kode Anda.

Meustrus
sumber
1
ArrayListstidak tahu kelas yang disimpan pada saat runtime karena Java tidak memiliki obat generik dan ketika mereka akhirnya diperkenalkan Oracle memilih kompatibilitas mundur dengan kode generik-kurang. equalsmemiliki masalah yang sama, dan itu adalah keputusan desain yang dipertanyakan; perbandingan kesetaraan tidak masuk akal untuk setiap jenis.
Doval
1
Secara teknis, koleksi Java tidak mengirimkan kontennya ke apa pun. Kompiler menyuntikkan typecast di lokasi masing-masing akses: misalnyaString x = (String) myListOfStrings.get(0)
Terakhir kali saya melihat sumber Java (yang mungkin 1,6 atau 1,5) ada pemain eksplisit dalam sumber ArrayList. Itu menghasilkan peringatan kompiler (ditekan) untuk alasan yang baik tetapi diizinkan pula. Saya kira Anda bisa mengatakan bahwa karena bagaimana obat generik diimplementasikan, selalu saja Objectsampai diakses pula; generik di Jawa hanya menyediakan pengecoran implisit yang dibuat aman oleh aturan kompiler.
meustrus
0

Ini dapat diterima dalam kasus di mana Anda harus membuat keputusan yang melibatkan dua jenis dan keputusan ini dikemas dalam objek di luar hierarki jenis itu. Misalnya, Anda menjadwalkan objek mana yang akan diproses berikutnya dalam daftar objek yang menunggu diproses:

abstract class Vehicle
{
    abstract void Process();
}

class Car : Vehicle { ... }
class Boat : Vehicle { ... }
class Truck : Vehicle { ... }

Sekarang katakanlah logika bisnis kita secara harfiah adalah "semua mobil lebih diutamakan daripada kapal dan truk". Menambahkan Priorityproperti ke kelas tidak memungkinkan Anda untuk mengekspresikan logika bisnis ini dengan bersih karena Anda akan berakhir dengan ini:

abstract class Vehicle
{
    abstract void Process();
    abstract int Priority { get }
}

class Car : Vehicle { public Priority { get { return 1; } } ... }
class Boat : Vehicle { public Priority { get { return 2; } } ... }
class Truck : Vehicle { public Priority { get { return 2; } } ... }

Masalahnya adalah bahwa sekarang untuk memahami prioritas pemesanan Anda harus melihat semua subclass, atau dengan kata lain Anda telah menambahkan kopling ke subclass.

Tentu saja Anda harus membuat prioritas menjadi konstanta dan menempatkannya ke dalam kelas sendiri, yang membantu menjaga penjadwalan logika bisnis bersama:

static class Priorities
{
    public const int CAR_PRIORITY = 1;
    public const int BOAT_PRIORITY = 2;
    public const int TRUCK_PRIORITY = 2;
}

Namun, pada kenyataannya algoritma penjadwalan adalah sesuatu yang mungkin berubah di masa depan dan pada akhirnya mungkin bergantung pada lebih dari sekadar mengetik. Misalnya, bisa dikatakan "truk dengan berat lebih dari 5000 kg mendapat prioritas khusus di atas semua kendaraan lain." Itu sebabnya algoritma penjadwalan termasuk dalam kelasnya sendiri, dan itu ide yang baik untuk memeriksa jenis untuk menentukan mana yang harus duluan:

class VehicleScheduler : IScheduleVehicles
{
    public Vehicle WhichVehicleGoesFirst(Vehicle vehicle1, Vehicle vehicle2)
    {
        if(vehicle1 is Car) return vehicle1;
        if(vehicle2 is Car) return vehicle2;
        return vehicle1;
    }
}

Ini adalah cara paling mudah untuk mengimplementasikan logika bisnis dan masih yang paling fleksibel untuk perubahan di masa depan.

Scott Whitlock
sumber
Saya tidak setuju dengan contoh khusus Anda, tetapi akan setuju dengan prinsip memiliki referensi pribadi yang dapat menampung berbagai jenis dengan makna yang berbeda. Apa yang saya sarankan sebagai contoh yang lebih baik adalah bidang yang dapat menampung null, a String, atau a String[]. Jika 99% dari objek akan membutuhkan tepat satu string, mengenkapsulasi setiap string dalam konstruksi yang terpisah String[]dapat menambah overhead penyimpanan yang cukup besar. Menangani kasing tunggal menggunakan referensi langsung ke Stringakan membutuhkan lebih banyak kode, tetapi akan menghemat penyimpanan dan dapat membuat segalanya lebih cepat.
supercat
0

Pengujian tipe adalah alat, gunakan dengan bijak dan bisa menjadi sekutu yang kuat. Gunakan dengan buruk dan kode Anda akan mulai berbau.

Dalam perangkat lunak kami, kami menerima pesan melalui jaringan sebagai tanggapan atas permintaan. Semua pesan deserialized berbagi kelas dasar yang sama Message.

Kelas-kelas itu sendiri sangat sederhana, hanya muatannya seperti mengetikkan properti C # dan rutinitas untuk menyusun dan mengosongkannya (Bahkan saya membuat sebagian besar kelas menggunakan templat t4 dari deskripsi XML format pesan)

Kode akan menjadi seperti:

Message response = await PerformSomeRequest(requestParameter);

// Server (out of our control) would send one message as response, but 
// the actual message type is not known ahead of time (it depended on 
// the exact request and the state of the server etc.)
if (response is ErrorMessage)
{ 
    // Extract error message and pass along (for example using exceptions)
}
else if (response is StuffHappenedMessage)
{
    // Extract results
}
else if (response is AnotherThingHappenedMessage)
{
    // Extract another type of result
}
// Half a dozen other possible checks for messages

Memang, orang dapat berargumentasi bahwa arsitektur pesan bisa dirancang lebih baik tetapi dirancang sejak lama dan bukan untuk C # jadi memang demikian. Di sini pengujian tipe memecahkan masalah nyata bagi kita dengan cara yang tidak terlalu buruk.

Yang perlu dicatat adalah bahwa C # 7.0 mendapatkan pencocokan pola (yang dalam banyak hal adalah pengujian jenis steroid) tidak mungkin semuanya buruk ...

Isak Savo
sumber
0

Ambil parser JSON generik. Hasil penguraian yang sukses adalah array, kamus, string, angka, boolean, atau nilai nol. Itu bisa salah satunya. Dan elemen-elemen dari array atau nilai-nilai dalam kamus dapat kembali menjadi tipe-tipe tersebut. Karena data disediakan dari luar program Anda, Anda harus menerima hasil apa pun (yaitu Anda harus menerimanya tanpa menabrak; Anda dapat menolak hasil yang bukan yang Anda harapkan).

gnasher729
sumber
Nah, beberapa deserializers JSON akan berusaha untuk instantiate objek pohon jika struktur Anda memiliki tipe info untuk "bidang inferensi" di dalamnya. Tapi ya. Saya pikir ini adalah arah jawaban Karl B menuju.
svidgen