Saya membaca C # Essential Pola Desain . Saya sedang membaca tentang pola iterator.
Saya sepenuhnya mengerti bagaimana menerapkannya, tetapi saya tidak mengerti pentingnya atau melihat use case. Dalam buku tersebut diberikan contoh di mana seseorang perlu mendapatkan daftar objek. Mereka bisa melakukan ini dengan mengekspos properti publik, seperti IList<T>
atau Array
.
Buku itu menulis
Masalah dengan ini adalah bahwa representasi internal di kedua kelas ini telah terkena proyek luar.
Apa representasi internal? Fakta itu array
atau IList<T>
? Saya benar-benar tidak mengerti mengapa ini adalah hal buruk bagi konsumen (programmer memanggil ini) untuk mengetahui ...
Buku itu kemudian mengatakan pola ini berfungsi dengan mengekspos GetEnumerator
fungsinya, sehingga kita dapat memanggil GetEnumerator()
dan mengekspos 'daftar' dengan cara ini.
Saya menganggap pola ini memiliki tempat (seperti semua) dalam situasi tertentu, tetapi saya gagal melihat di mana dan kapan.
sumber
F
yang bagus yang memberi saya pengetahuan (metode)a
,b
danc
. Semua baik dan bagus, bisa ada banyak hal yang berbeda tetapi hanyaF
bagi saya. Pikirkan metode ini sebagai "kendala" atau klausul kontrak yangF
membuat kelas harus dikerjakan. Misalkan kita menambahkand
, karena kita membutuhkannya. Ini menambahkan kendala tambahan, setiap kali kita melakukan ini, kita memaksakan lebih banyak padaF
s. Akhirnya (kasus terburuk) hanya satu hal yang bisaF
jadi kita mungkin tidak memilikinya. DanF
sangat membatasi hanya ada satu cara untuk melakukannya.Jawaban:
Perangkat lunak adalah permainan janji dan hak istimewa. Tidak pernah merupakan ide yang baik untuk menjanjikan lebih dari yang dapat Anda berikan, atau lebih dari kebutuhan kolaborator Anda.
Ini berlaku khususnya untuk tipe. Inti dari penulisan kumpulan iterable adalah bahwa penggunanya dapat beralih di atasnya - tidak lebih, tidak kurang. Mengekspos tipe konkret
Array
biasanya menciptakan banyak janji tambahan, misalnya bahwa Anda dapat mengurutkan koleksi berdasarkan fungsi yang Anda pilih sendiri, belum lagi fakta bahwa orang normalArray
mungkin akan memungkinkan kolaborator untuk mengubah data yang disimpan di dalamnya.Bahkan jika Anda berpikir ini adalah hal yang baik ("Jika penyaji mengetahui bahwa opsi ekspor baru tidak ada, itu hanya dapat menambalnya langsung! Rapi!"), Secara keseluruhan ini mengurangi koherensi basis kode, membuatnya lebih sulit untuk alasan tentang - dan membuat kode mudah dipikirkan adalah tujuan utama dari rekayasa perangkat lunak.
Sekarang, jika kolaborator Anda memerlukan akses ke sejumlah hal sehingga mereka dijamin tidak ketinggalan, Anda mengimplementasikan
Iterable
antarmuka dan hanya mengekspos metode-metode yang dinyatakan oleh antarmuka ini. Dengan begitu, tahun depan ketika struktur data yang jauh lebih baik dan lebih efisien muncul di perpustakaan standar Anda, Anda akan dapat mengganti kode yang mendasarinya dan mengambil manfaat darinya tanpa memperbaiki kode klien Anda di mana-mana . Ada manfaat lain untuk tidak menjanjikan lebih dari yang dibutuhkan, tetapi yang satu ini saja sangat besar sehingga dalam praktiknya tidak diperlukan yang lain.sumber
Exposing the concrete type Array usually creates many additional promises, e.g. that you can sort the collection by a function of your own choosing...
- Cemerlang. Aku hanya tidak memikirkannya. Ya, itu mengembalikan 'iterasi' dan hanya, iterasi!Menyembunyikan implementasi adalah prinsip inti dari OOP dan ide yang bagus di semua paradigma, tetapi ini sangat penting untuk iterator (atau apa pun namanya dalam bahasa tertentu) dalam bahasa yang mendukung iterasi malas.
Masalah dengan mengekspos jenis beton iterables - atau bahkan antarmuka seperti
IList<T>
- bukan pada objek yang mengeksposnya, tetapi pada metode yang menggunakannya. Misalnya, katakanlah Anda memiliki fungsi untuk mencetak daftarFoo
s:Anda hanya dapat menggunakan fungsi itu untuk mencetak daftar foo - tetapi hanya jika mereka menerapkan
IList<Foo>
Tetapi bagaimana jika Anda ingin mencetak setiap item daftar yang diindeks ? Anda harus membuat daftar baru:
Ini cukup panjang, tetapi dengan LINQ kita bisa melakukannya ke satu-liner, yang sebenarnya lebih mudah dibaca:
Sekarang, apakah Anda memperhatikannya
.ToList()
pada akhirnya? Ini mengubah permintaan malas menjadi daftar, sehingga kami dapat meneruskannyaPrintFoos
. Ini membutuhkan alokasi daftar kedua, dan dua lintasan pada item (satu pada daftar pertama untuk membuat daftar kedua, dan satu lagi pada daftar kedua untuk mencetaknya). Juga, bagaimana jika kita memiliki ini:Bagaimana jika
foos
memiliki ribuan entri? Kita harus memeriksa semuanya dan mengalokasikan daftar besar hanya untuk mencetak 6 dari mereka!Masukkan Enumerators - versi C # dari The Iterator Pattern. Alih-alih memiliki fungsi kami menerima daftar, kami membuatnya menerima
Enumerable
:Sekarang
Print6Foos
dapat dengan malas mengulangi 6 item pertama dari daftar, dan kita tidak perlu menyentuh sisanya.Tidak mengungkap representasi internal adalah kuncinya di sini. Ketika
Print6Foos
menerima daftar, kami harus memberikan daftar - sesuatu yang mendukung akses acak - dan karena itu kami harus mengalokasikan daftar, karena tanda tangan tidak menjamin bahwa itu hanya akan beralih di atasnya. Dengan menyembunyikan implementasinya, kita dapat dengan mudah membuatEnumerable
objek yang lebih efisien yang hanya mendukung fungsi yang sebenarnya dibutuhkan.sumber
Mengekspos representasi internal sebenarnya bukan ide yang bagus. Itu tidak hanya membuat kode lebih sulit untuk dipikirkan, tetapi juga lebih sulit untuk dipelihara. Bayangkan Anda telah memilih representasi internal apa saja - katakanlah sebuah
IList<T>
- dan membuka internal ini. Siapa pun yang menggunakan kode Anda dapat mengakses Daftar dan kode dapat mengandalkan representasi internal sebagai Daftar.Untuk alasan apa pun Anda memutuskan untuk mengubah representasi internal
IAmWhatever<T>
pada beberapa saat kemudian. Alih-alih hanya mengubah internal kelas Anda, Anda harus mengubah setiap baris kode dan metode bergantung pada representasi internal yang bertipeIList<T>
. Ini rumit dan rentan terhadap kesalahan, ketika Anda adalah satu-satunya yang menggunakan kelas, tetapi mungkin merusak kode orang lain menggunakan kelas Anda. Jika Anda hanya mengekspos serangkaian metode publik tanpa memaparkan internal Anda bisa mengubah representasi internal tanpa garis kode di luar kelas Anda memperhatikan, bekerja seolah-olah tidak ada yang berubah.Inilah sebabnya mengapa enkapsulasi adalah salah satu aspek terpenting dari setiap desain perangkat lunak nontrivial.
sumber
Semakin sedikit Anda mengatakan, semakin sedikit Anda harus terus berkata.
Semakin sedikit Anda bertanya, semakin sedikit Anda harus diberitahu.
Jika kode Anda hanya terpapar
IEnumerable<T>
, yang hanya mendukungnyaGetEnumrator()
dapat diganti dengan kode lain yang dapat mendukungIEnumerable<T>
. Ini menambah fleksibilitas.Jika kode Anda hanya menggunakannya
IEnumerable<T>
dapat didukung oleh kode apa pun yang mengimplementasikanIEnumerable<T>
. Sekali lagi, ada fleksibilitas ekstra.Semua linq-to-object misalnya, hanya bergantung pada
IEnumerable<T>
. Meskipun cepat jalur beberapa implementasi tertentuIEnumerable<T>
melakukan hal ini dengan cara uji-dan-gunakan yang selalu dapat mundur dengan hanya menggunakanGetEnumerator()
danIEnumerator<T>
implementasi yang kembali. Ini memberikan fleksibilitas lebih daripada jika dibangun di atas array atau daftar.sumber
Sebuah kata-kata yang saya suka gunakan mirror @CaptainMan komentar: "Tidak masalah bagi konsumen untuk mengetahui detail internal. Sangat buruk bagi pelanggan untuk perlu mengetahui detail internal."
Jika seorang pelanggan harus mengetik
array
atauIList<T>
dalam kode mereka, ketika tipe-tipe itu adalah detail internal, konsumen harus memperbaikinya. Jika mereka salah, konsumen mungkin tidak mengkompilasi, atau bahkan mendapatkan hasil yang salah. Ini menghasilkan perilaku yang sama dengan jawaban lain dari "selalu sembunyikan detail implementasi karena Anda tidak boleh mengeksposnya," tetapi mulai menggali pada keuntungan dari tidak mengekspos. Jika Anda tidak pernah mengungkapkan detail implementasi, pelanggan Anda tidak akan pernah bisa mengikat diri dengan menggunakannya!Saya suka memikirkannya dalam hal ini karena ini membuka konsep menyembunyikan implementasi ke nuansa makna, alih-alih kejam "selalu menyembunyikan detail implementasi Anda." Ada banyak situasi di mana mengekspos implementasi benar-benar valid. Pertimbangkan kasus pengandar perangkat waktu nyata, di mana kemampuan untuk melewati lapisan abstraksi masa lalu dan menyodok byte ke tempat yang tepat dapat menjadi perbedaan antara membuat waktu atau tidak. Atau pertimbangkan lingkungan pengembangan tempat Anda tidak punya waktu untuk membuat "API yang bagus," dan perlu segera menggunakannya. Seringkali mengekspos API yang mendasarinya di lingkungan seperti itu dapat menjadi perbedaan antara membuat tenggat waktu atau tidak.
Namun, ketika Anda meninggalkan kasus-kasus khusus dan melihat kasus-kasus yang lebih umum, itu mulai menjadi semakin penting untuk menyembunyikan implementasi. Mengungkap detail implementasi seperti tali. Digunakan dengan benar, Anda dapat melakukan segala macam hal dengan itu, seperti mendaki gunung tinggi. Digunakan secara tidak benar, dan itu dapat mengikat Anda dalam tali. Semakin sedikit Anda tahu tentang bagaimana produk Anda akan digunakan, semakin Anda harus berhati-hati.
Studi kasus yang saya lihat berulang kali adalah di mana pengembang membuat API yang tidak terlalu berhati-hati menyembunyikan detail implementasi. Pengembang kedua, menggunakan perpustakaan itu, menemukan bahwa API kehilangan beberapa fitur utama yang mereka inginkan. Alih-alih menulis permintaan fitur (yang bisa memiliki waktu penyelesaian berbulan-bulan), mereka malah memutuskan untuk menyalahgunakan akses mereka ke detail implementasi tersembunyi (yang membutuhkan beberapa detik). Ini tidak buruk dalam dirinya sendiri, tetapi seringkali pengembang API tidak pernah merencanakan hal itu menjadi bagian dari API. Kemudian, ketika mereka meningkatkan API dan mengubah detail yang mendasarinya, mereka menemukan bahwa mereka dirantai ke implementasi yang lama, karena seseorang menggunakannya sebagai bagian dari API. Versi terburuk dari ini adalah ketika seseorang menggunakan API Anda untuk menyediakan fitur yang berfokus pada pelanggan, dan sekarang perusahaan Anda cek gaji terkait dengan API yang cacat ini! Cukup dengan mengekspos detail implementasi ketika Anda tidak cukup tahu tentang pelanggan Anda terbukti cukup tali untuk mengikat tali di sekitar API Anda!
sumber
Anda tidak perlu tahu apa representasi internal data Anda - atau mungkin menjadi di masa depan.
Asumsikan Anda memiliki sumber data yang Anda wakili sebagai array, Anda dapat menggunakannya kembali dengan perulangan reguler.
Sekarang katakan Anda ingin refactor. Bos Anda telah meminta agar sumber data menjadi objek basis data. Selain itu, ia ingin memiliki opsi untuk menarik data dari API, atau hashmap, atau daftar yang ditautkan, atau drum magnetik berputar, atau mikrofon, atau jumlah serutan yak aktual yang ditemukan di Mongolia Luar.
Nah, jika Anda hanya memiliki satu untuk loop Anda dapat memodifikasi kode Anda dengan mudah untuk mengakses objek data dengan cara yang diharapkan akan diakses.
Jika Anda memiliki 1000 kelas yang mencoba mengakses objek data, nah sekarang Anda memiliki masalah besar refactoring.
Iterator adalah cara standar untuk mengakses sumber data berbasis daftar. Ini adalah antarmuka umum. Menggunakannya akan membuat kode sumber data Anda agnostik.
sumber