Kodlama stili sorunu: Parametre alan, değiştiren ve daha sonra bu parametreyi GERİ DÖNEN fonksiyonlarımız olmalı mı?


19

Bu iki uygulamanın aynı madalyonun sadece iki yüzü olup olmadığı ya da gerçekten daha iyi olup olmadığı konusunda arkadaşımla biraz tartışıyorum.

Bir parametre alan, bir üyeyi dolduran ve sonra döndüren bir fonksiyonumuz var:

Item predictPrice(Item item)

Aynı nesne üzerinde çalıştığından , öğeyi iade etmenin bir anlamı olmadığına inanıyorum . Aslında, herhangi bir şey, arayanın perspektifinden bakıldığında, yeni bir öğeyi iade etmesini beklediğiniz gibi, konuları karıştırır .

Hiçbir fark yaratmadığını iddia ediyor ve yeni bir Eşya yaratıp geri vermesinin önemi bile olmayacak. Aşağıdaki nedenlerden dolayı kesinlikle katılmıyorum:

  • Öğeye (veya işaretçiler veya her ne olursa olsun) birden fazla referansınız varsa, yeni bir nesne tahsis eder ve onu döndürmek bu referanslar yanlış olacağından önemli öneme sahiptir.

  • Bellek tarafından yönetilmeyen dillerde, yeni bir örneği ayıran işlev belleğin sahipliğini iddia eder ve bu nedenle bir noktada çağrılan bir temizleme yöntemi uygulamak zorunda kalırız.

  • Öbek üzerinde tahsis etmek potansiyel olarak pahalıdır ve bu nedenle çağrılan işlevin bunu yapıp yapmadığı önemlidir.

Bu nedenle, bir nesneyi değiştirip değiştirmediğini veya yeni bir nesne tahsis ettiğini bir yöntem imzasıyla görebilmenin çok önemli olduğuna inanıyorum. Sonuç olarak, işlev yalnızca iletilen nesneyi değiştirdikçe, imza şöyle olmalıdır:

void predictPrice(Item item)

Birlikte çalıştığım dil olan her kod tabanında (kuşkusuz C ve C ++ kod tabanlarında, Java değil), yukarıdaki stile esasen bağlı kaldım ve çok daha deneyimli programcılar tarafından tutuldu. Kod tabanları ve meslektaşlarımın örnek boyutu olarak tüm olası kod tabanları ve meslektaşlarımdan küçük olduğunu ve bu yüzden tecrübemin birinin üstün olup olmadığı konusunda gerçek bir gösterge olmadığını iddia ediyor.

Peki, herhangi bir düşünce?


strcat bir parametreyi yerinde değiştirir ve yine de döndürür. Strcat dönüşü iptal etmeli mi?
Jerry Jeremiah

Java ve C ++ geçiş parametreleri açısından çok farklı davranışlara sahiptir. Eğer Itembir class Item ...değil typedef ...& Item, yerel itemzaten bir kopyasıdır
Caleth

google.github.io/styleguide/cppguide.html#Output_Parameters Google, çıktı için RETURN değerini kullanmayı tercih ediyor
Firegun

Yanıtlar:


26

Bu gerçekten bir fikir meselesidir, ancak değeri için, öğe yerinde değiştirilirse değiştirilmiş öğeyi iade etmenin yanıltıcı olduğunu düşünüyorum. Ayrı olarak, predictPriceöğeyi değiştirecekse, bunu yapacağını belirten bir adı olmalıdır (benzer setPredictPriceveya benzeri).

(Sırayla) tercih ederim

  1. Yani predictPricebir oldu yöntemItem olan ya (kuyu genel tasarımda olabilir o olmaman için iyi bir neden, modulo)

    • Öngörülen fiyatı iade etti veya

    • setPredictedPriceBunun yerine bir isim vardı

  2. O predictPrice vermedi değiştirmek Item, ancak tahmin fiyatı döndü

  3. Bu predictPricebir voidyöntemdisetPredictedPrice

  4. Yani predictPricedöndü this(o bir parçası olursa olsun örneği üzerindeki diğer yöntemlere yöntem zincirleme için) (ve denirdi setPredictedPrice)


1
Cevap için teşekkürler. 1 ile ilgili olarak, Öğenin içindeFiyat'ı tahmin edemiyoruz. 2 iyi bir öneri - bence bu muhtemelen doğru çözüm.
Squimmy

2
@Squimmy: Ve elbette, sadece kullanan bir kişi tarafından kaynaklanan bir hata ile uğraşan 15 dakika geçirdi aynen : Eğer karşı kavga ettiler desen a = doSomethingWith(b) modifiye b ve bu kadar biliyordu düşünce daha sonra kodumu havaya uçurdu modifikasyonlar ile geri döndü bvardı içinde. :-)
TJ Crowder

11

Nesneye yönelik bir tasarım paradigmasında, nesnenin kendisinin dışındaki nesneleri değiştirmemelidir. Bir nesnenin bir duruma herhangi bir değişiklik gerekir nesne üzerinde yöntemlerle yapılabilir.

Bu nedenle, void predictPrice(Item item)başka bir sınıfın üye işlevi yanlıştır . C günlerinde kabul edilebilirdi, ancak Java ve C ++ için, bir nesnenin modifikasyonu, nesneyle yoldaki diğer tasarım sorunlarına yol açacak daha derin bir bağlantı anlamına gelir (sınıfı yeniden düzenlediğinizde ve alanlarını değiştirdiğinizde, şimdi 'tahmin fiyat' başka bir dosyada değiştirilmesi gerekiyor.

Yeni bir nesneyi geri döndürmek, ilişkili yan etkilere sahip değildir, iletilen parametre değişmez. Siz (predictPrice yöntemi), bu parametrenin başka nerede kullanıldığını bilmiyorsunuz. ItemBir yerde bir karma anahtarı var mı ? bunu yaparken karma kodunu değiştirdiniz mi? Bekleyen başka bir kişi değişmemesini bekliyor mu?

Bu tasarım sorunları, nesneleri değiştirmemenizi şiddetle önerir (birçok durumda değişmezliği savunacağım) ve eğer yaparsanız, durumdaki değişiklikler bir şey yerine nesnenin kendisi tarafından kontrol edilmeli ve kontrol edilmelidir. sınıf dışında başka.


Bir kişi bir karmadaki alanlarla uğraşırsa ne olduğuna bakalım. Biraz kod alalım:

import java.util.*;

public class Main {
    public static void main (String[] args) {
        Set set = new HashSet();
        Data d = new Data(1,"foo");
        set.add(d);
        set.add(new Data(2,"bar"));
        System.out.println(set.contains(d));
        d.field1 = 2;
        System.out.println(set.contains(d));
    }

    public static class Data {
        int field1;
        String field2;

        Data(int f1, String f2) {
            field1 = f1;
            field2 = f2;
        }

        public int hashCode() {
            return field2.hashCode() + field1;
        }

        public boolean equals(Object o) {
            if(!(o instanceof Data)) return false;
            Data od = (Data)o;
            return od.field1 == this.field1 && od.field2.equals(this.field2);

        }
    }
}

ideone

Ve bunun en büyük kod olmadığını (doğrudan alanlara eriştiğini) kabul edeceğim, ancak değişebilir verilerin bir HashMap'in anahtarı olarak kullanıldığını veya bu durumda bir HashSet.

Bu kodun çıktısı:

true
false

Olan şey, eklenirken kullanılan hashCode öğesinin nesnenin karma olduğu yerdir. HashCode'u hesaplamak için kullanılan değerleri değiştirmek, hash'ın kendisini yeniden hesaplamaz. Bu, değiştirilebilir herhangi bir nesneyi bir karma için anahtar olarak koyma tehlikesidir .

Böylece, sorunun orijinal yönüne dönersek, çağrılan yöntem bir parametre olarak aldığı nesnenin nasıl kullanıldığını "bilmez". Bunu yapmanın tek yolu olarak "nesneyi değiştirelim" olarak sunmak, sürünebilecek bir dizi ince hata olduğu anlamına gelir. Bir karma içinde değerleri kaybetmek gibi ... daha fazla eklemediğiniz ve karma yeniden şekillenmedikçe - güven bana , Bu aşağı avlamak için kötü bir hata (hashMap 20 daha fazla öğe ekleyin ve sonra aniden tekrar görünür kadar ben değer kaybetti).

  • Nesneyi değiştirmek için çok iyi bir neden olmadığı sürece , yeni bir nesneyi döndürmek en güvenli şeydir.
  • Orada zaman olduğu nesneyi değiştirmek için iyi bir neden, bu değiştirme işleminin yerine kendi alanları twiddle bazı dış işlevi aracılığıyla daha nesnenin kendisi (çağırarak yöntemleri) tarafından yapılmalıdır.
    • Bu, nesnenin daha düşük bakım maliyetiyle yeniden düzenlenmesini sağlar.
    • Bu nesne, kendi karma kodunun hesaplamak değerleri emin olmak için izin verir yok değişikliği (veya karma kodudur hesaplama parçası değildir)

Related: Bir if ifadesinin koşullu olarak kullanıldığı argümanın değerinin üzerine yazılması ve döndürülmesi, aynı if ifadesinin içinde


3
Kulağa doğru gelmiyor. Büyük olasılıkla üyeleriyle predictPricedoğrudan uğraşmamak gerekir Item, ancak bunun çağrı yöntemlerinde yanlış olan ne Item? Değiştirilen tek nesne bu ise, belki de değiştirilen nesnenin bir yöntemi olması gerektiğini iddia edebilirsiniz, ancak diğer zamanlarda birden fazla nesne (kesinlikle aynı sınıfta olmamalıdır) değiştirilir, özellikle de oldukça yüksek seviyeli yöntemler.

@delnan Bunun bir kerede bir karmadaki kaybolan ve yeniden ortaya çıkan bir değerden sonra izlemem gereken bir hataya (aşırı) bir tepki olabileceğini kabul edeceğim - birisi eklendikten sonra karmadaki nesnenin alanlarıyla uğraşıyordu. nesnenin kullanımı için bir kopyasını yapmak yerine. Birinin alabileceği savunma programlama önlemleri vardır (örneğin değişmez nesneler) - ancak nesnenin "sahibi" olmadığınızda nesnenin kendisindeki değerleri yeniden hesaplamak gibi şeyler veya nesnenin kendisi de ısırmaya geri dönebilecek bir şey değildir sen zorsun.

Bildiğiniz gibi, kapsüllemeyi geliştirmek için sınıf işlevleri yerine daha fazla serbest işlev kullanmak için iyi bir argüman var. Doğal olarak, sabit bir nesneyi alan bir işlev (ister örtülü ister bu parametre olsun) onu gözlemlenebilir bir şekilde değiştirmemelidir (bazı sabit nesneler bazı şeyleri önbelleğe alabilir).
Tekilleştirici

2

Ben her zaman başkasının nesnesini mutasyona uğratmama pratiğini takip ederim . Yani, sadece içinde bulunduğunuz sınıf / struct / whathaveyou'ya ait olan mutasyona uğrayan nesneler.

Aşağıdaki veri sınıfı göz önüne alındığında:

class Item {
    public double price = 0;
}

Tamamdır:

class Foo {
    private Item bar = new Item();

    public void predictPrice() {
        bar.price = bar.price + 5; // or something
    }
}

// Elsewhere...

Foo foo = Foo();
foo.predictPrice();
System.out.println(foo.bar.price);
// Since I called predictPrice on Foo, which takes and returns nothing, it's
// apparent something might've changed

Ve bu çok açık:

class Foo {
    private Item bar = new Item();
}
class Baz {
    public double predictPrice(Item item) {
        return item.price + 5; // or something
    }
}

// Elsewhere...

Foo foo = Foo();
Baz baz = Baz();
foo.bar.price = baz.predictPrice(foo.bar);
System.out.println(foo.bar.price);
// Since I explicitly set foo.bar.price to the return value of predictPrice, it's
// obvious foo.bar.price might've changed

Ama bu benim zevklerim için çok çamurlu ve gizemli:

class Foo {
    private Item bar = new Item();
}
class Baz {
    public void predictPrice(Item item) {
        item.price = item.price + 5; // or something
    }
}

// Elsewhere...

Foo foo = Foo();
Baz baz = Baz();
baz.predictPrice(foo.bar);
System.out.println(foo.bar.price);
// In my head, it doesn't appear that to foo, bar, or price changed. It seems to
// me that, if anything, I've placed a predicted price into baz that's based on
// the value of foo.bar.price

Herhangi bir downvotes bir yorum ile eşlik edin, bu yüzden gelecekte daha iyi cevaplar yazmayı biliyorum :)
Ben Leggiero

1

Sözdizimi farklı diller arasında farklıdır ve bir dile özgü olmayan bir kod parçasını göstermek anlamsızdır çünkü farklı dillerde tamamen farklı şeyler ifade eder.

Java'da, Itembir başvuru türüdür - örnek olan nesnelere işaretçi türüdür Item. C ++ 'da bu tür olarak yazılır Item *(aynı şeyin sözdizimi diller arasında farklıdır). Yani Item predictPrice(Item item)Java'da Item *predictPrice(Item *item)C ++ ile eşdeğerdir . Her iki durumda da, bir nesneye bir işaretçi alan ve bir nesneye bir işaretçi döndüren bir işlevdir (veya yöntemdir).

C ++ 'da Item(başka bir şey olmadan) bir nesne türüdür ( Itembir sınıfın adı olduğu varsayılarak ). Bu türdeki bir değer Item predictPrice(Item item)bir nesnedir ve bir nesneyi alıp bir nesneyi döndüren bir işlevi bildirir. Yana &kullanılmaz ama geçti ve değerine göre döndürülür. Bu, nesnenin geçtiğinde kopyalandığı ve iade edildiğinde kopyalandığı anlamına gelir. Java'da nesne türü olmadığından Java'da eşdeğer yoktur.

Peki hangisi? Soruda sorduğunuz şeylerin çoğu (örneğin, "Geçilen aynı nesne üzerinde çalıştığına inanıyorum ...") tam olarak burada neyin geçtiğini ve geri döndüğünü anlamaya bağlıdır.


1

Kod tabanlarının uyduğunu gördüğüm kural, sözdiziminden anlaşılır bir stili tercih etmektir. İşlev değer değiştirirse, işaretçiden geçmeyi tercih edin

void predictPrice(Item * const item);

Bu tarzın sonuçları:

  • Onu çağırırken şunları görürsünüz:

    predictPrice (& my_item);

ve stile bağlılığınızla değiştirileceğini biliyorsunuz.

  • Aksi takdirde, işlevin örneği değiştirmesini istemediğinizde const ref veya değerle iletmeyi tercih edin.

Bu çok daha açık bir sözdizimi olsa da, Java'da kullanılamadığına dikkat edilmelidir.
Ben Leggiero

0

Güçlü bir tercihim olmasa da, nesneyi döndürmenin bir API için daha iyi esnekliğe yol açtığını oylarım.

Sadece yöntemin diğer nesneyi döndürme özgürlüğü varken mantıklı olduğunu unutmayın. Javadoc'unuz derse @returns the same value of parameter aişe yaramaz. Ancak javadocunuz diyorsa @return an instance that holds the same data than parameter a, aynı örneği veya başka bir örneği döndürmek bir uygulama ayrıntısıdır.

Örneğin, şu anki projemde (Java EE) bir iş katmanım var; DB'de depolamak (ve otomatik bir kimlik atamak) gibi, bir Çalışan, örneğin bir çalışanım var

 public Employee createEmployee(Employee employeeData) {
   this.entityManager.persist(employeeData);
   return this.employeeData;
 }

Tabii ki, JPA Id atandıktan sonra aynı nesneyi döndürdüğü için bu uygulamada bir fark yok. Şimdi, JDBC'ye geçersem

 public Employee createEmployee(Employee employeeData) {
   PreparedStatement ps = this.connection.prepareStatement("INSERT INTO EMPLOYEE(name, surname, data) VALUES(?, ?, ?)");
   ... // set parameters
   ps.execute();
   int id = getIdFromGeneratedKeys(ps);
   ??????
 }

Şimdi ????? Üç seçeneğim var.

  1. Id için bir ayarlayıcı tanımlayın, ben de aynı nesneyi döndüreceğim. Çirkin, çünkü setIdher yerde mevcut olacak.

  2. Bazı ağır yansıma çalışmaları yapın ve Employeenesneyi kimliği ayarlayın . Kirli, ama en azından createEmployeekayıtla sınırlı .

  3. Orijinali kopyalayan Employeeve idkümeyi de kabul eden bir yapıcı sağlayın .

  4. Nesneyi DB'den alın (DB'de bazı alan değerleri hesaplanıyorsa veya bir taşınmaya girmek istiyorsanız gerekli olabilir).

Şimdi, 1. ve 2. için aynı örneği döndürüyor olabilirsiniz, ancak 3. ve 4. için yeni örnekleri döndürürsünüz. API'nızda "gerçek" değerin yöntem tarafından döndürülen değer olacağından emin olduğunuz prensipten daha fazla özgürlüğe sahipsiniz.

Buna karşı düşünebileceğim tek gerçek argüman, 1. veya 2. implementatios'u kullanarak API'nizi kullanan kişilerin sonucun atamasını göz ardı edebileceği ve 3. veya 4. uygulamalara geçtiğinizde kodlarının kırılacağı).

Her neyse, gördüğünüz gibi, tercihim diğer kişisel tercihlere dayanıyor (Ids için bir ayarlayıcıyı yasaklamak gibi), bu yüzden belki herkes için geçerli değildir.


0

Java'da kodlama yapılırken, değişebilir tipteki her nesnenin kullanımının iki modelden biriyle eşleştirilmesi gerekir:

  1. Tam olarak bir varlık, değişken nesnenin "sahibi" olarak kabul edilir ve bu nesnenin durumunu kendi parçasının bir parçası olarak görür. Diğer varlıklar referans alabilir, ancak referansı başka birine ait bir nesneyi tanımlamak olarak kabul etmelidir .

  2. Nesnenin değişebilir olmasına rağmen, ona referans veren hiç kimsenin onu değiştirmesine izin verilmez, böylece örneği etkili bir şekilde değişmez hale getirir (Java'nın bellek modelindeki tuhaflıklar nedeniyle, etkili bir şekilde değiştirilemeyen bir nesnenin iş parçacığı oluşturmasını sağlamanın iyi bir yolu olmadığını unutmayın. her erişime fazladan bir dolaylı aktarma seviyesi eklemeden güvenli değişmez anlambilim) Böyle bir nesneye başvuru kullanarak durumu kapsülleyen ve kapsüllenmiş durumu değiştirmek isteyen varlıkların, uygun durumu içeren yeni bir nesne oluşturmaları gerekir.

Yöntemler, altta yatan nesneyi mutasyona uğratmak ve bir referans döndürmek istemiyorum, çünkü yukarıdaki ikinci kalıbı uydurma görünümü veriyorlar. Yaklaşımın yararlı olabileceği birkaç durum vardır, ancak nesneleri değiştirecek kod, bunu yapacak gibi görünmelidir ; gibi kod myThing = myThing.xyz(q);çok daha az böyle bakışlarla nesne tespit mutasyona uğrar myThingbasitçe olduğundan daha myThing.xyz(q);.

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.