Bagaimana seseorang memutuskan apakah tipe objek data harus dirancang agar tidak berubah?

23

Saya suka "pola" abadi karena kekuatannya, dan di masa lalu saya merasa bermanfaat untuk merancang sistem dengan tipe data yang tidak dapat diubah (beberapa, sebagian atau bahkan semua). Seringkali ketika saya melakukannya, saya menemukan diri saya menulis lebih sedikit bug dan men-debug jauh lebih mudah.

Namun, teman-teman saya pada umumnya menghindar dari kekekalan. Mereka sama sekali tidak berpengalaman (jauh dari itu), namun mereka menulis objek data dengan cara klasik - anggota pribadi dengan pengambil dan penyetel untuk setiap anggota. Maka biasanya konstruktor mereka tidak mengambil argumen, atau mungkin hanya mengambil beberapa argumen untuk kenyamanan. Begitu sering, membuat objek terlihat seperti ini:

Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

Mungkin mereka melakukan itu di mana-mana. Mungkin mereka bahkan tidak mendefinisikan konstruktor yang mengambil dua string, tidak peduli betapa pentingnya mereka. Dan mungkin mereka tidak mengubah nilai string itu nanti dan tidak perlu. Jelas jika hal-hal itu benar, objek akan dirancang lebih baik sebagai abadi, bukan? (konstruktor mengambil dua properti, tidak ada setter sama sekali).

Bagaimana Anda memutuskan apakah suatu jenis objek harus dirancang sebagai tidak berubah? Apakah ada seperangkat kriteria yang baik untuk menilai itu?

Saat ini saya sedang berdebat apakah akan mengganti beberapa tipe data dalam proyek saya sendiri menjadi tidak berubah, tetapi saya harus membenarkannya kepada rekan-rekan saya, dan data dalam tipe tersebut mungkin (SANGAT jarang) berubah - pada saat itu tentu saja Anda dapat mengubah itu cara abadi (buat yang baru, salin properti dari objek lama kecuali yang ingin Anda ubah). Tetapi saya tidak yakin apakah ini hanya kecintaan saya terhadap kekekalan yang ditunjukkan, atau apakah ada kebutuhan aktual / manfaat dari mereka.

Ricket
sumber
1
Program di Erlang dan seluruh masalah terpecahkan, semuanya tidak berubah
Zachary K
1
@ ZacharyK Saya sebenarnya berpikir untuk menyebutkan sesuatu tentang pemrograman fungsional, tetapi menahan diri karena pengalaman saya yang terbatas dengan bahasa pemrograman fungsional.
Ricket

Jawaban:

27

Sepertinya Anda mendekatinya ke belakang. Orang harus default ke tidak berubah. Hanya membuat objek bisa berubah jika Anda benar-benar harus / tidak bisa membuatnya berfungsi sebagai objek abadi.

Brian Knoblauch
sumber
11

Manfaat utama benda tidak bergerak adalah menjamin keamanan benang. Di dunia di mana banyak inti dan utas adalah norma, manfaat ini menjadi sangat penting.

Tetapi menggunakan benda yang bisa berubah sangat nyaman. Mereka berkinerja baik, dan selama Anda tidak memodifikasi mereka dari utas terpisah, dan Anda memiliki pemahaman yang baik tentang apa yang Anda lakukan, mereka cukup dapat diandalkan.

Robert Harvey
sumber
15
Manfaat dari objek yang tidak berubah adalah lebih mudah dipikirkan. Dalam lingkungan multithreaded, ini sangat mudah; dalam algoritma single-threaded biasa, itu masih lebih mudah. Misalnya, Anda tidak perlu peduli apakah statusnya konsisten, apakah objek lulus semua mutasi yang diperlukan untuk digunakan dalam konteks tertentu, dll. Objek yang dapat berubah terbaik memungkinkan Anda untuk sedikit peduli tentang keadaan tepatnya, juga: misalnya cache.
9000
-1, saya setuju dengan @ 9000. Keamanan utas bersifat sekunder (pertimbangkan objek yang tampak tidak berubah tetapi memiliki kondisi internal yang dapat berubah karena misalnya memoisasi). Selain itu, menampilkan kinerja yang dapat diubah adalah pengoptimalan prematur, dan dimungkinkan untuk mempertahankan apa pun jika Anda mengharuskan pengguna "tahu apa yang mereka lakukan". Jika saya tahu apa yang saya lakukan sepanjang waktu, saya tidak akan pernah menulis sebuah program dengan bug.
Doval
4
@Doval: Tidak ada yang tidak setuju. 9000 benar sekali; objek abadi yang mudah untuk alasan tentang. Itu sebagian yang membuat mereka sangat berguna untuk pemrograman bersamaan. Bagian lainnya adalah Anda tidak perlu khawatir tentang perubahan kondisi. Optimalisasi prematur tidak relevan di sini; penggunaan objek yang tidak dapat diubah adalah tentang desain, bukan kinerja, dan pilihan yang buruk dari struktur data di muka adalah pesimisasi dini.
Robert Harvey
@ RobertTarvey Saya tidak yakin apa yang Anda maksud dengan "pilihan struktur data yang buruk di muka." Ada versi yang tidak berubah dari sebagian besar struktur data yang bisa berubah di luar sana yang memberikan kinerja serupa. Jika Anda membutuhkan daftar, dan Anda memiliki pilihan untuk menggunakan daftar yang tidak dapat diubah dan daftar yang dapat diubah, Anda memilih yang tidak berubah sampai Anda tahu pasti itu adalah hambatan dalam aplikasi Anda dan versi yang dapat diubah akan bekerja lebih baik.
Doval
2
@Doval: Saya sudah membaca Okasaki. Struktur data tersebut tidak umum digunakan kecuali jika Anda menggunakan bahasa yang sepenuhnya mendukung paradigma fungsional, seperti Haskell atau Lisp. Dan saya membantah anggapan bahwa struktur yang tidak dapat diubah adalah pilihan default; sebagian besar sistem komputasi bisnis masih dirancang di sekitar struktur yang bisa berubah (yaitu basis data relasional). Memulai dengan struktur data yang tidak berubah adalah ide yang bagus, tetapi menara ini masih sangat gading.
Robert Harvey
6

Ada dua cara utama untuk memutuskan apakah suatu objek tidak dapat diubah.

a) Berdasarkan sifat Objek

Mudah untuk menangkap situasi ini karena kita tahu bahwa benda-benda ini tidak akan berubah setelah dibangun. Misalnya jika Anda memiliki RequestHistoryentitas dan entitas sejarah alami tidak berubah setelah dibangun. Objek-objek ini dapat langsung dirancang sebagai kelas yang tidak berubah. Perlu diingat bahwa Obyek Permintaan adalah berkualitas karena dapat mengubah keadaannya dan kepada siapa ia ditugaskan ke dll dari waktu ke waktu tetapi sejarah permintaan tidak berubah. Misalnya, ada elemen riwayat yang dibuat minggu lalu ketika dipindahkan dari status diserahkan ke yang ditugaskan DAN entitiy sejarah ini tidak pernah bisa berubah. Jadi ini adalah kasus abadi klasik.

b) Berdasarkan pilihan desain, faktor eksternal

Ini mirip dengan contoh java.lang.String. String sebenarnya dapat berubah dari waktu ke waktu tetapi dengan desain, mereka menjadikannya tidak dapat diubah karena faktor caching / string pool / concurrency. Sama halnya caching / konkurensi, dll. Dapat memainkan peran yang baik dalam membuat objek tidak bergerak jika caching / konkurensi dan kinerja terkait sangat penting dalam aplikasi. Tetapi keputusan ini harus diambil dengan sangat hati-hati setelah menganalisis semua dampaknya.

Keuntungan utama dari objek yang tidak dapat diubah adalah mereka tidak mengalami pola guling-guling. Yaitu objek tidak akan menerima perubahan apa pun selama masa hidup dan itu membuat pengkodean dan pemeliharaannya sangat sangat mudah.

java_mouse
sumber
4

Saat ini saya sedang berdebat apakah akan mengganti beberapa tipe data dalam proyek saya sendiri menjadi tidak berubah, tetapi saya harus membenarkannya kepada rekan-rekan saya, dan data dalam tipe tersebut mungkin (SANGAT jarang) berubah - pada saat itu tentu saja Anda dapat mengubah itu cara abadi (buat yang baru, salin properti dari objek lama kecuali yang ingin Anda ubah).

Meminimalkan keadaan suatu program sangat bermanfaat.

Tanyakan kepada mereka apakah mereka ingin menggunakan tipe nilai yang bisa berubah di salah satu kelas Anda untuk penyimpanan sementara dari kelas klien.

Jika mereka menjawab ya, tanyakan mengapa? Keadaan yang tidak dapat berubah tidak termasuk dalam skenario seperti ini. Memaksa mereka untuk membuat negara di mana ia sebenarnya berada dan menjadikan status tipe data Anda sejelas mungkin adalah alasan yang sangat baik.

brian
sumber
4

Jawabannya agak tergantung pada bahasa. Kode Anda terlihat seperti Java, di mana masalah ini sesulit mungkin. Di Jawa, objek hanya bisa dilewatkan dengan referensi, dan klon benar-benar rusak.

Tidak ada jawaban sederhana, tetapi yang pasti Anda ingin membuat objek nilai ish kecil tidak dapat diubah. Java dengan benar membuat String tidak dapat diubah, tetapi salah membuat Tanggal dan Kalender bisa berubah.

Jadi pasti membuat objek bernilai kecil tidak berubah, dan mengimplementasikan copy constructor. Lupakan semua tentang Cloneable, itu dirancang sangat buruk sehingga tidak berguna.

Untuk objek bernilai lebih besar, jika tidak nyaman untuk membuatnya tidak berubah, maka membuatnya mudah untuk disalin.

kevin cline
sumber
Kedengarannya sangat mirip bagaimana memilih antara tumpukan dan tumpukan ketika menulis C atau C ++ :)
Ricket
@Ricket: IMO tidak terlalu banyak. Stack / heap tergantung pada objek seumur hidup. Sangat umum di C ++ untuk memiliki objek yang bisa berubah pada stack.
kevin cline
1

Dan mungkin mereka tidak mengubah nilai string itu nanti dan tidak perlu. Jelas jika hal-hal itu benar, objek akan dirancang lebih baik sebagai abadi, bukan?

Agak kontra-intuitif, tidak perlu mengubah string di kemudian hari adalah argumen yang cukup bagus bahwa tidak masalah apakah objek tidak berubah atau tidak. Programmer sudah memperlakukan mereka sebagai tidak dapat diubah secara efektif apakah kompiler memaksakannya atau tidak.

Kekekalan biasanya tidak menyakitkan, tetapi juga tidak selalu membantu. Cara mudah untuk mengetahui apakah objek Anda mungkin mendapat manfaat dari ketidakberubahan adalah jika Anda perlu membuat salinan objek atau mendapatkan mutex sebelum mengubahnya . Jika tidak pernah berubah, maka ketidakmampuan tidak benar-benar membelikan Anda apa-apa, dan terkadang membuat segalanya lebih rumit.

Anda memang memiliki poin yang baik tentang risiko membangun objek dalam keadaan tidak valid, tapi itu benar-benar masalah terpisah dari ketidakberubahan. Objek dapat berubah - ubah dan selalu dalam keadaan valid setelah konstruksi.

Pengecualian untuk aturan itu adalah karena Java tidak mendukung parameter bernama atau parameter default, kadang-kadang bisa sulit untuk mendesain kelas yang menjamin objek yang valid hanya menggunakan konstruktor kelebihan beban. Tidak begitu banyak dengan kasus dua-properti, tetapi kemudian ada sesuatu yang bisa dikatakan untuk konsistensi juga, jika pola itu sering terjadi dengan kelas yang serupa tetapi lebih besar di bagian lain dari kode Anda.

Karl Bielefeldt
sumber
Saya merasa aneh bahwa diskusi tentang mutabilitas gagal mengenali hubungan antara immutabilitas dan cara-cara di mana suatu objek dapat diekspos atau dibagikan dengan aman. Referensi ke objek kelas yang sangat tidak dapat diubah dapat dengan aman dibagikan dengan kode yang tidak dipercaya. Referensi ke turunan dari kelas yang dapat berubah dapat dibagikan jika semua orang yang memiliki referensi dapat dipercaya untuk tidak pernah memodifikasi objek atau memaparkannya ke kode yang mungkin melakukannya. Referensi ke instance kelas yang mungkin bermutasi umumnya tidak boleh dibagikan sama sekali. Jika hanya satu referensi ke suatu objek ada di mana saja di alam semesta ...
supercat
... dan tidak ada yang mempertanyakan "kode hash identitas" -nya, menggunakannya untuk mengunci, atau mengakses " Objectfitur tersembunyi" -nya , kemudian mengubah suatu objek secara langsung tidak ada bedanya dengan menimpa referensi dengan referensi ke objek baru yang sebelumnya identik tetapi untuk perubahan yang ditunjukkan. Salah satu kelemahan semantik terbesar di Jawa, IMHO, adalah bahwa ia tidak memiliki cara kode yang dapat menunjukkan bahwa variabel hanya satu-satunya referensi non-sesaat untuk sesuatu; referensi hanya boleh diloloskan ke metode yang tidak mungkin menyimpan salinan setelah mereka kembali.
supercat
0

Saya mungkin memiliki pandangan tingkat terlalu rendah tentang hal ini dan kemungkinan karena saya menggunakan C dan C ++ yang tidak persis membuatnya begitu mudah untuk membuat semuanya tidak berubah, tapi saya melihat tipe data yang tidak dapat diubah sebagai detail optimasi untuk menulis lebih banyak fungsi efisien tanpa efek samping dan dapat dengan mudah memberikan fitur seperti sistem undo dan pengeditan non-destruktif.

Misalnya, ini bisa sangat mahal:

/// @return A new mesh whose vertices have been transformed
/// by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

... jika Meshtidak dirancang untuk menjadi struktur data yang persisten dan, sebaliknya, merupakan tipe data yang harus disalin secara penuh (yang dapat menjangkau gigabyte dalam beberapa skenario) bahkan jika semua yang akan kita lakukan adalah mengubah sebuah bagian dari itu (seperti dalam skenario di atas di mana kami hanya mengubah posisi titik).

Jadi saat itulah saya meraih kekekalan dan mendesain struktur data untuk memungkinkan bagian yang tidak dimodifikasi itu disalin dangkal dan referensi dihitung, untuk memungkinkan fungsi di atas menjadi cukup efisien tanpa harus menyalin seluruh jerat sekitar sementara masih bisa menulis berfungsi untuk bebas dari efek samping yang secara dramatis menyederhanakan keamanan benang, keselamatan pengecualian, kemampuan untuk membatalkan operasi, menerapkannya tanpa merusak, dll.

Dalam kasus saya, terlalu mahal (setidaknya dari sudut pandang produktivitas) untuk membuat semuanya tidak berubah, jadi saya menyimpannya untuk kelas yang terlalu mahal untuk menyalin sepenuhnya. Kelas-kelas itu biasanya struktur data yang kuat seperti jerat dan gambar dan saya biasanya menggunakan antarmuka yang bisa berubah untuk mengekspresikan perubahan kepada mereka melalui objek "pembangun" untuk mendapatkan salinan baru yang tidak dapat diubah. Dan saya tidak melakukan itu terlalu banyak untuk mencoba mencapai jaminan abadi di tingkat pusat kelas begitu banyak membantu saya untuk menggunakan kelas dalam fungsi yang dapat bebas dari efek samping. Keinginan saya untuk membuat Meshabadi di atas tidak secara langsung membuat jerat tidak dapat diubah, tetapi untuk memungkinkan dengan mudah menulis fungsi bebas dari efek samping yang menginputkan mesh dan menghasilkan yang baru tanpa membayar memori besar dan biaya komputasi sebagai imbalan.

Sebagai hasilnya, saya hanya memiliki 4 tipe data yang tidak dapat diubah di seluruh basis kode saya, dan mereka semua struktur data yang besar, tetapi saya sangat menggunakannya untuk membantu saya menulis fungsi yang bebas dari efek samping. Jawaban ini mungkin berlaku jika, seperti saya, Anda bekerja dalam bahasa yang tidak membuatnya begitu mudah untuk membuat semuanya tidak berubah. Dalam hal ini, Anda dapat mengalihkan fokus Anda untuk membuat sebagian besar fungsi Anda menghindari efek samping, pada titik mana Anda mungkin ingin membuat struktur data terpilih tidak berubah, tipe PDS, sebagai detail optimisasi untuk menghindari salinan lengkap yang mahal. Sementara itu ketika saya memiliki fungsi seperti ini:

/// @return The v1 * v2.
Vector3f vec_mul(Vector3f v1, Vector3f v2);

... maka saya tidak memiliki godaan untuk membuat vektor tidak dapat diubah karena mereka cukup murah untuk hanya menyalin secara penuh. Tidak ada keuntungan kinerja yang diperoleh di sini dengan mengubah Vektor menjadi struktur yang tidak dapat diubah yang dapat menyalin bagian yang tidak dimodifikasi dengan dangkal. Biaya seperti itu akan lebih besar daripada biaya hanya menyalin seluruh vektor.


sumber
-1
Foo a = new Foo();
a.setProperty1("asdf");
a.setProperty2("bcde");

Jika Foo hanya memiliki dua properti, maka mudah untuk menulis konstruktor yang membutuhkan dua argumen. Tapi misalkan Anda menambahkan properti lain. Maka Anda harus menambahkan konstruktor lain:

public Foo(String a, String b, SomethingElse c)

Pada titik ini, masih dapat dikelola. Tetapi bagaimana jika ada 10 properti? Anda tidak ingin konstruktor dengan 10 argumen. Anda bisa menggunakan pola builder untuk membuat instance, tetapi itu menambah kompleksitas. Pada saat ini, Anda akan berpikir "mengapa saya tidak menambahkan setter untuk semua properti seperti yang dilakukan orang normal"?

Mike Baranczak
sumber
4
Apakah konstruktor dengan sepuluh argumen lebih buruk daripada NullPointerException yang terjadi ketika property7 tidak disetel? Apakah pola pembangun lebih kompleks daripada kode untuk memeriksa properti yang tidak diinisialisasi dalam setiap metode? Lebih baik periksa sekali ketika objek dibangun.
kevin cline
2
Seperti yang dikatakan beberapa dekade yang lalu tepatnya untuk kasus ini, "Jika Anda memiliki prosedur dengan sepuluh parameter, Anda mungkin melewatkan beberapa."
9000
1
Mengapa Anda tidak menginginkan konstruktor dengan 10 argumen? Pada titik apa Anda menggambar garis? Saya pikir konstruktor dengan 10 argumen adalah harga kecil untuk membayar manfaat dari ketidakberdayaan. Selain itu, Anda memiliki satu baris dengan 10 hal yang dipisahkan oleh koma (secara opsional menyebar ke beberapa baris atau bahkan 10 baris, jika terlihat lebih baik), atau Anda memiliki 10 baris saat Anda mengatur setiap properti secara individual ...
Ricket
1
@Ricket: Karena meningkatkan risiko menempatkan argumen dalam urutan yang salah . Jika Anda menggunakan setter, atau pola builder, itu sangat tidak mungkin.
Mike Baranczak
4
Jika Anda memiliki konstruktor dengan 10 argumen, mungkin sudah waktunya untuk meringkas argumen-argumen tersebut ke dalam kelas mereka sendiri dan / atau melihat secara kritis desain kelas Anda.
Adam Lear