Ketika merancang kelas untuk menampung model data Anda, saya sudah membacanya dapat berguna untuk membuat objek yang tidak dapat diubah tetapi pada titik apa beban daftar parameter konstruktor dan salinan yang dalam menjadi terlalu banyak dan Anda harus meninggalkan batasan yang tidak dapat diubah?
Sebagai contoh, ini adalah kelas yang tidak dapat diubah untuk merepresentasikan sesuatu yang bernama (Saya menggunakan sintaksis C # tetapi prinsipnya berlaku untuk semua bahasa OO)
class NamedThing
{
private string _name;
public NamedThing(string name)
{
_name = name;
}
public NamedThing(NamedThing other)
{
this._name = other._name;
}
public string Name
{
get { return _name; }
}
}
Hal-hal yang dinamai dapat dikonstruksikan, ditanyakan dan disalin ke hal-hal yang dinamai baru tetapi namanya tidak dapat diubah.
Ini semua baik tetapi apa yang terjadi ketika saya ingin menambahkan atribut lain? Saya harus menambahkan parameter ke konstruktor dan memperbarui konstruktor salin; yang tidak terlalu banyak bekerja tetapi masalahnya mulai, sejauh yang saya bisa lihat, ketika saya ingin membuat objek yang kompleks tidak dapat diubah.
Jika kelas berisi atribut may dan koleksi, berisi kelas kompleks lainnya, menurut saya daftar parameter konstruktor akan menjadi mimpi buruk.
Jadi pada titik apa kelas menjadi terlalu rumit untuk tidak dapat diubah?
Jawaban:
Kapan mereka menjadi beban? Sangat cepat (khususnya jika bahasa pilihan Anda tidak memberikan dukungan sintaksis yang cukup untuk kekekalan.)
Kekekalan dijual sebagai peluru perak untuk dilema multi-inti dan semua itu. Tetapi kekekalan dalam sebagian besar bahasa OO memaksa Anda untuk menambahkan artefak dan praktik buatan dalam model dan proses Anda. Untuk setiap kelas yang tidak dapat diubah kompleks Anda harus memiliki pembangun yang sama-sama kompleks (setidaknya secara internal). Tidak peduli bagaimana Anda mendesainnya, itu tetap memperkenalkan kopling yang kuat (jadi kami lebih baik memiliki alasan yang baik untuk memperkenalkannya.)
Hal ini belum tentu memungkinkan untuk memodelkan semuanya dalam kelas kecil non-kompleks. Jadi untuk kelas dan struktur besar, kami membuat partisi secara artifisial - bukan karena hal itu masuk akal dalam model domain kami, tetapi karena kami harus berurusan dengan instantiasi dan pembangunnya yang rumit dalam kode.
Lebih buruk lagi ketika orang mengambil gagasan tentang ketidakbergantungan terlalu jauh dalam bahasa tujuan umum seperti Java atau C #, membuat semuanya tidak berubah. Kemudian, sebagai hasilnya, Anda melihat orang-orang memaksa konstruksi s-ekspresi dalam bahasa yang tidak mendukung hal-hal seperti itu dengan mudah.
Rekayasa adalah tindakan pemodelan melalui kompromi dan pertukaran. Membuat semuanya tidak dapat diubah oleh dekrit karena seseorang membaca bahwa semuanya tidak dapat diubah dalam bahasa fungsional X atau Y (model pemrograman yang sama sekali berbeda), yang tidak dapat diterima. Itu bukan rekayasa yang bagus.
Hal-hal kecil, mungkin yang bersifat kesatuan, dapat dibuat tidak berubah. Hal-hal yang lebih kompleks dapat dibuat tidak berubah ketika itu masuk akal . Tapi ketetapan bukan peluru perak. Kemampuan untuk mengurangi bug, untuk meningkatkan skalabilitas dan kinerja, itu bukan satu-satunya fungsi kekekalan. Ini adalah fungsi dari praktik rekayasa yang tepat . Lagi pula, orang-orang telah menulis perangkat lunak yang bagus dan dapat diukur tanpa cacat.
Kekekalan menjadi beban yang sangat cepat (menambah kompleksitas yang tidak disengaja) jika dilakukan tanpa alasan, ketika dilakukan di luar apa yang masuk akal dalam konteks model domain.
Saya, untuk satu, mencoba menghindarinya (kecuali saya bekerja dalam bahasa pemrograman dengan dukungan sintaksis yang baik untuk itu.)
sumber
Saya melalui fase bersikeras bahwa kelas tidak dapat diubah jika memungkinkan. Apakah pembangun untuk hampir semuanya, array tidak berubah, dll, dll. Saya menemukan jawaban untuk pertanyaan Anda sederhana: Pada titik apa kelas tidak berubah menjadi beban? Sangat cepat. Begitu Anda ingin membuat serial sesuatu, Anda harus dapat deserialize, yang berarti harus bisa diubah; segera setelah Anda ingin menggunakan ORM, sebagian besar dari mereka bersikeras bahwa properti bisa berubah. Dan seterusnya.
Saya akhirnya mengganti kebijakan itu dengan antarmuka yang tidak dapat diubah ke objek yang bisa berubah.
Sekarang objek memiliki fleksibilitas tetapi Anda masih bisa memberi tahu kode panggilan bahwa itu tidak boleh mengedit properti ini.
sumber
IComparable<T>
menjamin bahwa jikaX.CompareTo(Y)>0
danY.CompareTo(Z)>0
, kemudianX.CompareTo(Z)>0
. Antarmuka memiliki kontrak . Jika kontrak untukIImmutableList<T>
menentukan bahwa nilai semua item dan properti harus "ditetapkan" sebelum contoh apa pun diekspos ke dunia luar, maka semua implementasi yang sah akan melakukannya. Tidak ada yang mencegahIComparable
implementasi melanggar transitivitas, tetapi implementasi yang melakukannya tidak sah. JikaSortedDictionary
kerusakan saat diberi tidak sahIComparable
, ...IReadOnlyList<T>
akan berubah, mengingat bahwa (1) ada ketentuan tersebut dinyatakan dalam dokumentasi antarmuka, dan (2) pelaksanaan yang paling umum,List<T>
, bahkan tidak hanya-baca ? Saya tidak begitu jelas apa yang mendua tentang istilah saya: koleksi dapat dibaca jika data di dalamnya dapat dibaca. Ini hanya-baca jika dapat menjanjikan bahwa data yang terkandung tidak dapat diubah kecuali beberapa referensi eksternal dipegang oleh kode yang akan mengubahnya. Itu abadi jika dapat menjamin bahwa itu tidak dapat diubah, titik.Saya kira tidak ada jawaban umum untuk ini. Semakin kompleks suatu kelas, semakin sulit untuk berpikir tentang perubahan negaranya, dan semakin mahal untuk membuat salinan baru darinya. Jadi di atas beberapa tingkat kompleksitas (pribadi) itu akan menjadi terlalu menyakitkan untuk membuat / menjaga kelas tetap.
Perhatikan bahwa kelas yang terlalu kompleks, atau daftar parameter metode panjang adalah desain bau per se, terlepas dari keabadian.
Jadi biasanya solusi yang disukai adalah memecah kelas seperti itu menjadi beberapa kelas yang berbeda, yang masing-masing dapat dibuat bisa berubah atau berubah dengan sendirinya. Jika ini tidak layak, itu bisa berubah bisa berubah.
sumber
Anda dapat menghindari masalah penyalinan jika Anda menyimpan semua bidang yang tidak dapat diubah di bagian dalam
struct
. Ini pada dasarnya adalah variasi dari pola kenang-kenangan. Kemudian ketika Anda ingin membuat salinan, cukup salin kenang-kenangannya:sumber
Anda memiliki beberapa hal yang sedang bekerja di sini. Kumpulan data yang tidak berubah sangat bagus untuk skalabilitas multithreaded. Pada dasarnya, Anda dapat sedikit mengoptimalkan memori Anda sehingga satu set parameter adalah satu instance dari kelas - di mana saja. Karena objek tidak pernah berubah, Anda tidak perlu khawatir tentang sinkronisasi saat mengakses anggotanya. Itu hal yang baik. Namun, seperti yang Anda tunjukkan, semakin kompleks objek semakin Anda membutuhkan beberapa sifat berubah-ubah. Saya akan mulai dengan alasan seperti ini:
Dalam bahasa yang hanya mendukung objek yang tidak dapat diubah (seperti Erlang), jika ada operasi yang tampaknya mengubah keadaan objek yang tidak dapat diubah, hasil akhirnya adalah salinan baru dari objek dengan nilai yang diperbarui. Misalnya, ketika Anda menambahkan item ke vektor / daftar:
Itu bisa menjadi cara yang waras untuk bekerja dengan objek yang lebih rumit. Ketika Anda menambahkan simpul pohon misalnya, hasilnya adalah pohon baru dengan simpul yang ditambahkan. Metode dalam contoh di atas mengembalikan daftar baru. Dalam contoh dalam paragraf ini
tree.add(newNode)
akan mengembalikan pohon baru dengan simpul yang ditambahkan. Bagi pengguna, ini menjadi mudah untuk dikerjakan. Bagi para penulis perpustakaan menjadi membosankan ketika bahasa tidak mendukung penyalinan implisit. Ambang itu terserah kesabaran Anda sendiri. Untuk pengguna perpustakaan Anda, batas paling waras yang saya temukan adalah sekitar tiga hingga empat parameter teratas.sumber
Jika Anda memiliki beberapa anggota kelas akhir dan tidak ingin mereka diekspos ke semua objek yang perlu membuatnya, Anda dapat menggunakan pola builder:
keuntungannya adalah Anda dapat dengan mudah membuat objek baru dengan nilai yang berbeda dari nama yang berbeda.
sumber
newObject
.NamedThing
dalam hal ini)Builder
digunakan kembali, ada risiko nyata dari hal yang terjadi yang saya sebutkan. Seseorang mungkin sedang membangun banyak objek dan memutuskan bahwa karena sebagian besar propertinya sama, untuk digunakan kembaliBuilder
, dan pada kenyataannya, mari kita menjadikannya singleton global yang disuntikkan ketergantungan! Aduh. Bug utama diperkenalkan. Jadi saya pikir pola campuran instantiated vs statis ini buruk.Menurut pendapat saya, tidak ada gunanya membuat kelas kecil tidak dapat diubah dalam bahasa seperti yang Anda tampilkan. Saya menggunakan kecil di sini dan tidak kompleks , karena bahkan jika Anda menambahkan sepuluh bidang ke kelas itu dan itu benar-benar suka operasi pada mereka, saya ragu itu akan mengambil kilobyte apalagi megabyte apalagi gigabyte, jadi fungsi apa pun menggunakan instance dari Anda kelas dapat dengan mudah membuat salinan murah dari seluruh objek untuk menghindari memodifikasi yang asli jika ingin menghindari menyebabkan efek samping eksternal.
Struktur Data Persisten
Di mana saya menemukan penggunaan pribadi untuk kekekalan adalah untuk struktur data pusat yang besar yang mengagregasi sekelompok kecil data seperti contoh dari kelas yang Anda tampilkan, seperti yang menyimpan sejuta
NamedThings
. Dengan menjadi bagian dari struktur data persisten yang tidak dapat diubah dan berada di belakang antarmuka yang hanya memungkinkan akses baca-saja, elemen-elemen yang menjadi milik kontainer menjadi tidak dapat diubah tanpa kelas elemen (NamedThing
) yang harus menghadapinya.Salinan Murah
Struktur data yang persisten memungkinkan bagian-bagiannya ditransformasi dan dibuat unik, menghindari modifikasi ke aslinya tanpa harus menyalin struktur data secara keseluruhan. Itulah keindahan yang sebenarnya. Jika Anda ingin secara naif menulis fungsi yang menghindari efek samping yang menginput struktur data yang membutuhkan gigabytes memori dan hanya memodifikasi memori senilai satu megabyte, maka Anda harus menyalin seluruh hal aneh untuk menghindari menyentuh input dan mengembalikan yang baru keluaran. Baik menyalin gigabyte untuk menghindari efek samping atau menyebabkan efek samping dalam skenario itu, membuat Anda harus memilih di antara dua pilihan yang tidak menyenangkan.
Dengan struktur data yang persisten, ini memungkinkan Anda untuk menulis fungsi seperti itu dan menghindari membuat salinan dari seluruh struktur data, hanya membutuhkan sekitar satu megabita memori ekstra untuk output jika fungsi Anda hanya mengubah nilai memori satu megabita.
Beban
Adapun beban, ada yang langsung setidaknya dalam kasus saya. Saya perlu pembangun yang dibicarakan orang atau "transien" saat saya memanggil mereka untuk dapat secara efektif mengekspresikan transformasi pada struktur data besar itu tanpa menyentuhnya. Kode seperti ini:
... maka harus ditulis seperti ini:
Tetapi sebagai ganti dari dua baris kode tambahan, fungsi sekarang aman untuk memanggil utas dengan daftar asli yang sama, itu menyebabkan tidak ada efek samping, dll. Ini juga membuatnya sangat mudah untuk membuat operasi ini menjadi tindakan pengguna yang tidak dapat diurungkan karena batalkan hanya dapat menyimpan salinan murah dari daftar lama.
Pengecualian-Keamanan atau Pemulihan Kesalahan
Tidak semua orang dapat mengambil manfaat sebanyak yang saya lakukan dari struktur data yang persisten dalam konteks seperti ini (saya menemukan banyak kegunaannya dalam sistem undo dan pengeditan non-destruktif yang merupakan konsep sentral dalam domain VFX saya), tetapi satu hal yang dapat diterapkan pada hampir semua semua orang untuk dipertimbangkan adalah pengecualian-keselamatan atau pemulihan kesalahan .
Jika Anda ingin membuat fungsi mutasi yang asli pengecualian-aman, maka itu memerlukan logika rollback, yang untuk itu implementasi paling sederhana memerlukan menyalin seluruh daftar:
Pada titik ini versi pengecualian-aman yang dapat diubah bahkan lebih mahal secara komputasi dan bahkan lebih sulit untuk menulis dengan benar daripada versi yang tidak dapat diubah menggunakan "builder". Dan banyak pengembang C ++ mengabaikan keamanan-pengecualian dan mungkin itu baik-baik saja untuk domain mereka, tetapi dalam kasus saya saya ingin memastikan kode saya berfungsi dengan benar bahkan jika ada pengecualian (bahkan menulis tes yang dengan sengaja melemparkan pengecualian untuk menguji pengecualian) keamanan), dan itu membuatnya jadi saya harus bisa mengembalikan semua efek samping yang menyebabkan fungsi setengah jalan jika ada sesuatu yang dilemparkan.
Ketika Anda ingin menjadi pengecualian-aman dan pulih dari kesalahan dengan anggun tanpa aplikasi Anda mogok dan terbakar, maka Anda harus mengembalikan / membatalkan segala efek samping yang dapat disebabkan oleh suatu fungsi jika terjadi kesalahan / pengecualian. Dan di sana pembangun sebenarnya dapat menghemat lebih banyak waktu programmer daripada biaya seiring dengan waktu komputasi karena: ...
Jadi kembali ke pertanyaan mendasar:
Mereka selalu menjadi beban dalam bahasa yang lebih banyak berkisar seputar mutabilitas daripada ketidakmampuan, itulah sebabnya saya pikir Anda harus menggunakannya di mana manfaatnya jauh lebih besar daripada biayanya. Tetapi pada tingkat yang cukup luas untuk struktur data yang cukup besar, saya percaya ada banyak kasus di mana itu merupakan trade-off yang layak.
Juga di tambang, saya hanya memiliki beberapa tipe data yang tidak dapat diubah, dan semuanya merupakan struktur data besar yang dimaksudkan untuk menyimpan sejumlah besar elemen (piksel gambar / tekstur, entitas dan komponen ECS, dan simpul / tepi / poligon dari sebuah jaring).
sumber