Jangan mendeklarasikan antarmuka untuk objek yang tidak dapat diubah

27

Jangan mendeklarasikan antarmuka untuk objek yang tidak dapat diubah

[EDIT] Di mana objek yang dimaksud mewakili Data Transfer Objects (DTOs) atau Data Lama Biasa (PODs)

Apakah itu pedoman yang masuk akal?

Hingga kini, saya sering membuat antarmuka untuk kelas tersegel yang tidak dapat diubah (data tidak dapat diubah). Saya sudah mencoba berhati-hati untuk tidak menggunakan antarmuka di mana pun saya peduli tentang ketidakberubahan.

Sayangnya, antarmuka mulai meresapi kode (dan bukan hanya kode saya yang saya khawatirkan). Anda akhirnya lulus antarmuka, dan kemudian ingin meneruskannya ke beberapa kode yang benar-benar ingin mengasumsikan bahwa hal yang diteruskan ke itu tidak dapat diubah.

Karena masalah ini, saya mempertimbangkan untuk tidak pernah mendeklarasikan antarmuka untuk objek yang tidak dapat diubah.

Ini mungkin memiliki konsekuensi sehubungan dengan Unit Testing, tetapi selain itu, apakah ini tampak sebagai pedoman yang masuk akal?

Atau apakah ada pola lain yang harus saya gunakan untuk menghindari masalah "penyebaran antarmuka" yang saya lihat?

(Saya menggunakan objek yang tidak dapat diubah ini karena beberapa alasan: Terutama untuk keamanan utas karena saya menulis banyak kode multi-utas; tetapi juga karena itu berarti saya dapat menghindari membuat salinan defensif objek yang diteruskan ke metode. Kode menjadi lebih sederhana dalam banyak kasus ketika Anda mengetahui sesuatu tidak dapat diubah - yang tidak Anda lakukan jika Anda telah diberi antarmuka. Bahkan, seringkali Anda bahkan tidak dapat membuat salinan defensif dari objek yang dirujuk melalui antarmuka jika itu tidak memberikan operasi klon atau cara serialisasi ...)

[EDIT]

Untuk memberikan lebih banyak konteks karena alasan saya ingin membuat objek tidak berubah, lihat posting blog ini dari Eric Lippert:

http://blogs.msdn.com/b/ericlippert/archive/tags/immutability/

Saya juga harus menunjukkan bahwa saya sedang bekerja dengan beberapa konsep tingkat rendah di sini, seperti item yang sedang dimanipulasi / diedarkan dalam antrian pekerjaan multi-threaded. Ini pada dasarnya adalah DTO.

Juga Joshua Bloch merekomendasikan penggunaan benda-benda abadi dalam bukunya Java Efektif .


Mengikuti

Terima kasih atas umpan baliknya, semuanya. Saya telah memutuskan untuk terus maju dan menggunakan pedoman ini untuk DTO dan sejenisnya. Sejauh ini sudah bekerja dengan baik, tapi baru seminggu ... Tetap saja, ini terlihat bagus.

Ada beberapa masalah lain yang berkaitan dengan ini yang ingin saya tanyakan; terutama sesuatu yang saya sebut "Kerusakan Dalam atau Dangkal" (nomenklatur yang saya curi dari kloning Deep dan Dangkal) - tapi itu pertanyaan lain kali.

Matthew Watson
sumber
3
+1 pertanyaan bagus. Tolong jelaskan mengapa sangat penting bagi Anda bahwa objeknya adalah kelas yang tidak dapat diubah itu daripada sesuatu yang hanya mengimplementasikan antarmuka yang sama. Mungkin saja masalah Anda berakar di tempat lain. Ketergantungan Injeksi sangat penting untuk pengujian unit, tetapi memiliki banyak manfaat penting lainnya yang semuanya hilang jika Anda memaksakan kode Anda untuk memerlukan kelas abadi tertentu.
Steven Doggart
1
Saya sedikit bingung dengan penggunaan istilah tidak berubah . Untuk memperjelas, maksud Anda kelas disegel, yang tidak dapat diwarisi dan diganti, atau apakah Anda maksud bahwa data yang diekspos adalah hanya-baca dan tidak dapat diubah setelah objek dibuat. Dengan kata lain, apakah Anda ingin menjamin bahwa objek adalah tipe tertentu, atau nilainya tidak akan pernah berubah. Dalam komentar pertama saya, saya berasumsi berarti yang pertama, tetapi sekarang dengan suntingan Anda, sepertinya yang terakhir. Atau apakah Anda khawatir dengan keduanya?
Steven Doggart
3
Saya bingung, mengapa tidak meninggalkan setter dari antarmuka? Tetapi di sisi lain, saya bosan melihat antarmuka untuk objek yang benar-benar objek domain dan / atau DTO yang membutuhkan pabrik untuk objek-objek itu ... menyebabkan disonansi kognitif bagi saya.
Michael Brown
2
Bagaimana antarmuka untuk kelas yang semua berubah? Sebagai contoh, Jawa Jumlah kelas memungkinkan seseorang untuk menentukan List<Number>yang dapat menampung Integer, Float, Long, BigDecimal, dll ... Semua yang berubah sendiri.
1
@ MikeBrown Saya kira itu hanya karena antarmuka menyiratkan kekekalan tidak berarti implementasi menegakkannya. Anda bisa menyerahkannya ke konvensi (yaitu mendokumentasikan antarmuka bahwa ketidakbergantungan adalah persyaratan) tetapi Anda bisa berakhir dengan beberapa masalah yang benar-benar buruk jika seseorang melanggar konvensi.
vaughandroid

Jawaban:

17

Menurut pendapat saya, aturan Anda adalah yang baik (atau setidaknya itu bukan yang buruk), tetapi hanya karena situasi yang Anda gambarkan. Saya tidak akan mengatakan bahwa saya setuju dengan itu dalam semua situasi, jadi, dari sudut pandang pedant batin saya, saya harus mengatakan aturan Anda secara teknis terlalu luas.

Biasanya Anda tidak akan mendefinisikan objek tidak berubah kecuali mereka pada dasarnya digunakan sebagai objek transfer data (DTO), yang berarti bahwa mereka berisi properti data tetapi sangat sedikit logika dan tidak ada ketergantungan. Jika itu masalahnya, seperti yang terlihat di sini, saya akan mengatakan Anda aman untuk menggunakan jenis beton secara langsung daripada antarmuka.

Saya yakin akan ada beberapa puritan unit-test yang akan tidak setuju, tetapi menurut saya, kelas DTO dapat dengan aman dikeluarkan dari persyaratan unit-testing dan dependensi-injeksi. Tidak perlu menggunakan pabrik untuk membuat DTO, karena tidak memiliki ketergantungan. Jika semuanya membuat DTO secara langsung sesuai kebutuhan, maka sebenarnya tidak ada cara untuk menyuntikkan tipe yang berbeda, jadi tidak perlu antarmuka. Dan karena mereka tidak mengandung logika, tidak ada yang perlu diuji unit. Bahkan jika mereka memang mengandung beberapa logika, selama mereka tidak memiliki dependensi, maka itu harus sepele untuk unit-test logika, jika perlu.

Karena itu, saya pikir membuat aturan bahwa semua kelas DTO tidak akan mengimplementasikan antarmuka, meskipun berpotensi tidak perlu, tidak akan mengganggu desain perangkat lunak Anda. Karena Anda memiliki persyaratan bahwa data harus tetap, dan Anda tidak dapat memaksakannya melalui antarmuka, maka saya akan mengatakan itu benar-benar sah untuk menetapkan aturan itu sebagai standar pengkodean.

Masalah yang lebih besar, adalah kebutuhan untuk secara ketat menegakkan lapisan DTO yang bersih. Selama kelas Anda yang tidak berubah antarmuka hanya ada di lapisan DTO, dan lapisan DTO Anda tetap bebas dari logika dan dependensi, maka Anda akan aman. Jika Anda mulai mencampur lapisan Anda, dan Anda memiliki kelas tanpa antarmuka yang berfungsi ganda sebagai kelas lapisan bisnis, maka saya pikir Anda akan mulai mengalami banyak masalah.

Steven Doggart
sumber
Kode yang mengharapkan secara khusus untuk berurusan dengan DTO atau objek nilai yang tidak berubah harus menggunakan fitur tipe itu daripada antarmuka, tetapi itu tidak berarti tidak akan ada kasus penggunaan untuk antarmuka yang diimplementasikan oleh kelas seperti itu dan juga oleh kelas lain, dengan metode yang menjanjikan untuk mengembalikan nilai yang akan valid untuk beberapa waktu minimum (misalnya jika antarmuka dilewatkan ke fungsi, metode harus mengembalikan nilai yang valid hingga fungsi itu kembali). Antarmuka seperti itu dapat membantu jika ada sejumlah jenis yang merangkum data serupa dan satu keinginan ...
supercat
... untuk memiliki cara menyalin data dari satu ke yang lain. Jika semua jenis yang termasuk data tertentu mengimplementasikan antarmuka yang sama untuk membacanya, maka data tersebut dapat disalin dengan mudah di antara semua jenis, tanpa mengharuskan masing-masing mengetahui cara mengimpor dari masing-masing yang lain.
supercat
Pada titik itu, Anda juga bisa menghilangkan getter Anda (dan setters, jika DTO Anda bisa berubah karena alasan bodoh) dan membuat bidang publik. Anda tidak akan pernah menempatkan logika di sana, kan?
Kevin
@Kevin saya kira, tetapi dengan kenyamanan modern dari properti otomatis, sangat mudah untuk membuatnya menjadi properti, mengapa tidak? Saya kira jika kinerja dan efisiensi menjadi perhatian utama maka mungkin itu penting. Bagaimanapun, pertanyaannya adalah, level "logika" apa yang Anda izinkan dalam DTO? Bahkan jika Anda memasukkan beberapa logika validasi di propertinya, tidak akan ada masalah untuk mengujinya asalkan tidak memiliki dependensi yang akan memerlukan ejekan. Selama DTO tidak menggunakan objek bisnis ketergantungan apa pun, maka aman untuk memiliki sedikit logika yang dibangun ke dalamnya karena mereka masih akan dapat diuji.
Steven Doggart
Secara pribadi, saya lebih memilih untuk menjaga mereka sebersih mungkin dari hal-hal semacam itu, tetapi selalu ada waktu ketika membengkokkan aturan diperlukan dan jadi lebih baik membiarkan pintu terbuka untuk berjaga-jaga.
Steven Doggart
7

Saya biasa membuat keributan besar tentang membuat kode saya kebal terhadap penyalahgunaan. Saya membuat antarmuka read-only untuk menyembunyikan anggota yang bermutasi, menambahkan banyak kendala pada tanda tangan generik saya, dll., Ternyata sebagian besar waktu saya membuat keputusan desain karena saya tidak mempercayai rekan kerja imajiner saya. "Mungkin suatu hari nanti mereka akan merekrut pria entry-level baru dan dia tidak akan tahu bahwa kelas XYZ tidak dapat memperbarui DTO ABC. Oh tidak!" Di lain waktu saya berfokus pada masalah yang salah - mengabaikan solusi yang jelas - tidak melihat hutan melalui pepohonan.

Saya tidak pernah membuat antarmuka untuk DTO saya lagi. Saya bekerja dengan asumsi bahwa orang yang menyentuh kode saya (terutama saya sendiri) tahu apa yang diperbolehkan dan apa yang masuk akal. Jika saya terus membuat kesalahan bodoh yang sama, saya biasanya tidak mencoba mengeraskan antarmuka saya. Sekarang saya menghabiskan sebagian besar waktu saya untuk mencoba memahami mengapa saya terus membuat kesalahan yang sama. Biasanya karena saya terlalu menganalisis sesuatu atau melewatkan konsep kunci. Kode saya jauh lebih mudah untuk dikerjakan sejak saya menyerah menjadi paranoid. Saya juga berakhir dengan lebih sedikit "kerangka kerja" yang membutuhkan pengetahuan orang dalam untuk bekerja pada sistem.

Kesimpulan saya adalah menemukan hal paling sederhana yang berhasil. Kompleksitas tambahan membuat antarmuka yang aman hanya membuang waktu pengembangan dan menyulitkan kode sederhana. Khawatir tentang hal-hal seperti ini ketika Anda memiliki 10.000 pengembang menggunakan perpustakaan Anda. Percayalah, itu akan menyelamatkan Anda dari banyak ketegangan yang tidak perlu.

Taman Travis
sumber
Saya biasanya menyimpan antarmuka ketika saya membutuhkan injeksi ketergantungan untuk pengujian unit.
Travis Parks
5

Sepertinya panduan yang oke, tapi untuk alasan aneh. Saya sudah memiliki sejumlah tempat di mana antarmuka (atau kelas dasar abstrak) menyediakan akses seragam ke serangkaian objek yang tidak dapat diubah. Strategi cenderung jatuh di sini. Benda-benda negara cenderung jatuh di sini. Saya tidak berpikir itu terlalu tidak masuk akal untuk membentuk antarmuka agar tampak tidak berubah dan mendokumentasikannya di API Anda.

Yang mengatakan, orang cenderung over-antarmuka objek Data Lama Plain (selanjutnya, POD), dan bahkan struktur sederhana (sering tidak berubah). Jika kode Anda tidak memiliki alternatif yang waras untuk beberapa struktur fundamental, ia tidak memerlukan antarmuka. Tidak, pengujian unit bukan alasan yang cukup untuk mengubah desain Anda (mengejek akses database bukan alasan Anda menyediakan antarmuka untuk itu, itu adalah fleksibilitas untuk perubahan di masa mendatang) - itu bukan akhir dunia jika pengujian Anda gunakan struktur dasar dasar apa adanya.

Telastyn
sumber
2

Kode menjadi jauh lebih sederhana dalam banyak kasus ketika Anda tahu ada sesuatu yang tidak dapat diubah - yang tidak Anda lakukan jika Anda diberi antarmuka.

Saya pikir itu bukan masalah yang harus dikhawatirkan oleh pelakunya. Jika interface Xdimaksudkan untuk tidak berubah, bukankah itu tanggung jawab pelaksana antarmuka untuk memastikan bahwa mereka mengimplementasikan antarmuka dengan cara yang tidak berubah?

Namun, dalam pikiran saya, tidak ada yang namanya antarmuka tidak dapat diubah - kontrak kode yang ditentukan oleh antarmuka hanya berlaku untuk metode objek yang terbuka, dan tidak ada apa pun tentang internal objek.

Jauh lebih umum untuk melihat imutabilitas diimplementasikan sebagai dekorator daripada antarmuka, tetapi kelayakan solusi itu benar-benar tergantung pada struktur objek Anda dan kompleksitas implementasi Anda.

Jonathan Rich
sumber
1
Bisakah Anda memberikan contoh penerapan imutabilitas sebagai dekorator?
vaughandroid
Tidak sepele, saya tidak berpikir, tetapi ini adalah latihan pemikiran yang relatif sederhana - jika MutableObjectmemiliki nmetode yang mengubah keadaan dan mmetode yang mengembalikan keadaan, ImmutableDecoratordapat terus mengekspos metode yang mengembalikan keadaan ( m), dan, tergantung pada lingkungan, menegaskan atau melempar pengecualian ketika salah satu metode yang bisa diubah dipanggil.
Jonathan Rich
Saya benar-benar tidak suka mengubah kepastian waktu kompilasi menjadi kemungkinan pengecualian run-time ...
Matthew Watson
OK, tapi bagaimana ImmutableDecorator bisa tahu jika metode yang diberikan berubah atau tidak?
vaughandroid
Anda tidak dapat memastikan dalam hal apa pun jika kelas yang mengimplementasikan antarmuka Anda tidak dapat diubah sehubungan dengan metode yang ditentukan oleh antarmuka Anda. @ Baqueta Dekorator harus memiliki pengetahuan tentang implementasi kelas dasar.
Jonathan Rich
0

Anda akhirnya lulus antarmuka, dan kemudian ingin meneruskannya ke beberapa kode yang benar-benar ingin mengasumsikan bahwa hal yang diteruskan ke itu tidak dapat diubah.

Dalam C #, metode yang mengharapkan objek dari tipe "tidak dapat diubah" seharusnya tidak memiliki parameter tipe antarmuka karena antarmuka C # tidak dapat menentukan kontrak immutability. Oleh karena itu, pedoman yang Anda usulkan sendiri tidak ada artinya dalam C # karena Anda tidak dapat melakukannya sejak awal (dan versi bahasa yang akan datang tidak memungkinkan Anda melakukannya).

Pertanyaan Anda berasal dari kesalahpahaman halus artikel Eric Lippert tentang ketidakberdayaan. Eric tidak mendefinisikan antarmuka IStack<T>dan IQueue<T>untuk menentukan kontrak untuk tumpukan dan antrian yang tidak berubah. Mereka tidak melakukannya. Dia mendefinisikan mereka untuk kenyamanan. Antarmuka ini memungkinkannya untuk menentukan tipe berbeda untuk tumpukan kosong dan antrian. Kita dapat mengusulkan desain dan implementasi stack yang berbeda dengan menggunakan tipe tunggal tanpa memerlukan antarmuka atau tipe terpisah untuk mewakili stack kosong, tetapi kode yang dihasilkan tidak akan terlihat bersih dan akan sedikit kurang efisien.

Sekarang mari kita berpegang pada desain Eric. Metode yang membutuhkan stack yang tidak dapat diubah harus memiliki parameter tipe Stack<T>daripada antarmuka umum IStack<T>yang mewakili tipe data abstrak dari stack dalam arti umum. Tidak jelas bagaimana melakukan ini ketika menggunakan tumpukan Eric yang tidak bisa diubah dan dia tidak membahas ini di artikelnya, tapi itu mungkin. Masalahnya adalah dengan jenis tumpukan kosong. Anda dapat mengatasi masalah ini dengan memastikan bahwa Anda tidak pernah mendapatkan tumpukan kosong. Ini dapat dipastikan dengan mendorong nilai dummy sebagai nilai pertama pada tumpukan dan tidak pernah mengetuknya. Dengan cara ini, Anda dapat dengan aman melemparkan hasil Pushdan Popke Stack<T>.

Memiliki Stack<T>alat IStack<T>dapat bermanfaat. Anda dapat menentukan metode yang memerlukan tumpukan, tumpukan apa pun, belum tentu tumpukan yang tidak dapat diubah. Metode-metode ini dapat memiliki parameter tipe IStack<T>. Ini memungkinkan Anda untuk melewati tumpukan yang tidak bisa diubah. Idealnya, IStack<T>akan menjadi bagian dari perpustakaan standar itu sendiri. Di .NET, tidak ada IStack<T>, tetapi ada antarmuka standar lain yang dapat diimplementasikan oleh stack immutable, membuat tipe ini lebih bermanfaat.

Desain alternatif dan implementasi stack tidak berubah yang saya sebutkan sebelumnya menggunakan antarmuka yang disebut IImmutableStack<T>. Tentu saja, memasukkan "Immutable" di nama antarmuka tidak membuat setiap jenis yang mengimplementasikannya tidak dapat diubah. Namun, dalam antarmuka ini, kontrak imutabilitas hanyalah verbal. Pengembang yang baik harus menghargainya.

Jika Anda sedang mengembangkan perpustakaan internal kecil, maka Anda dapat setuju dengan semua orang di tim untuk menghormati kontrak ini dan Anda dapat menggunakan IImmutableStack<T>sebagai jenis parameter. Jika tidak, Anda tidak boleh menggunakan tipe antarmuka.

Saya ingin menambahkan, karena Anda menandai pertanyaan C #, bahwa dalam spesifikasi C #, tidak ada DTO dan POD. Oleh karena itu, membuangnya atau dengan tepat mendefinisikannya meningkatkan pertanyaan.

Hadi Brais
sumber