Antarmuka dalam C # 8: Asumsi Berbahaya dalam Implementasi Default

Halo, Habr!

Sebagai bagian dari mengeksplorasi topik C # 8, kami menyarankan untuk membahas artikel berikut tentang aturan baru untuk mengimplementasikan antarmuka.



Melihat dari dekat bagaimana antarmuka terstruktur dalam C # 8 , Anda perlu mempertimbangkan bahwa ketika mengimplementasikan antarmuka, Anda dapat memecah kayu bakar secara default.

Asumsi yang terkait dengan implementasi default dapat menyebabkan kode rusak, pengecualian runtime, dan kinerja yang buruk.

Salah satu fitur antarmuka C # 8 yang diiklankan secara aktif adalah Anda dapat menambahkan anggota ke antarmuka tanpa merusak implementer yang ada. Tetapi kurangnya perhatian dalam hal ini penuh dengan masalah besar. Pertimbangkan kode di mana asumsi yang salah dibuat - ini akan membuatnya lebih jelas betapa pentingnya untuk menghindari masalah seperti itu.

Semua kode untuk artikel ini diposting di GitHub: jeremybytes / interfaces-in-csharp-8 , khususnya di proyek DangerousAssumptions .

Catatan: Artikel ini membahas fitur C # 8, saat ini hanya diterapkan di .NET Core 3.0. Dalam contoh yang saya gunakan, Visual Studio 16.3.0 dan .NET Core 3.0.100 .

Asumsi tentang detail implementasi

Alasan utama mengapa saya mengartikulasikan masalah ini adalah sebagai berikut: Saya menemukan artikel di Internet di mana penulis menawarkan kode dengan asumsi yang sangat buruk tentang implementasi (saya tidak akan menunjukkan artikel karena saya tidak ingin penulis digulung dengan komentar; Saya akan menghubunginya secara pribadi) .

Artikel ini berbicara tentang seberapa baik implementasi default, karena memungkinkan kita untuk menambah antarmuka bahkan setelah kode sudah memiliki pelaksana. Namun, sejumlah asumsi buruk dibuat dalam kode ini (kode tersebut ada di folder BadInterface dalam proyek GitHub saya)

Inilah antarmuka aslinya:



Sisa artikel menunjukkan implementasi antarmuka MyFile (bagi saya, dalam file MyFile.cs ):

Artikel kemudian menunjukkan bagaimana Anda dapat menambahkan metode Rename dengan implementasi default, dan itu tidak akan merusak kelas MyFile ada.

Berikut ini antarmuka yang diperbarui (dari file IFileHandler.cs ):



MyFile masih berfungsi, jadi semuanya baik-baik saja. Jadi? Tidak juga.

Asumsi buruk

Masalah utama dengan metode Rename adalah asumsi BESAR terkait dengan itu: implementasi menggunakan file fisik yang terletak di sistem file.

Pertimbangkan implementasi yang saya buat untuk digunakan dalam sistem file yang terletak di RAM. (Catatan: ini adalah kode saya. Ini bukan dari artikel yang saya kritik. Anda akan menemukan implementasi penuh dalam file MemoryStringFileHandler.cs .)



Kelas ini mengimplementasikan sistem file formal yang menggunakan kamus yang terletak di RAM, yang berisi file teks. Tidak ada apa pun di sini yang akan memengaruhi sistem file fisik, umumnya tidak ada referensi ke System.IO .

Implementer rusak

Setelah memperbarui antarmuka, kelas ini rusak.

Jika kode klien memanggil metode Ganti nama, itu akan menghasilkan kesalahan runtime (atau, lebih buruk, mengganti nama file yang disimpan dalam sistem file).

Bahkan jika implementasi kami akan bekerja dengan file fisik, ia dapat mengakses file yang terletak di penyimpanan cloud, dan file tersebut tidak dapat diakses melalui System.IO.File.

Ada juga masalah potensial ketika datang ke unit testing. Jika objek yang disimulasikan atau palsu tidak diperbarui, dan kode yang diuji diperbarui, maka ia akan mencoba mengakses sistem file ketika melakukan tes unit.
Karena asumsi yang salah menyangkut antarmuka, para pelaksana antarmuka ini rusak.
Ketakutan yang tidak masuk akal?

Tidak ada gunanya menganggap kekhawatiran semacam itu tidak berdasar. Ketika saya berbicara tentang pelanggaran dalam kode, mereka menjawab saya: "Yah, hanya saja seseorang tidak tahu cara memprogram." Saya tidak bisa tidak setuju dengan ini.

Biasanya saya melakukan ini: Saya menunggu dan melihat bagaimana itu akan bekerja. Sebagai contoh, saya takut kemungkinan "penggunaan statis" akan disalahgunakan. Sejauh ini, ini belum harus diyakinkan.

Harus diingat bahwa ide-ide seperti itu ada di udara, jadi itu adalah kekuatan kita untuk membantu orang lain mengambil jalan yang lebih nyaman, yang tidak akan terlalu menyakitkan untuk diikuti.


Masalah kinerja

Saya mulai berpikir tentang masalah apa yang bisa menunggu kami jika kami membuat asumsi yang salah tentang pelaksana antarmuka.

Pada contoh sebelumnya, kode disebut berada di luar antarmuka itu sendiri (dalam hal ini, di luar System.IO). Anda mungkin akan setuju bahwa tindakan seperti itu adalah bel berbahaya. Tetapi, jika kita menggunakan hal-hal yang sudah menjadi bagian dari antarmuka, semuanya harus baik-baik saja, bukan?

Tidak selalu.

Sebagai contoh, saya membuat antarmuka IReader.

Antarmuka sumber dan implementasinya

Ini adalah antarmuka IReader asli (dari file IReader.cs - meskipun sekarang sudah ada pembaruan dalam file ini):



Ini adalah antarmuka metode generik yang memungkinkan Anda mendapatkan koleksi item hanya baca.

Salah satu implementasi dari antarmuka ini menghasilkan urutan angka Fibonacci (ya, saya memiliki minat tidak sehat dalam menghasilkan urutan Fibonacci). Ini adalah antarmuka FibonacciReader (dari file FibonacciReader.cs - juga diperbarui di github saya):



Kelas FibonacciSequence adalah implementasi dari IEnumerable <int> (dari file FibonacciSequence.cs). Ia menggunakan integer 32-bit sebagai tipe data, sehingga overflow terjadi cukup cepat.



Jika Anda tertarik dengan implementasi ini, lihat TDDing saya ke dalam Urutan Fibonacci dalam artikel C # .

Proyek DangerousAssumptions adalah aplikasi konsol yang menampilkan hasil FibonacciReader (dari file Program.cs ):



Dan inilah kesimpulannya:



Antarmuka yang diperbarui

Jadi sekarang kita memiliki kode yang berfungsi. Tapi, cepat atau lambat, kita mungkin perlu mendapatkan elemen terpisah dari IReader, dan bukan seluruh koleksi sekaligus. Karena kami menggunakan tipe generik dengan antarmuka, namun kami tidak memiliki properti "natural ID" di objek, kami akan memperluas elemen yang terletak pada indeks tertentu.

Berikut adalah antarmuka kami yang GetItemAt metode GetItemAt (dari versi final file IReader.cs ):



GetItemAt sini mengasumsikan implementasi default. Sekilas - tidak terlalu buruk. Ia menggunakan anggota antarmuka yang ada ( GetItems ), oleh karena itu, tidak ada asumsi "eksternal" yang dibuat di sini. Dengan hasilnya, ia menggunakan metode LINQ. Saya penggemar berat LINQ, dan kode ini, menurut pendapat saya, dibangun dengan wajar.

Perbedaan kinerja

Karena implementasi standar memanggil GetItems , itu membutuhkan seluruh koleksi untuk dikembalikan sebelum item tertentu dipilih.

Dalam kasus FibonacciReader ini menyiratkan bahwa semua nilai akan dihasilkan. Dalam formulir yang diperbarui, file Program.cs akan berisi kode berikut:



Jadi kami menyebutnya GetItemAt . Ini kesimpulannya:



Jika kita menempatkan pos pemeriksaan di dalam file FibonacciSequence.cs, kita akan melihat bahwa seluruh urutan dihasilkan untuk ini.

Setelah memulai program, kita akan menemukan titik kontrol ini dua kali: pertama ketika memanggil GetItems , dan kemudian saat memanggil GetItemAt .

Asumsi berbahaya bagi kinerja

Masalah paling serius dengan metode ini adalah ia membutuhkan pengambilan seluruh kumpulan elemen. Jika IReader ini akan mengambilnya dari database, maka banyak elemen yang harus ditarik darinya, dan kemudian hanya satu dari mereka yang akan dipilih. Akan jauh lebih baik jika seleksi akhir ditangani dalam database.

Bekerja dengan FibonacciReader kami, kami menghitung setiap elemen baru. Dengan demikian, seluruh daftar harus dihitung seluruhnya untuk mendapatkan hanya satu elemen yang kita butuhkan. Perhitungan urutan fibonacci adalah operasi yang tidak memuat prosesor terlalu banyak, tetapi bagaimana jika kita berurusan dengan sesuatu yang lebih rumit, misalnya, kita akan menghitung bilangan prima?

Anda mungkin berkata: “Ya, kami memiliki metode GetItems yang mengembalikan semuanya. Jika bekerja terlalu lama, maka mungkin seharusnya tidak ada di sini. Dan ini pernyataan yang jujur.

Namun, kode panggilan tidak tahu apa-apa tentang ini. Jika saya memanggil GetItems , maka saya tahu bahwa (mungkin) informasi saya harus melalui jaringan, dan proses ini akan intensif tanggal. Jika saya meminta satu item, lalu mengapa saya harus mengharapkan biaya seperti itu?

Optimalisasi Kinerja Khusus

Dalam kasus FibonacciReader kita dapat menambahkan implementasi kita sendiri untuk secara signifikan meningkatkan kinerja (dalam versi final file FibonacciReader.cs ):



Metode GetItemAt mengesampingkan implementasi default yang disediakan di antarmuka.

Di sini saya menggunakan metode ElementAt LINQ yang sama seperti dalam implementasi default. Namun, saya tidak menggunakan metode ini dengan koleksi read-only yang dikembalikan GetItems, tetapi dengan FibonacciSequence, yang merupakan IEnumerable .

Karena FibonacciSequence adalah IEnumerable , panggilan ke ElementAt berakhir segera setelah program mencapai elemen yang telah kita pilih. Jadi, kami tidak akan menghasilkan seluruh koleksi, tetapi hanya elemen yang terletak hingga posisi yang ditentukan dalam indeks.

Untuk mencoba ini, biarkan titik kontrol yang kami buat di atas dalam aplikasi dan jalankan aplikasi lagi. Kali ini kami menemukan breakpoint sekali saja (saat memanggil GetItems ). Saat memanggil GetItemAt ini tidak akan terjadi.

Contoh yang sedikit dibuat-buat

Contoh ini agak dibuat-buat, karena, sebagai aturan, Anda tidak harus memilih elemen dari kumpulan data menurut indeks. Namun, Anda dapat membayangkan hal serupa yang dapat terjadi jika kami bekerja dengan properti ID alami.

Jika kami menarik item berdasarkan ID, bukan berdasarkan indeks, kami mungkin menghadapi masalah kinerja yang sama dengan implementasi default. Implementasi default membutuhkan pengembalian semua elemen, setelah itu hanya satu yang dipilih dari mereka. Jika Anda mengizinkan basis data atau "pembaca" lain untuk menarik elemen tertentu dengan ID-nya, operasi seperti itu akan jauh lebih efisien.

Pikirkan asumsi Anda

Asumsi sangat diperlukan. Jika kami mencoba mempertimbangkan dalam kode apa pun kemungkinan kasus penggunaan pustaka kami, maka tidak ada tugas yang akan selesai. Tetapi Anda masih perlu mempertimbangkan asumsi dalam kode dengan hati-hati.

Ini tidak berarti bahwa implementasi GetElementAt selalu buruk. Ya, ada potensi masalah kinerja dengannya. Namun, jika set data kecil, atau elemen yang dihitung "murah," maka implementasi default mungkin merupakan kompromi yang masuk akal.

Saya, bagaimanapun, tidak menerima perubahan di antarmuka setelah sudah memiliki implementer. Tapi saya mengerti bahwa ada juga beberapa skenario di mana opsi alternatif lebih disukai. Pemrograman adalah solusi dari masalah, dan ketika memecahkan masalah perlu untuk menimbang pro dan kontra yang melekat dalam setiap alat dan pendekatan yang kita gunakan.

Implementasi default berpotensi merusak implementator antarmuka (dan mungkin kode yang akan meminta implementasi ini). Oleh karena itu, Anda harus sangat berhati-hati tentang asumsi yang terkait dengan implementasi default.

Semoga berhasil dalam pekerjaan Anda!

Source: https://habr.com/ru/post/id471872/


All Articles