Apakah memodifikasi objek yang lulus dengan referensi merupakan praktik yang buruk?

12

Di masa lalu, saya biasanya melakukan sebagian besar manipulasi objek pada metode utama yang sedang dibuat / dimutakhirkan, tetapi saya mendapati diri saya mengambil pendekatan yang berbeda akhir-akhir ini, dan saya ingin tahu apakah ini merupakan praktik yang buruk.

Ini sebuah contoh. Katakanlah saya memiliki repositori yang menerima Userentitas, tetapi sebelum memasukkan entitas, kami memanggil beberapa metode untuk memastikan semua bidangnya disetel ke apa yang kami inginkan. Sekarang, daripada memanggil metode dan menetapkan nilai bidang dari dalam metode Sisipkan, saya memanggil serangkaian metode persiapan yang membentuk objek sebelum penyisipannya.

Metode lama:

public void InsertUser(User user) {
    user.Username = GenerateUsername(user);
    user.Password = GeneratePassword(user);

    context.Users.Add(user);
}

Metode Baru:

public void InsertUser(User user) {
    SetUsername(user);
    SetPassword(user);

    context.Users.Add(user);
}

private void SetUsername(User user) {
    var username = "random business logic";

    user.Username = username;
}

private void SetPassword(User user) {
    var password = "more business logic";

    user.Password = password;
}

Pada dasarnya, apakah praktik menetapkan nilai properti dari metode lain merupakan praktik buruk?

JD Davis
sumber
6
Demikian dikatakan ... Anda belum melewati apa pun dengan referensi di sini. Melewati referensi berdasarkan nilai sama sekali tidak sama.
cao
4
@ cHao: Itu perbedaan tanpa perbedaan. Perilaku kode tetap sama.
Robert Harvey
2
@ JDDavis: Pada dasarnya, satu-satunya perbedaan nyata antara dua contoh Anda adalah bahwa Anda telah memberikan nama yang bermakna untuk nama pengguna yang ditetapkan dan mengatur tindakan kata sandi.
Robert Harvey
1
Semua panggilan Anda dengan referensi di sini. Anda harus menggunakan tipe nilai dalam C # untuk melewati nilai.
Frank Hileman
2
@ Frankrankileman: Semua panggilan berdasarkan nilai di sini. Itu default di C #. Melewati sebuah referensi dan melewati oleh referensi adalah binatang yang berbeda, dan perbedaan itu penting. Jika userdilewatkan dengan referensi, kode dapat menariknya keluar dari tangan penelepon dan menggantinya dengan hanya mengatakan, katakan user = null;,.
cao

Jawaban:

10

Masalahnya di sini adalah bahwa Usersebenarnya bisa mengandung dua hal yang berbeda:

  1. Entitas Pengguna yang lengkap, yang dapat diteruskan ke penyimpanan data Anda.

  2. Kumpulan elemen data yang diperlukan dari pemanggil untuk memulai proses pembuatan entitas Pengguna. Sistem harus menambahkan nama pengguna dan kata sandi sebelum benar-benar Pengguna yang valid seperti pada # 1 di atas.

Ini terdiri dari nuansa tidak berdokumen untuk model objek Anda yang tidak diungkapkan dalam sistem tipe Anda sama sekali. Anda hanya perlu "mengetahuinya" sebagai pengembang. Itu tidak bagus, dan itu mengarah ke pola kode aneh seperti yang Anda temui.

Saya sarankan Anda membutuhkan dua entitas, misalnya Userkelas dan EnrollRequestkelas. Yang terakhir dapat berisi semua yang perlu Anda ketahui untuk membuat Pengguna. Itu akan terlihat seperti kelas Pengguna Anda, tetapi tanpa nama pengguna dan kata sandi. Maka Anda bisa melakukan ini:

public User InsertUser(EnrollRequest request) {
    var userName = GenerateUserName();
    var password = GeneratePassword();

    //You might want to replace this with a factory call, but "new" works here as an example
    var newUser = new User
    (
        request.Name, 
        request.Email, 
        userName, 
        password
    );
    context.Users.Add(user);
    return newUser;
}

Penelepon mulai dengan hanya informasi pendaftaran dan mendapatkan kembali pengguna yang sudah selesai setelah dimasukkan. Dengan cara ini Anda menghindari mutasi kelas apa pun dan Anda juga memiliki perbedaan jenis-aman antara pengguna yang dimasukkan dan yang tidak.

John Wu
sumber
2
Metode dengan nama seperti InsertUserkemungkinan memiliki efek samping menyisipkan. Saya tidak yakin apa artinya "icky" dalam konteks ini. Sedangkan untuk menggunakan "pengguna" yang belum ditambahkan, Anda sepertinya telah melewatkan intinya. Jika belum ditambahkan, itu bukan pengguna, hanya permintaan untuk membuat pengguna. Anda bebas bekerja dengan permintaan itu, tentu saja.
John Wu
@candiedorange Itu bukan efek samping, mereka efek yang diinginkan dari memanggil metode. Jika Anda tidak ingin segera menambahkan, Anda membuat metode lain yang memenuhi use case. Tapi itu bukan use case yang diteruskan dalam pertanyaan sehingga dikatakan bahwa keprihatinan itu tidak penting.
Andy
@candiedorange Jika kopling membuat kode klien lebih mudah saya akan mengatakan itu baik-baik saja. Apa yang dipikirkan oleh para dev atau pos di masa depan tidak relevan. Jika saat itu tiba, Anda mengubah kode. Tidak ada yang lebih sia-sia daripada mencoba membuat kode yang dapat menangani setiap kasus penggunaan yang mungkin terjadi di masa depan.
Andy
5
@CandiedOrange Saya sarankan Anda mencoba untuk tidak memasangkan erat sistem implementasi yang didefinisikan dengan tepat dengan entitas bisnis konseptual, Inggris-polos. Konsep bisnis "pengguna" tentu saja dapat diimplementasikan dalam dua kelas, jika tidak lebih, untuk mewakili varian signifikan secara fungsional. Seorang pengguna yang tidak bertahan tidak dijamin nama pengguna yang unik, tidak memiliki kunci utama untuk transaksi yang dapat diikat, dan bahkan tidak dapat masuk, jadi saya akan mengatakan itu terdiri dari varian yang signifikan. Bagi orang awam, tentu saja, baik EnrollRequest dan Pengguna adalah "pengguna," dalam bahasa yang tidak jelas.
John Wu
5

Efek samping tidak masalah selama tidak datang secara tak terduga. Jadi tidak ada yang salah secara umum ketika repositori memiliki metode menerima pengguna dan mengubah keadaan internal pengguna. Tapi IMHO nama metode seperti InsertUser tidak mengkomunikasikan hal ini dengan jelas , dan itulah yang membuatnya rentan kesalahan. Saat menggunakan repo Anda, saya akan mengharapkan panggilan seperti

 repo.InsertUser(user);

untuk mengubah status internal repositori, bukan status objek pengguna. Masalah ini ada di kedua implementasi Anda, bagaimana InsertUserhal ini secara internal sama sekali tidak relevan.

Untuk mengatasi ini, Anda bisa

  • pisahkan inisialisasi dari penyisipan (sehingga pemanggil InsertUserkebutuhan untuk menyediakan Userobjek yang diinisialisasi penuh , atau,

  • membangun inisialisasi ke dalam proses konstruksi objek pengguna (seperti yang disarankan oleh beberapa jawaban lain), atau

  • coba saja cari nama yang lebih baik untuk metode yang mengekspresikan lebih jelas apa yang dilakukannya.

Jadi memilih nama metode seperti PrepareAndInsertUser, OrchestrateUserInsertion, InsertUserWithNewNameAndPasswordatau apa pun yang Anda memilih untuk membuat efek samping yang lebih jelas.

Tentu saja, nama metode yang panjang menunjukkan bahwa metode tersebut mungkin melakukan "terlalu banyak" (pelanggaran SRP), tetapi kadang-kadang Anda tidak ingin atau tidak dapat dengan mudah memperbaiki ini, dan ini adalah solusi pragmatis yang paling tidak mengganggu.

Doc Brown
sumber
3

Saya melihat dua opsi yang Anda pilih, dan saya harus mengatakan bahwa saya lebih memilih metode lama daripada metode baru yang diusulkan. Ada beberapa alasan untuk itu meskipun mereka pada dasarnya melakukan hal yang sama.

Dalam kedua kasus Anda mengatur user.UserNamedan user.Password. Saya memiliki reservasi tentang item kata sandi, tetapi reservasi tersebut tidak sesuai dengan topik yang sedang dibahas.

Implikasi memodifikasi objek referensi

  • Itu membuat pemrograman bersamaan lebih sulit - tetapi tidak semua aplikasi multithreaded
  • Modifikasi itu bisa mengejutkan, terutama jika tidak ada metode yang menunjukkan bahwa itu akan terjadi
  • Kejutan itu dapat membuat perawatan lebih sulit

Metode Lama vs. Metode Baru

Metode lama membuat hal-hal lebih mudah untuk diuji:

  • GenerateUserName()dapat diuji secara independen. Anda dapat menulis tes terhadap metode itu dan memastikan nama-nama tersebut dihasilkan dengan benar
  • Jika nama memerlukan informasi dari objek pengguna, maka Anda dapat mengubah tanda tangan GenerateUserName(User user)dan mempertahankan testability itu

Metode baru menyembunyikan mutasi:

  • Anda tidak tahu bahwa Userobjeknya berubah hingga kedalaman 2 lapis
  • Perubahan pada Userobjek lebih mengejutkan dalam hal itu
  • SetUserName()tidak lebih dari menetapkan nama pengguna. Itu bukan kebenaran dalam periklanan yang menyulitkan pengembang baru untuk menemukan cara kerja di aplikasi Anda
Berin Loritsch
sumber
1

Saya sepenuhnya setuju dengan jawaban John Wu. Sarannya bagus. Tapi itu sedikit merindukan pertanyaan langsung Anda.

Pada dasarnya, apakah praktik menetapkan nilai properti dari metode lain merupakan praktik buruk?

Tidak secara inheren.

Anda tidak dapat mengambil ini terlalu jauh, karena Anda akan mengalami perilaku yang tidak terduga. Misalnya PrintName(myPerson)tidak boleh mengubah objek orang, karena metode ini menyiratkan itu hanya tertarik membaca nilai yang ada. Tapi itu argumen yang berbeda dari kasus Anda, karena SetUsername(user)sangat menyiratkan bahwa itu akan menetapkan nilai.


Ini sebenarnya adalah pendekatan yang sering saya gunakan untuk tes unit / integrasi, di mana saya membuat metode khusus untuk mengubah objek untuk mengatur nilai-nilainya ke situasi tertentu yang ingin saya uji.

Sebagai contoh:

var myContract = CreateEmptyContract();

ArrrangeContractDeletedStatus(myContract);

Saya secara eksplisit mengharapkan ArrrangeContractDeletedStatusmetode untuk mengubah keadaan myContractobjek.

Manfaat utama adalah bahwa metode ini memungkinkan saya untuk menguji penghapusan kontrak dengan berbagai kontrak awal; mis. kontrak yang memiliki riwayat status panjang, atau kontrak yang tidak memiliki riwayat status sebelumnya, kontrak yang sengaja memiliki riwayat status keliru, kontrak yang tidak boleh dihapus oleh pengguna pengujian saya.

Jika saya telah bergabung CreateEmptyContractdan ArrrangeContractDeletedStatusmenjadi satu metode; Saya harus membuat beberapa varian dari metode ini untuk setiap kontrak berbeda yang ingin saya uji dalam keadaan terhapus.

Dan sementara saya bisa melakukan sesuatu seperti:

myContract = ArrrangeContractDeletedStatus(myContract);

Ini bisa berlebihan (karena saya toh mengubah objek), atau saya sekarang memaksakan diri untuk membuat klon myContractobjek yang dalam; yang sangat sulit jika Anda ingin membahas setiap kasus (bayangkan jika saya ingin properti navigasi beberapa tingkat. Apakah saya perlu mengkloning semuanya? Hanya entitas tingkat atas? ... Begitu banyak pertanyaan, begitu banyak harapan tersirat )

Mengubah objek adalah cara termudah untuk mendapatkan apa yang saya inginkan tanpa harus melakukan lebih banyak pekerjaan hanya untuk menghindari tidak mengubah objek sebagai masalah prinsip.


Jadi jawaban langsung untuk pertanyaan Anda adalah bahwa itu pada dasarnya bukan praktik yang buruk, selama Anda tidak mengaburkan bahwa metode ini bertanggung jawab untuk mengubah objek yang dikirimkan. Untuk situasi Anda saat ini, itu dibuat sangat jelas melalui nama metode.

Flater
sumber
0

Saya menemukan yang lama dan yang baru bermasalah.

Cara lama:

1) InsertUser(User user)

Saat membaca nama metode ini muncul di IDE saya, hal pertama yang terlintas di benak saya adalah

Di mana pengguna dimasukkan?

Nama metode harus dibaca AddUserToContext. Setidaknya, inilah yang dilakukan metode ini pada akhirnya.

2) InsertUser(User user)

Ini jelas melanggar prinsip kejutan paling tidak. Katakanlah, saya melakukan pekerjaan saya sejauh ini dan menciptakan contoh baru Userdan memberinya namedan menetapkan password, saya akan terkejut:

a) ini tidak hanya memasukkan pengguna ke dalam sesuatu

b) itu juga menumbangkan niat saya untuk memasukkan pengguna apa adanya; nama dan kata sandi diganti.

c) apakah ini menunjukkan, bahwa itu juga merupakan pelanggaran terhadap prinsip tanggung jawab tunggal.

Jalan baru:

1) InsertUser(User user)

Masih melanggar SRP dan prinsip yang paling tidak mengejutkan

2) SetUsername(User user)

Pertanyaan

Tetapkan nama pengguna? Untuk apa?

Lebih baik: SetRandomNameatau sesuatu yang mencerminkan niat.

3) SetPassword(User user)

Pertanyaan

Setel kata sandi? Untuk apa?

Lebih baik: SetRandomPasswordatau sesuatu yang mencerminkan niat.


Saran:

Yang ingin saya baca adalah:

public User GenerateRandomUser(UserFactory Uf){
    User u = Uf.GetUser();
    u.Name = GenerateRandomUserName();
    u.Password = GenerateRandomUserPassword();
    return u;
}

...

public void AddUserToContext(User u){
    this.context.Users.Add(u);
}

Mengenai pertanyaan awal Anda:

Apakah memodifikasi objek yang lulus dengan referensi merupakan praktik yang buruk?

Tidak. Ini lebih merupakan masalah selera.

Thomas Junk
sumber