Katakanlah, misalnya, Anda memiliki aplikasi dengan kelas yang dibagikan secara luas User
. Kelas ini memaparkan semua informasi tentang pengguna, ID mereka, nama, tingkat akses ke setiap modul, zona waktu dll.
Data pengguna jelas banyak direferensikan di seluruh sistem, tetapi untuk alasan apa pun, sistem ini diatur sehingga alih-alih mengirimkan objek pengguna ini ke kelas yang bergantung padanya, kami hanya meneruskan properti individual darinya.
Kelas yang membutuhkan id pengguna, hanya akan memerlukan GUID userId
sebagai parameter, kadang-kadang kita mungkin memerlukan nama pengguna juga, sehingga diteruskan sebagai parameter terpisah. Dalam beberapa kasus, ini diteruskan ke metode individual, sehingga nilai tidak dipegang di tingkat kelas sama sekali.
Setiap kali saya membutuhkan akses ke informasi yang berbeda dari kelas Pengguna, saya harus melakukan perubahan dengan menambahkan parameter dan di mana menambahkan kelebihan baru tidak sesuai, saya harus mengubah setiap referensi ke metode atau konstruktor kelas juga.
Pengguna hanyalah satu contoh. Ini banyak dipraktikkan dalam kode kita.
Apakah saya benar dalam berpikir bahwa ini merupakan pelanggaran prinsip Terbuka / Tertutup? Bukan hanya tindakan mengubah kelas yang ada, tetapi mengaturnya di tempat pertama sehingga perubahan luas sangat mungkin diperlukan di masa depan?
Jika kita baru saja melewati User
objek, saya bisa membuat perubahan kecil ke kelas yang saya kerjakan. Jika saya harus menambahkan parameter, saya mungkin harus membuat lusinan perubahan pada referensi ke kelas.
Apakah ada prinsip lain yang dilanggar oleh praktik ini? Keterbalikan inversi mungkin? Meskipun kami tidak mereferensikan abstraksi, hanya ada satu jenis pengguna, jadi tidak ada kebutuhan nyata untuk memiliki antarmuka pengguna.
Apakah ada prinsip non-SOLID lainnya yang dilanggar, seperti prinsip dasar pemrograman defensif?
Haruskah konstruktor saya terlihat seperti ini:
MyConstructor(GUID userid, String username)
Atau ini:
MyConstructor(User theUser)
Edit Posting:
Disarankan bahwa pertanyaan dijawab dalam "Pass ID atau Object?". Ini tidak menjawab pertanyaan tentang bagaimana keputusan untuk pergi dengan cara apa pun mempengaruhi upaya untuk mengikuti prinsip-prinsip SOLID, yang merupakan inti dari pertanyaan ini.
I
diSOLID
dalamnya?MyConstructor
pada dasarnya mengatakan sekarang "Aku butuh aGuid
dan astring
". Jadi mengapa tidak memiliki antarmuka yang menyediakan aGuid
dan astring
, biarkanUser
implementasikan antarmuka itu dan biarkanMyConstructor
bergantung pada instance yang mengimplementasikan antarmuka itu? Dan jika perluMyConstructor
perubahan, ubah antarmuka. - Ini sangat membantu saya untuk memikirkan antarmuka untuk "milik" konsumen daripada penyedia . Jadi pikirkan "sebagai konsumen saya butuh sesuatu yang melakukan ini dan itu" daripada "sebagai penyedia saya bisa melakukan ini dan itu".Jawaban:
Sama sekali tidak ada yang salah dengan melewatkan seluruh
User
objek sebagai parameter. Bahkan, mungkin membantu memperjelas kode Anda, dan membuatnya lebih jelas bagi programmer apa metode yang diperlukan jika tanda tangan metode memerlukan aUser
.Melewati tipe data sederhana itu bagus, sampai mereka berarti sesuatu selain apa adanya. Pertimbangkan contoh ini:
Dan contoh penggunaan:
Bisakah Anda menemukan cacat? Kompiler tidak bisa. "ID pengguna" yang diteruskan hanyalah bilangan bulat. Kami memberi nama variabel
user
tetapi menginisialisasi nilainya dariblogPostRepository
objek, yang mungkin mengembalikanBlogPost
objek, bukanUser
objek - namun kode mengkompilasi dan Anda berakhir dengan kesalahan runtime.Sekarang perhatikan contoh yang diubah ini:
Mungkin
Bar
metode ini hanya menggunakan "ID pengguna" tetapi tanda tangan metode membutuhkanUser
objek. Sekarang mari kita kembali ke contoh penggunaan yang sama seperti sebelumnya, tetapi ubah untuk melewati seluruh "pengguna" di:Sekarang kami memiliki kesalahan kompiler. The
blogPostRepository.Find
Metode mengembalikanBlogPost
objek, yang kita cerdik sebut "pengguna". Kemudian kami meneruskan "pengguna" ini keBar
metode dan segera mendapatkan kesalahan kompiler, karena kami tidak dapat meneruskanBlogPost
ke metode yang menerima aUser
.Sistem Jenis bahasa sedang ditingkatkan untuk menulis kode yang benar lebih cepat, dan mengidentifikasi cacat pada waktu kompilasi, daripada menjalankan waktu.
Sungguh, harus memperbaiki banyak kode karena perubahan informasi pengguna hanyalah gejala dari masalah lain. Dengan melewati seluruh
User
objek Anda mendapatkan manfaat di atas, selain manfaat tidak harus memperbaiki semua tanda tangan metode yang menerima informasi pengguna ketika ada sesuatu tentangUser
kelas yang berubah.sumber
Tidak, itu bukan pelanggaran prinsip itu. Prinsip itu berkaitan dengan tidak mengubah
User
cara yang mempengaruhi bagian lain dari kode yang menggunakannya. Perubahan Anda padaUser
bisa menjadi pelanggaran seperti itu, tetapi itu tidak terkait.Tidak. Apa yang Anda gambarkan - hanya menyuntikkan bagian-bagian yang diperlukan dari objek pengguna ke dalam setiap metode - adalah kebalikannya: ini adalah inversi ketergantungan murni.
Tidak. Pendekatan ini adalah cara pengkodean yang benar-benar valid. Itu tidak melanggar prinsip-prinsip seperti itu.
Tetapi inversi ketergantungan hanya merupakan prinsip; itu bukan hukum yang tidak bisa dipatahkan. Dan DI murni dapat menambah kompleksitas ke sistem. Jika Anda menemukan bahwa hanya menyuntikkan nilai-nilai pengguna yang diperlukan ke dalam metode, alih-alih meneruskan seluruh objek pengguna ke dalam metode atau konstruktor, menciptakan masalah, maka jangan lakukan itu. Ini semua tentang mendapatkan keseimbangan antara prinsip dan pragmatisme.
Untuk menanggapi komentar Anda:
Bagian dari masalah di sini adalah Anda jelas tidak menyukai pendekatan ini, sesuai dengan komentar "tidak perlu [lulus] ...". Dan itu cukup adil; tidak ada jawaban yang tepat di sini. Jika Anda merasa itu memberatkan, jangan lakukan itu.
Namun, sehubungan dengan prinsip terbuka / tertutup, jika Anda mengikuti secara ketat maka "... ubah semua referensi ke semua lima metode yang ada ..." adalah indikasi bahwa metode tersebut dimodifikasi, padahal seharusnya tertutup untuk modifikasi. Pada kenyataannya, prinsip terbuka / tertutup masuk akal untuk API publik, tetapi tidak masuk akal untuk internal aplikasi.
Tapi kemudian Anda berkeliaran di wilayah YAGNI dan itu masih ortogonal dengan prinsip tersebut. Jika Anda memiliki metode
Foo
yang menggunakan nama pengguna dan kemudian Anda inginFoo
mengambil tanggal lahir juga, mengikuti prinsipnya, Anda menambahkan metode baru;Foo
tetap tidak berubah. Sekali lagi itu praktik yang baik untuk API publik, tetapi itu omong kosong untuk kode internal.Seperti yang disebutkan sebelumnya, ini tentang keseimbangan dan akal sehat untuk situasi apa pun. Jika parameter tersebut sering berubah, maka ya, gunakan
User
langsung. Ini akan menyelamatkan Anda dari perubahan skala besar yang Anda gambarkan. Tetapi jika mereka tidak sering berubah, maka hanya lewat apa yang dibutuhkan adalah pendekatan yang baik juga.sumber
User
contoh dan kemudian meminta objek itu untuk mendapatkan parameter, maka Anda hanya membalik sebagian dependensi; masih ada yang bertanya. Inversi ketergantungan sebenarnya adalah 100% "katakan, jangan tanya". Tapi itu datang dengan harga yang rumit.Ya, mengubah fungsi yang ada merupakan pelanggaran terhadap Prinsip Terbuka / Tertutup. Anda sedang memodifikasi sesuatu yang harus ditutup untuk modifikasi karena perubahan persyaratan. Desain yang lebih baik (untuk tidak berubah ketika persyaratan berubah) adalah untuk meneruskan Pengguna ke hal-hal yang seharusnya berfungsi pada pengguna.
Tapi itu mungkin bertabrakan dengan Segregasi Prinsip Interface, karena Anda bisa melewati sepanjang jalan informasi lebih dari kebutuhan fungsi untuk melakukan tugasnya.
Jadi, seperti kebanyakan hal - itu tergantung .
Dengan hanya menggunakan nama pengguna, mari fungsi ini menjadi lebih fleksibel, bekerja dengan nama pengguna terlepas dari mana mereka berasal dan tanpa perlu membuat objek Pengguna yang berfungsi penuh. Ini memberikan ketahanan untuk berubah jika Anda berpikir bahwa sumber data akan berubah.
Menggunakan seluruh Pengguna membuatnya lebih jelas tentang penggunaan dan membuat kontrak lebih kuat dengan peneleponnya. Ini memberikan ketahanan untuk berubah jika Anda berpikir bahwa lebih banyak Pengguna akan dibutuhkan.
sumber
User.find()
. Bahkan ada bahkan tidak harus menjadi seorangUser.find
. Menemukan pengguna tidak boleh menjadi tanggung jawabUser
.User
fungsi. Mungkin itu masuk akal. Tapi mungkin fungsinya hanya peduli dengan nama pengguna - dan menyampaikan hal-hal seperti tanggal bergabung Pengguna, atau alamat tidak patut.User
seharusnya tidak dimiliki kelas. Mungkin adaUserRepository
atau serupa yang berhubungan dengan hal-hal seperti itu.Desain ini mengikuti Pola Objek Parameter . Ini memecahkan masalah yang timbul dari memiliki banyak parameter dalam tanda tangan metode.
Tidak. Menerapkan pola ini memungkinkan prinsip Buka / tutup (OCP). Misalnya kelas turunan dari
User
dapat diberikan sebagai parameter yang menginduksi perilaku yang berbeda di kelas konsumsi.Itu bisa terjadi. Izinkan saya menjelaskan berdasarkan prinsip-prinsip SOLID.
The prinsip tanggung jawab Tunggal (SRP) dapat dilanggar jika memiliki desain seperti yang Anda telah dijelaskan:
Masalahnya dengan semua informasi . Jika
User
kelas memiliki banyak properti, itu menjadi Obyek Transfer Data besar yang mengangkut informasi yang tidak terkait dari perspektif kelas konsumsi. Contoh: Dari perspektif kelas konsumenUserAuthentication
, propertiUser.Id
danUser.Name
relevan, tetapi tidakUser.Timezone
.The Antarmuka segregasi Prinsip (ISP) juga melanggar dengan alasan serupa tetapi menambahkan perspektif lain. Contoh: Misalkan kelas konsumsi
UserManagement
membutuhkan propertiUser.Name
untuk dipecah menjadiUser.LastName
danUser.FirstName
kelasUserAuthentication
juga harus dimodifikasi untuk ini.Untungnya ISP juga memberi Anda kemungkinan jalan keluar dari masalah: Biasanya Objek Parameter atau Objek Transport Data mulai kecil dan tumbuh seiring waktu. Jika hal ini menjadi sulit, pertimbangkan pendekatan berikut: Memperkenalkan antarmuka yang disesuaikan dengan kebutuhan kelas konsumen. Contoh: Perkenalkan antarmuka dan biarkan
User
kelas berasal darinya:Setiap antarmuka harus mengekspos subset properti terkait dari
User
kelas yang diperlukan untuk kelas konsumsi untuk memenuhi operasinya. Cari kelompok properti. Cobalah untuk menggunakan kembali antarmuka. Dalam halUserAuthentication
penggunaan kelas konsumsiIUserAuthenticationInfo
bukanUser
. Kemudian jika mungkin memecahUser
kelas menjadi beberapa kelas beton menggunakan antarmuka sebagai "stensil".sumber
Ketika dihadapkan dengan masalah ini dalam kode saya sendiri, saya telah menyimpulkan bahwa model kelas dasar / objek adalah jawabannya.
Contoh umum adalah pola repositori. Seringkali ketika menanyakan database melalui repositori, banyak metode dalam repositori mengambil banyak parameter yang sama.
Aturan praktis saya untuk repositori adalah:
Di mana lebih dari satu metode mengambil 2 parameter yang sama atau lebih, parameter harus dikelompokkan bersama sebagai objek model.
Di mana metode mengambil lebih dari 2 parameter, parameter harus dikelompokkan bersama sebagai objek model.
Model dapat mewarisi dari basis bersama, tetapi hanya ketika itu benar-benar masuk akal (biasanya lebih baik untuk refactor lebih lambat daripada memulai dengan pewarisan dalam pikiran).
Masalah dengan menggunakan model dari lapisan / area lain tidak menjadi jelas sampai proyek mulai menjadi sedikit rumit. Hanya saat itulah Anda menemukan lebih sedikit kode yang menciptakan lebih banyak pekerjaan atau lebih banyak komplikasi.
Dan ya, benar-benar baik untuk memiliki 2 model berbeda dengan properti identik yang melayani berbagai lapisan / tujuan (mis. ViewModels vs POCOs).
sumber
Mari kita periksa aspek individual SOLID:
Satu hal yang cenderung membingungkan insting desain adalah bahwa kelas pada dasarnya untuk objek global, dan pada dasarnya hanya-baca. Dalam situasi seperti itu, melanggar abstraksi tidak banyak merugikan: Hanya membaca data yang tidak dimodifikasi menciptakan kopling yang sangat lemah; hanya ketika itu menjadi tumpukan besar rasa sakit menjadi nyata.
Untuk mengembalikan insting desain, anggap saja objeknya tidak terlalu global. Konteks apa yang dibutuhkan suatu fungsi jika
User
objek dapat dimutasi kapan saja? Komponen objek apa yang kemungkinan akan bermutasi bersama? Ini dapat dipisahkanUser
, apakah sebagai sub-objek yang direferensikan atau sebagai antarmuka yang memaparkan hanya "potongan" bidang terkait tidak begitu penting.Prinsip lain: Lihatlah fungsi yang menggunakan bagian
User
dan lihat bidang (atribut) mana yang cenderung disatukan. Itu daftar awal sub-proyek yang bagus - Anda pasti perlu memikirkan apakah mereka benar-benar milik bersama.Ini banyak pekerjaan, dan agak sulit untuk dilakukan, dan kode Anda akan menjadi sedikit kurang fleksibel karena akan menjadi sedikit lebih sulit untuk mengidentifikasi subobject (subinterface) yang perlu diteruskan ke suatu fungsi, terutama jika subobject memiliki tumpang tindih.
Berpisah
User
sebenarnya akan menjadi jelek jika subobjek tumpang tindih, maka orang akan bingung tentang mana yang harus dipilih jika semua bidang yang diperlukan berasal dari tumpang tindih. Jika Anda berpisah secara hierarkis (mis. Anda memilikiUserMarketSegment
yang, antara lain, milikiUserLocation
), orang tidak akan yakin pada level apa fungsi yang mereka tulis: apakah berurusan dengan data pengguna diLocation
tingkat atau diMarketSegment
level? Tidak persis membantu bahwa ini dapat berubah dari waktu ke waktu, yaitu Anda kembali pada perubahan tanda tangan fungsi, kadang-kadang di seluruh rantai panggilan.Dengan kata lain: Kecuali Anda benar-benar tahu domain Anda dan memiliki ide yang cukup jelas tentang modul apa yang berhubungan dengan aspek apa
User
, tidak benar-benar layak untuk memperbaiki struktur program.sumber
Ini pertanyaan yang sangat menarik. Itu tergantung.
Jika Anda berpikir metode Anda dapat berubah di masa mendatang secara internal untuk memerlukan parameter objek Pengguna yang berbeda, Anda tentu harus menyampaikan semuanya. Keuntungannya adalah bahwa kode eksternal untuk metode ini kemudian dilindungi dari perubahan dalam metode dalam hal parameter apa yang digunakannya, yang seperti yang Anda katakan akan menyebabkan kaskade perubahan secara eksternal. Jadi mengirimkan seluruh Pengguna meningkatkan enkapsulasi.
Jika Anda cukup yakin Anda tidak akan perlu menggunakan apa pun selain mengatakan email Pengguna, Anda harus meneruskannya. Keuntungannya adalah Anda kemudian dapat menggunakan metode ini dalam konteks yang lebih luas: Anda bisa menggunakannya misalnya dengan email Perusahaan atau email yang baru saja diketik seseorang. Ini meningkatkan fleksibilitas.
Ini adalah bagian dari serangkaian pertanyaan yang lebih luas tentang membangun kelas untuk memiliki cakupan yang luas atau sempit, termasuk apakah akan menyuntikkan dependensi atau tidak dan apakah memiliki objek yang tersedia secara global. Ada kecenderungan yang tidak menguntungkan saat ini untuk berpikir bahwa ruang lingkup yang lebih sempit selalu baik. Namun, selalu ada tradeoff antara enkapsulasi dan fleksibilitas seperti dalam kasus ini.
sumber
Saya menemukan yang terbaik untuk melewati parameter sesedikit mungkin dan sebanyak yang diperlukan. Ini membuat pengujian lebih mudah dan tidak membutuhkan kisi seluruh objek.
Dalam contoh Anda, jika Anda hanya akan menggunakan user-id atau nama pengguna maka ini semua yang harus Anda lewati. Jika pola ini berulang beberapa kali dan objek pengguna yang sebenarnya jauh lebih besar maka saran saya adalah membuat antarmuka yang lebih kecil untuk itu. Bisa jadi
atau
Ini membuat pengujian dengan mengejek jauh lebih mudah dan Anda langsung tahu nilai mana yang benar-benar digunakan. Kalau tidak, Anda sering perlu menginisialisasi objek kompleks dengan banyak dependensi lain meskipun pada akhirnya Anda hanya perlu satu atau dua properti.
sumber
Inilah sesuatu yang saya temui dari waktu ke waktu:
User
(atauProduct
atau apa pun) yang memiliki banyak properti, meskipun metode hanya menggunakan beberapa dari mereka.User
. Itu menciptakan sebuah instance dan menginisialisasi hanya properti metode yang sebenarnya dibutuhkan.User
argumen, Anda harus menemukan panggilan ke metode itu untuk menemukan dari manaUser
asalnya sehingga Anda tahu properti mana yang terisi. Apakah itu pengguna "nyata" dengan alamat email, atau hanya dibuat untuk melewati ID pengguna dan beberapa izin?Jika Anda membuat
User
dan hanya mengisi beberapa properti karena itulah yang dibutuhkan metode, maka penelepon benar-benar tahu lebih banyak tentang cara kerja dalam metode daripada yang seharusnya.Lebih buruk lagi, ketika Anda memiliki instance
User
, Anda harus tahu dari mana asalnya sehingga Anda tahu properti mana yang dihuni. Anda tidak mau harus tahu itu.Seiring waktu, ketika pengembang melihat
User
digunakan sebagai wadah untuk argumen metode, mereka mungkin mulai menambahkan properti ke dalamnya untuk skenario sekali pakai. Sekarang semakin jelek, karena kelas semakin berantakan dengan properti yang hampir selalu nol atau default.Korupsi semacam itu tidak bisa dihindari, tetapi terjadi berulang kali ketika kita melewati objek hanya karena kita membutuhkan akses ke beberapa propertinya. Zona bahaya adalah pertama kalinya Anda melihat seseorang membuat instance
User
dan hanya mengisi beberapa properti sehingga mereka dapat meneruskannya ke suatu metode. Letakkan kaki Anda di atasnya karena itu jalan yang gelap.Jika memungkinkan, berikan contoh yang tepat untuk pengembang berikutnya dengan hanya memberikan apa yang Anda harus lulus.
sumber