Pola iterator - mengapa penting untuk tidak mengekspos representasi internal?


24

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 arrayatau 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 GetEnumeratorfungsinya, 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.



4
Karena, implementasi mungkin ingin berubah dari menggunakan array, ke menggunakan daftar tertaut tanpa memerlukan perubahan di kelas konsumen. Ini tidak mungkin jika konsumen tahu persis kelas yang dikembalikan.
MTilsted

11
Ini bukan hal buruk bagi konsumen untuk diketahui , itu adalah hal buruk bagi konsumen untuk diandalkan . Saya tidak suka istilah penyembunyian informasi karena itu membuat Anda percaya bahwa informasi itu "pribadi" seperti kata sandi Anda, bukan pribadi seperti bagian dalam telepon. Komponen dalam disembunyikan karena tidak relevan bagi pengguna, bukan karena mereka semacam rahasia. Yang perlu Anda ketahui adalah menghubungi di sini, bicara di sini, dengarkan di sini.
Kapten Man

Misalkan kita memiliki "antarmuka" Fyang bagus yang memberi saya pengetahuan (metode) a, bdan c. Semua baik dan bagus, bisa ada banyak hal yang berbeda tetapi hanya Fbagi saya. Pikirkan metode ini sebagai "kendala" atau klausul kontrak yang Fmembuat kelas harus dikerjakan. Misalkan kita menambahkan d, karena kita membutuhkannya. Ini menambahkan kendala tambahan, setiap kali kita melakukan ini, kita memaksakan lebih banyak pada Fs. Akhirnya (kasus terburuk) hanya satu hal yang bisa Fjadi kita mungkin tidak memilikinya. Dan Fsangat membatasi hanya ada satu cara untuk melakukannya.
Alec Teal

@ kapten, komentar yang luar biasa. Ya saya melihat mengapa 'abstrak' banyak hal, tetapi twist tentang mengetahui dan mengandalkan adalah ... Terus terang .... Cemerlang. Dan perbedaan penting yang tidak saya pertimbangkan sampai membaca posting Anda.
MyDaftQuestions

Jawaban:


56

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 konkretArray biasanya menciptakan banyak janji tambahan, misalnya bahwa Anda dapat mengurutkan koleksi berdasarkan fungsi yang Anda pilih sendiri, belum lagi fakta bahwa orang normal Arraymungkin 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 Iterableantarmuka 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.


12
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!
MyDaftQuestions

18

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 daftar Foos:

void PrintFoos(IList<Foo> foos)
{
    foreach (foo in foos)
    {
        Console.WriteLine(foo);
    }
}

Anda hanya dapat menggunakan fungsi itu untuk mencetak daftar foo - tetapi hanya jika mereka menerapkan IList<Foo>

IList<Foo> foos = //.....
PrintFoos(foos);

Tetapi bagaimana jika Anda ingin mencetak setiap item daftar yang diindeks ? Anda harus membuat daftar baru:

IList<Foo> everySecondFoo = new List<T>();
bool isIndexEven = true;
foreach (foo; foos)
{
    if (isIndexEven)
    {
        everySecondFoo.Add(foo);
    }
    isIndexEven = !isIndexEven;
}
PrintFoos(everySecondFoo);

Ini cukup panjang, tetapi dengan LINQ kita bisa melakukannya ke satu-liner, yang sebenarnya lebih mudah dibaca:

PrintFoos(foos.Where((foo, i) => i % 2 == 0).ToList());

Sekarang, apakah Anda memperhatikannya .ToList()pada akhirnya? Ini mengubah permintaan malas menjadi daftar, sehingga kami dapat meneruskannya PrintFoos. 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:

void Print6Foos(IList<Foo> foos)
{
    int counter = 0;
    foreach (foo in foos)
    {
        Console.WriteLine(foo);
        ++ counter;
        if (6 < counter)
        {
            return;
        }
    }
}

// ........

Print6Foos(foos.Where((foo, i) => i % 2 == 0).ToList());

Bagaimana jika foosmemiliki 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:

void Print6Foos(Enumerable<Foo> foos)
{
    // everything else stays the same
}

// ........

Print6Foos(foos.Where((foo, i) => i % 2 == 0));

Sekarang Print6Foosdapat dengan malas mengulangi 6 item pertama dari daftar, dan kita tidak perlu menyentuh sisanya.

Tidak mengungkap representasi internal adalah kuncinya di sini. Ketika Print6Foosmenerima 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 membuat Enumerableobjek yang lebih efisien yang hanya mendukung fungsi yang sebenarnya dibutuhkan.


6

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 bertipe IList<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.


6

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 mendukungnya GetEnumrator()dapat diganti dengan kode lain yang dapat mendukung IEnumerable<T>. Ini menambah fleksibilitas.

Jika kode Anda hanya menggunakannya IEnumerable<T>dapat didukung oleh kode apa pun yang mengimplementasikan IEnumerable<T>. Sekali lagi, ada fleksibilitas ekstra.

Semua linq-to-object misalnya, hanya bergantung pada IEnumerable<T>. Meskipun cepat jalur beberapa implementasi tertentu IEnumerable<T>melakukan hal ini dengan cara uji-dan-gunakan yang selalu dapat mundur dengan hanya menggunakan GetEnumerator()dan IEnumerator<T>implementasi yang kembali. Ini memberikan fleksibilitas lebih daripada jika dibangun di atas array atau daftar.


Jawaban yang jelas dan bagus. Sangat tepat karena Anda membuatnya singkat, dan karena itu ikuti saran Anda di kalimat pertama. Bravo!
Daniel Hollinrake

0

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 arrayatau IList<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!


1
Re: "Atau pertimbangkan lingkungan pengembangan di mana Anda tidak punya waktu untuk membuat 'API yang baik,' dan perlu segera menggunakan hal-hal": Jika Anda menemukan diri Anda dalam lingkungan seperti itu, dan untuk beberapa alasan tidak ingin berhenti ( atau tidak yakin jika Anda melakukannya), saya merekomendasikan buku Death March: Panduan Lengkap Pengembang Perangkat Lunak untuk Bertahan dari Proyek 'Mission Impossible' . Ini memiliki saran yang sangat baik tentang masalah ini. (Juga, saya sarankan mengubah pikiran Anda tentang tidak berhenti.)
ruakh

@ruakh Saya merasa selalu penting untuk memahami cara bekerja di lingkungan di mana Anda tidak punya waktu untuk membuat API yang bagus. Terus terang, sama seperti kita menyukai pendekatan menara gading, kode sebenarnya adalah apa yang sebenarnya membayar tagihan. Semakin cepat kita mengakui bahwa, semakin cepat kita dapat mulai mendekati perangkat lunak sebagai keseimbangan, menyeimbangkan kualitas API dengan kode produktif, agar sesuai dengan lingkungan kita yang sebenarnya. Saya telah melihat terlalu banyak pengembang muda yang terjebak dalam kekacauan cita-cita, tidak menyadari bahwa mereka perlu melenturkannya. (Saya juga pengembang muda itu .. masih pulih dari 5 tahun desain "API yang baik")
Cort Ammon - Reinstate Monica

1
Kode asli membayar tagihan, ya. Desain API yang baik adalah bagaimana Anda memastikan bahwa Anda akan terus dapat menghasilkan kode nyata. Kode jelek berhenti membayar untuk dirinya sendiri ketika proyek menjadi tidak dapat dipelihara dan menjadi tidak mungkin untuk memperbaiki bug tanpa membuat yang baru. (Lagi pula, ini adalah dilema palsu. Apa yang dibutuhkan oleh kode yang baik bukanlah waktu sebanyak backbone .)
ruakh

1
@ruakh Apa yang saya temukan adalah mungkin untuk membuat kode yang bagus tanpa "API yang bagus." Jika diberi kesempatan, API yang baik itu bagus, tetapi memperlakukannya sebagai kebutuhan menyebabkan lebih banyak perselisihan daripada nilainya. Pertimbangkan: API untuk tubuh manusia sangat menghebohkan, tapi kami masih berpikir itu adalah hal-hal kecil yang bagus =)
Cort Ammon - Reinstate Monica

Contoh lain yang akan saya berikan adalah salah satu yang menjadi inspirasi untuk argumen tentang membuat waktu. Salah satu kelompok yang bekerja dengan saya memiliki beberapa hal driver perangkat yang perlu mereka kembangkan, dengan persyaratan waktu nyata yang sulit. Mereka memiliki 2 API untuk bekerja. Satu bagus, satu kurang begitu. API yang baik tidak dapat menentukan waktu karena menyembunyikan implementasinya. API yang lebih kecil mengekspos penerapannya, dan dengan melakukannya Anda dapat menggunakan teknik khusus prosesor untuk membuat produk yang berfungsi. API yang baik ditempatkan dengan sopan di rak, dan mereka terus memenuhi persyaratan waktu.
Cort Ammon - Reinstate Monica

0

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.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.