Referansla geçirilen bir nesneyi değiştirmek kötü bir uygulama mıdır?


12

Geçmişte, genellikle bir nesneyi oluşturduğum / güncellediğim birincil yöntemdeki manipülasyonumun çoğunu yaptım, ancak son zamanlarda kendimi farklı bir yaklaşımla buldum ve bunun kötü bir uygulama olup olmadığını merak ediyorum.

İşte bir örnek. Diyelim ki bir Uservarlığı kabul eden bir havuzum var, ancak varlığı eklemeden önce, tüm alanlarının istediğimiz şeye ayarlandığından emin olmak için bazı yöntemler çağırıyoruz. Şimdi, yöntemleri çağırmak ve Ekle yönteminin içinden alan değerlerini ayarlamak yerine, eklemeden önce nesneyi şekillendiren bir dizi hazırlama yöntemini çağırıyorum.

Eski Yöntem:

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

    context.Users.Add(user);
}

Yeni Yöntemler:

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;
}

Temel olarak, bir mülkün değerini başka bir yöntemden belirleme uygulaması kötü bir uygulama mıdır?


6
Demek istediğim ... burada referans olarak bir şey geçmedin. Referansların değere göre iletilmesi aynı şey değildir.
cHao

4
@cHao: Bu farksız bir ayrım. Kodun davranışı ne olursa olsun aynıdır.
Robert Harvey

2
@JDDavis: Temel olarak, iki örneğiniz arasındaki tek gerçek fark, ayarlanan kullanıcı adına anlamlı bir ad vermeniz ve şifre eylemlerini ayarlamanızdır.
Robert Harvey

1
Tüm çağrılarınız burada referans içindir. Değere göre geçmek için C # 'da bir değer türü kullanmanız gerekir.
Frank Hileman

2
@FrankHileman: Tüm çağrılar burada değerlidir. Bu C # için varsayılan. Geçme bir referans ve geçme ile referans farklı hayvanlardır ve ayrım önemli. Eğer userreferans olarak geçirildi, kod, arayanın elinden hızla çekmek ve basitçe diyelim diyerek yerini alabilir user = null;.
cHao

Yanıtlar:


10

Buradaki sorun User, a'nın aslında iki farklı şey içerebilmesidir:

  1. Veri deponuza aktarılabilen eksiksiz bir Kullanıcı varlığı.

  2. Bir Kullanıcı varlığı oluşturma işlemine başlamak için arayanın gerektirdiği veri öğeleri kümesi. Sistem, yukarıdaki # 1'deki gibi gerçekten geçerli bir Kullanıcı olmadan önce bir kullanıcı adı ve şifre eklemelidir.

Bu, nesne modelinize, tür sisteminizde hiç ifade edilmeyen belgesiz bir nüans içerir. Sadece bir geliştirici olarak "bilmek" gerekir. Bu harika değil ve karşılaştığınız gibi garip kod kalıplarına yol açıyor.

UserSınıf ve sınıf gibi iki varlığa ihtiyacınız olduğunu öneririm EnrollRequest. İkincisi, bir Kullanıcı oluşturmak için bilmeniz gereken her şeyi içerebilir. Kullanıcı sınıfınız gibi görünür, ancak kullanıcı adı ve parola olmadan. Sonra bunu yapabilirsin:

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;
}

Arayan yalnızca kayıt bilgileriyle başlar ve tamamlandıktan sonra, eklendikten sonra kullanıcıyı geri alır. Bu şekilde, herhangi bir sınıfı mutasyona uğratmaktan kaçınmış olursunuz ve yerleştirilen bir kullanıcı ile girmeyen bir kullanıcı arasında güvenli bir ayrım yaparsınız.


2
Benzer bir ada sahip bir yöntemin InsertUserekleme yan etkisi olması muhtemeldir. Bu bağlamda "icky" nin ne anlama geldiğinden emin değilim. Eklenmemiş bir "kullanıcı" kullanmaya gelince, noktayı kaçırmışsınız gibi görünüyor. Eklenmediyse, bu bir kullanıcı değil, yalnızca kullanıcı oluşturma isteğidir. Elbette, istekle çalışmakta özgürsünüz.
John Wu

@candiedorange Bunlar, yöntemi çağırmanın istenen etkileri değildir. Hemen eklemek istemiyorsanız, kullanım durumunu karşılayan başka bir yöntem oluşturursunuz. Ancak bu soruda geçen kullanım durumu değildir, yani endişe önemli değildir.
Andy

@candiedorange Kuplaj istemci kodunu kolaylaştırırsa bunun iyi olduğunu söyleyebilirim. Geliştiricilerin ya da konumun geleceği düşündüğü şey önemsizdir. Bu süre gelirse, kodu değiştirirsiniz. Hiçbir şey, gelecekteki olası kullanım durumlarını işleyebilecek bir kod oluşturmaya çalışmaktan daha israf edemez.
Andy

5
@CandiedOrange Uygulamanın tam olarak tanımlanmış tip sistemini, işletmenin kavramsal, sade-İngilizce varlıklarıyla sıkıca birleştirmemeye çalışmanızı öneririm. "Kullanıcı" işletme kavramı, işlevsel olarak önemli varyantları temsil etmek için daha fazla olmasa da iki sınıfta uygulanabilir. Kalıcı olmayan bir kullanıcının benzersiz bir kullanıcı adı garanti edilmez, işlemlerin bağlanabileceği hiçbir birincil anahtarı yoktur ve oturum açamaz bile, bu yüzden önemli bir varyant içerdiğini söyleyebilirim. Tabii ki, bir layman için, hem EnrollRequest hem de Kullanıcı belirsiz dillerinde "kullanıcılar" dır.
John Wu

5

Yan etkiler beklenmedik gelmediği sürece iyidir. Dolayısıyla, bir havuzda bir kullanıcıyı kabul eden ve kullanıcının dahili durumunu değiştiren bir yöntem olduğunda genel olarak yanlış bir şey yoktur. Ancak IMHO gibi bir yöntem adı InsertUser bunu açıkça bildirmez ve bu da onu hataya eğilimli kılar. Reponuzu kullanırken şöyle bir arama beklerim

 repo.InsertUser(user);

depoların iç durumunu değiştirmek için kullanıcı nesnesinin durumunu değil. Bu sorun her iki uygulamanızda da mevcuttur, InsertUserbunun dahili olarak nasıl tamamen ilgisiz olduğu.

Bunu çözmek için,

  • ilklendirmeyi ekten ayırın (böylece arayan kişinin InsertUsertamamen başlatılmış bir Usernesne sağlaması gerekir , veya,

  • başlatmayı kullanıcı nesnesinin yapım sürecine (diğer cevaplardan bazılarının önerdiği gibi) inşa etmek veya

  • yöntem için ne yaptığını daha net ifade eden daha iyi bir isim bulmaya çalışın .

Yani böyle bir yöntem adı seçmek PrepareAndInsertUser, OrchestrateUserInsertion, InsertUserWithNewNameAndPasswordya da yan etki daha net yapmayı tercih ne olursa olsun.

Tabii ki, böyle uzun bir yöntem adı, yöntemin belki de "çok fazla" (SRP ihlali) yaptığını gösterir, ancak bazen bunu kolayca düzeltmek istemezsiniz ya da çözemezsiniz ve bu en az müdahaleci, pragmatik bir çözümdür.


3

Seçtiğiniz iki seçeneğe bakıyorum ve önerilen yeni yöntemden çok eski yöntemi tercih ettiğimi söylemeliyim. Temelde aynı şeyi yapsalar da bunun birkaç nedeni var.

Her iki durumda da sen ayarlıyorsunuz user.UserNameve user.Password. Parola öğesi hakkında çekincelerim var, ancak bu rezervasyonlar eldeki konuya uygun değil.

Referans nesnelerini değiştirmenin etkileri

  • Eşzamanlı programlamayı zorlaştırır - ancak tüm uygulamalar çok iş parçacıklı değildir
  • Bu modifikasyonlar şaşırtıcı olabilir, özellikle yöntem hakkında hiçbir şey bunun gerçekleşmeyeceğini düşündürmezse
  • Bu sürprizler bakımı daha zor hale getirebilir

Eski Yöntem ve Yeni Yöntem

Eski yöntem işleri test etmeyi kolaylaştırdı:

  • GenerateUserName()bağımsız olarak test edilebilir. Bu yönteme karşı testler yazabilir ve adların doğru oluşturulduğundan emin olabilirsiniz
  • Ad, kullanıcı nesnesinden bilgi gerektiriyorsa, imzayı değiştirebilir GenerateUserName(User user)ve bu test edilebilirliği koruyabilirsiniz

Yeni yöntem mutasyonları gizler:

  • UserNesnenin 2 katman derine inene kadar değiştiğini bilmiyorsunuz
  • Bu Userdurumda nesnedeki değişiklikler daha şaşırtıcı
  • SetUserName()bir kullanıcı adı ayarlamaktan daha fazlasını yapar. Reklamcılıkta gerçek bu değil, bu da yeni geliştiricilerin uygulamanızda işlerin nasıl çalıştığını keşfetmesini zorlaştırıyor

1

John Wu'nun cevabına tamamen katılıyorum. Onun önerisi iyi. Ama doğrudan sorunuzu biraz özlüyor.

Temel olarak, bir mülkün değerini başka bir yöntemden belirleme uygulaması kötü bir uygulama mıdır?

Doğal olarak değil.

Beklenmedik davranışlarla karşılaşacağınız için bunu fazla ileriye götüremezsiniz. Örneğin PrintName(myPerson), kişi nesnesini değiştirmemelidir, çünkü yöntem yalnızca mevcut değerleri okumakla ilgilendiğini ima eder . Ama bu sizin durumunuzdan farklı bir argüman, çünkü SetUsername(user)değerler ayarlayacağını kuvvetle ima ediyor.


Bu aslında birim / entegrasyon testleri için sıklıkla kullandığım bir yaklaşımdır, burada değerlerini test etmek istediğim belirli bir duruma ayarlamak için özellikle bir nesneyi değiştirmek için bir yöntem oluştururum.

Örneğin:

var myContract = CreateEmptyContract();

ArrrangeContractDeletedStatus(myContract);

Açıkça ArrrangeContractDeletedStatusyöntemin myContractnesnenin durumunu değiştirmek için bekliyoruz .

Temel fayda, yöntemin farklı ilk sözleşmelerle sözleşme silme işlemini test etmeme izin vermesidir; örneğin, uzun statü geçmişine sahip veya daha önce statü geçmişi olmayan bir sözleşme, kasıtlı olarak hatalı durum geçmişine sahip bir sözleşme, test kullanıcımın silmesine izin verilmeyen bir sözleşme.

Eğer birleşmiş CreateEmptyContractve ArrrangeContractDeletedStatustek bir yönteme girmiş olsaydım ; Silinmiş bir durumda test etmek istediğim her farklı sözleşme için bu yöntemin birden çok varyantını oluşturmam gerekirdi.

Ve böyle bir şey yapabilirken:

myContract = ArrrangeContractDeletedStatus(myContract);

Bu ya gereksiz (zaten nesneyi değiştirdiğim için) ya da şimdi kendimi myContractnesnenin derin bir klonunu yapmaya zorluyorum ; Eğer her bir vakayı ele almak istiyorsanız bu çok zordur (birkaç seviye navigasyon değerine sahip olup olmadığımı hayal edin. Hepsini klonlamam gerekir mi? Sadece en üst düzey varlık mı? ... )

Nesneyi değiştirmek, nesneyi prensip olarak değiştirmemek için daha fazla iş yapmak zorunda kalmadan istediğimi elde etmenin en kolay yoludur.


Bu nedenle, sorunuzun doğrudan cevabı , yöntemin iletilen nesneyi değiştirmekle yükümlü olmadığı konusunda doğası gereği kötü bir uygulama olmadığıdır . Mevcut durumunuz için, bu yöntem yöntem adıyla çok açık bir şekilde anlaşılır.


0

Eski ve yeni problemli buluyorum.

Eski yol:

1) InsertUser(User user)

IDE'mde ortaya çıkan bu yöntem adını okurken, aklıma gelen ilk şey

Kullanıcı nereye yerleştirilir?

Yöntem adı okunmalıdır AddUserToContext. En azından, yöntemin sonunda yaptığı budur.

2) InsertUser(User user)

Bu, en az sürpriz ilkesini açıkça ihlal ediyor. Söyle, şimdiye kadar çalışmamı yaptım ve yeni bir örneği oluşturdum Userve ona bir nameve bir set verdim password, şaşırdım:

a) bu sadece kullanıcıyı bir şeye eklemekle kalmaz

b) aynı zamanda kullanıcıyı olduğu gibi yerleştirme niyetimi de bozar; ad ve şifre geçersiz kılındı.

c) bu aynı zamanda tek sorumluluk ilkesinin de ihlal edildiğini gösterir mi?

Yeni yol:

1) InsertUser(User user)

Hala SRP ihlali ve en az sürpriz ilkesi

2) SetUsername(User user)

Soru

Kullanıcı adı belirlensin mi? Neye?

Daha iyi: SetRandomNameya da niyeti yansıtan bir şey.

3) SetPassword(User user)

Soru

Şifreyi belirle? Neye?

Daha iyi: SetRandomPasswordya da niyeti yansıtan bir şey.


Öneri:

Ne okumayı tercih ederdim:

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);
}

İlk sorunuzla ilgili olarak:

Referansla geçirilen bir nesneyi değiştirmek kötü bir uygulama mıdır?

Hayır. Bu daha çok bir zevk meselesi.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.