Masalah gaya pengkodean: Haruskah kita memiliki fungsi yang mengambil parameter, memodifikasinya, dan kemudian MENGEMBALIKAN parameter itu?

19

Saya sedang berdebat dengan teman saya mengenai apakah kedua latihan ini hanyalah dua sisi dari mata uang yang sama, atau apakah satu benar-benar lebih baik.

Kami memiliki fungsi yang mengambil parameter, mengisi anggota itu, dan kemudian mengembalikannya:

Item predictPrice(Item item)

Saya percaya bahwa saat ia bekerja pada objek yang sama yang dilewatkan, tidak ada gunanya mengembalikan item. Bahkan, jika ada, dari sudut pandang penelepon, itu membingungkan karena Anda bisa mengharapkannya untuk mengembalikan item baru yang tidak.

Dia mengklaim bahwa tidak ada bedanya, dan bahkan tidak masalah jika itu menciptakan Item baru dan mengembalikannya. Saya sangat tidak setuju, karena alasan berikut:

  • Jika Anda memiliki beberapa referensi ke item yang lewat (atau pointer atau apa pun), itu mengalokasikan objek baru dan mengembalikannya adalah penting secara materi karena referensi tersebut akan salah.

  • Dalam bahasa yang dikelola non-memori, fungsi yang mengalokasikan instance baru mengklaim kepemilikan memori, dan dengan demikian kita harus menerapkan metode pembersihan yang disebut di beberapa titik.

  • Mengalokasikan pada heap berpotensi mahal, dan karena itu penting untuk apakah fungsi yang dipanggil melakukan itu.

Oleh karena itu, saya percaya sangat penting untuk dapat melihat melalui metode tanda tangan apakah itu memodifikasi objek, atau mengalokasikan yang baru. Sebagai hasilnya, saya percaya bahwa karena fungsinya hanya memodifikasi objek yang dilewatkan, tanda tangannya harus:

void predictPrice(Item item)

Di setiap basis kode (diakui C dan C ++ basis kode, bukan Java yang merupakan bahasa tempat kami bekerja) saya telah bekerja dengan, gaya di atas pada dasarnya telah dipatuhi, dan dipelihara oleh programmer yang jauh lebih berpengalaman. Dia mengklaim bahwa ukuran sampel basis kode dan kolega saya kecil dari semua basis kode dan kolega yang mungkin, dan dengan demikian pengalaman saya bukanlah indikator yang benar tentang apakah seseorang lebih unggul.

Jadi, ada pemikiran?

Squimmy
sumber
strcat memodifikasi parameter di tempat dan masih mengembalikannya. Haruskah strcat kembali batal?
Jerry Jeremiah
Java dan C ++ memiliki perilaku yang sangat berbeda sehubungan dengan melewati parameter. Jika Itemini class Item ...dan tidak typedef ...& Item, lokal itemsudah salinan
Caleth
google.github.io/styleguide/cppguide.html#Output_Parameters Google lebih suka menggunakan nilai RETURN untuk output
Firegun

Jawaban:

26

Ini benar-benar masalah pendapat, tetapi untuk apa nilainya, saya merasa menyesatkan untuk mengembalikan barang yang dimodifikasi jika barang tersebut dimodifikasi di tempat. Secara terpisah, jika predictPriceakan memodifikasi item, item tersebut harus memiliki nama yang mengindikasikan akan melakukan hal itu (suka setPredictPriceatau semacamnya).

Saya lebih suka (dalam urutan)

  1. Itu predictPriceadalah metodeItem (modulo alasan yang bagus untuk itu tidak ada, yang mungkin ada dalam desain keseluruhan Anda) yang baik

    • Mengembalikan harga yang diprediksi, atau

    • Punya nama seperti setPredictedPricegantinya

  2. Itu predictPrice tidak berubah Item, tetapi mengembalikan harga yang diprediksi

  3. Itu predictPriceadalah voidmetode yang disebutsetPredictedPrice

  4. Itu predictPricedikembalikan this(untuk metode chaining ke metode lain pada contoh apa pun itu adalah bagian dari) (dan dipanggil setPredictedPrice)

TJ Crowder
sumber
1
Terima kasih balasannya. Sehubungan dengan 1, kami tidak dapat memiliki prediktif Harga di dalam Item. 2 adalah saran yang bagus - saya pikir itu mungkin solusi yang tepat di sini.
Squimmy
2
@ Squimmy: Dan tentu saja, saya hanya menghabiskan 15 menit berurusan dengan bug yang disebabkan oleh seseorang yang menggunakan persis pola yang Anda perdebatkan: a = doSomethingWith(b) dimodifikasi b dan dikembalikan dengan modifikasi, yang meledakkan kode saya kemudian pada pemikiran yang tahu apa yang btelah di dalamnya. :-)
TJ Crowder
11

Dalam paradigma desain berorientasi objek, benda tidak boleh memodifikasi objek di luar objek itu sendiri. Setiap perubahan pada keadaan suatu objek harus dilakukan melalui metode pada objek.

Jadi, void predictPrice(Item item)sebagai fungsi anggota dari beberapa kelas lain salah . Bisa saja dapat diterima pada zaman C, tetapi untuk Java dan C ++, modifikasi suatu objek menyiratkan penggandaan yang lebih dalam pada objek yang kemungkinan akan menyebabkan masalah desain lainnya di jalan (ketika Anda refactor kelas dan mengubah bidangnya, sekarang 'predictPrice' perlu diubah ke dalam beberapa file lain.

Mengembalikan objek baru, tidak memiliki efek samping terkait, parameter yang dikirimkan tidak diubah. Anda (metode predictPrice) tidak tahu di mana lagi parameter itu digunakan. Apakah Itemkunci hash di suatu tempat? apakah Anda mengubah kode hash dalam melakukan ini? Apakah ada orang lain yang mengharapkannya agar tidak berubah?

Masalah-masalah desain ini adalah masalah-masalah yang sangat menyarankan bahwa Anda tidak boleh memodifikasi objek (saya berpendapat dalam hal kekekalan dalam banyak kasus), dan jika Anda melakukannya, perubahan-perubahan pada keadaan harus dikendalikan dan dikendalikan oleh objek itu sendiri daripada sesuatu. lain di luar kelas.


Mari kita lihat apa yang terjadi jika seseorang mengotak-atik bidang sesuatu dalam hash. Mari kita ambil beberapa kode:

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

Dan saya akui bahwa ini bukan kode terhebat (secara langsung mengakses bidang), tetapi ini berfungsi untuk menunjukkan masalah dengan data yang dapat diubah digunakan sebagai kunci ke HashMap, atau dalam hal ini, hanya dimasukkan ke dalam HashSet.

Output dari kode ini adalah:

true
false

Apa yang terjadi adalah kode hash yang digunakan ketika dimasukkan adalah tempat objek berada di hash. Mengubah nilai yang digunakan untuk menghitung kode hash tidak menghitung ulang hash itu sendiri. Ini adalah bahaya menempatkan setiap objek bisa berubah sebagai kunci untuk hash.

Jadi, kembali ke aspek asli dari pertanyaan, metode yang dipanggil tidak "tahu" bagaimana objek itu mendapatkan sebagai parameter yang digunakan. Menyediakan "mari bermutasi objek" sebagai satu-satunya cara untuk melakukan ini berarti ada sejumlah bug halus yang dapat merayap masuk. Seperti kehilangan nilai dalam hash ... kecuali jika Anda menambahkan lebih banyak dan hash akan diulang kembali - percayalah padaku , itu bug jahat untuk diburu (saya kehilangan nilainya sampai saya menambahkan 20 item lagi ke hashMap dan kemudian tiba-tiba muncul lagi).

  • Kecuali ada alasan yang sangat bagus untuk memodifikasi objek, mengembalikan objek baru adalah hal paling aman untuk dilakukan.
  • Ketika ada adalah alasan yang baik untuk memodifikasi objek, modifikasi yang harus dilakukan oleh obyek itu sendiri (metode menyerukan) daripada melalui beberapa fungsi eksternal yang dapat bermalas bidangnya.
    • Ini memungkinkan objek untuk di refactored dengan biaya perawatan yang lebih rendah.
    • Ini memungkinkan objek untuk memastikan bahwa nilai-nilai yang menghitung kode hash-nya tidak berubah (atau bukan bagian dari perhitungan kode hash-nya)

Terkait: Menimpa dan mengembalikan nilai argumen yang digunakan sebagai persyaratan pernyataan if, di dalam pernyataan if yang sama

Komunitas
sumber
3
Kedengarannya tidak benar. Kemungkinan besar predictPricetidak boleh secara langsung mengutak-atik Itemanggota, tetapi apa yang salah dengan metode panggilan Itemyang melakukannya? Jika itu adalah satu-satunya objek yang sedang dimodifikasi, mungkin Anda dapat membuat argumen bahwa itu harus menjadi metode objek yang sedang dimodifikasi, tetapi di lain waktu beberapa objek (yang tentunya tidak boleh dari kelas yang sama) sedang dimodifikasi, terutama di metode tingkat tinggi.
@delnan Saya akan mengakui bahwa ini mungkin reaksi (lebih) dari bug yang pernah saya lacak ketika nilai menghilang dan muncul kembali dalam hash - seseorang mengotak-atik bidang objek dalam hash setelah dimasukkan alih-alih membuat salinan objek untuk penggunaannya. Ada langkah-langkah pemrograman defensif yang dapat diambil (mis. Objek yang tidak dapat diubah) - tetapi hal-hal seperti mengkomputasi ulang nilai dalam objek itu sendiri ketika Anda bukan "pemilik" objek, atau objek itu sendiri adalah sesuatu yang dapat dengan mudah kembali menggigit kamu keras.
Anda tahu, ada argumen yang bagus untuk menggunakan lebih banyak fungsi bebas alih-alih fungsi kelas untuk meningkatkan enkapsulasi. Secara alami, fungsi yang mendapatkan objek konstan (baik implisit atau parameter) tidak boleh mengubahnya dengan cara apa pun yang dapat diamati (beberapa objek konstan mungkin menyimpan sesuatu).
Deduplicator
2

Saya selalu mengikuti praktik tidak mengubah objek orang lain. Dengan kata lain, hanya bermutasi objek yang dimiliki oleh kelas / struct / yang Anda miliki di dalam.

Diberikan kelas data berikut:

class Item {
    public double price = 0;
}

Ini bagus:

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

Dan ini sangat jelas:

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

Tapi ini terlalu berlumpur dan misterius untuk seleraku:

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
Ben Leggiero
sumber
Tolong temani downvotes dengan komentar jadi saya tahu bagaimana menulis jawaban yang lebih baik di masa depan :)
Ben Leggiero
1

Sintaks berbeda antara bahasa yang berbeda, dan tidak ada artinya untuk menunjukkan sepotong kode yang tidak spesifik untuk bahasa karena itu berarti hal yang sama sekali berbeda dalam bahasa yang berbeda.

Di Jawa, Itemadalah tipe referensi - itu adalah jenis pointer ke objek yang merupakan contoh Item. Dalam C ++, tipe ini ditulis sebagai Item *(sintaks untuk hal yang sama berbeda antara bahasa). Jadi Item predictPrice(Item item)di Jawa sama dengan Item *predictPrice(Item *item)di C ++. Dalam kedua kasus, itu adalah fungsi (atau metode) yang mengambil pointer ke objek, dan mengembalikan pointer ke objek.

Dalam C ++, Item(tanpa apa pun) adalah tipe objek (dengan asumsi Itemadalah nama kelas). Nilai dari tipe ini "adalah" objek, dan Item predictPrice(Item item)mendeklarasikan fungsi yang mengambil objek dan mengembalikan objek. Karena &tidak digunakan, itu dilewatkan dan dikembalikan dengan nilai. Itu berarti objek disalin ketika melewati, dan disalin ketika kembali. Tidak ada padanan di Java karena Java tidak memiliki tipe objek.

Jadi yang mana? Banyak hal yang Anda tanyakan dalam pertanyaan (mis. "Saya percaya bahwa ketika ia bekerja pada objek yang sama dengan yang dilewati ...") tergantung pada pemahaman persis apa yang sedang berlalu dan dikembalikan ke sini.

pengguna102008
sumber
1

Kebiasaan yang saya lihat dari basis kode adalah untuk memilih gaya yang jelas dari sintaksis. Jika fungsi berubah nilainya, lebih suka lewat pointer

void predictPrice(Item * const item);

Gaya ini menghasilkan:

  • Sebut saja Anda lihat:

    predictPrice (& my_item);

dan Anda tahu itu akan dimodifikasi, oleh kepatuhan Anda terhadap gaya.

  • Kalau tidak, pilih lewat ref ref atau nilai ketika Anda tidak ingin fungsi untuk memodifikasi instance.
pengguna27886
sumber
Perlu dicatat bahwa, meskipun sintaks ini jauh lebih jelas, itu tidak tersedia di Jawa.
Ben Leggiero
0

Meskipun saya tidak memiliki preferensi yang kuat, saya akan memilih bahwa mengembalikan objek mengarah pada fleksibilitas yang lebih baik untuk API.

Perhatikan bahwa itu hanya masuk akal sementara metode memiliki kebebasan untuk mengembalikan objek lain. Jika javadoc Anda mengatakan @returns the same value of parameter aitu tidak ada gunanya. Tetapi jika javadoc Anda mengatakan @return an instance that holds the same data than parameter a, maka mengembalikan instance yang sama atau yang lain adalah detail implementasi.

Misalnya, dalam proyek saya saat ini (Java EE) saya memiliki lapisan bisnis; untuk menyimpan dalam DB (dan menetapkan ID otomatis) seorang Karyawan, katakanlah, seorang karyawan saya memiliki sesuatu seperti

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

Tentu saja, tidak ada perbedaan dengan implementasi ini karena JPA mengembalikan objek yang sama setelah menetapkan ID. Sekarang, jika saya beralih ke JDBC

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

Sekarang jam ????? Saya punya tiga opsi.

  1. Tentukan setter untuk Id, dan saya akan mengembalikan objek yang sama. Jelek, karena setIdakan tersedia di mana-mana.

  2. Lakukan beberapa pekerjaan refleksi berat dan atur id di Employeeobjek. Kotor, tetapi setidaknya itu terbatas pada createEmployeecatatan.

  3. Berikan konstruktor yang menyalin yang asli Employeedan menerima juga iduntuk mengatur.

  4. Ambil objek dari DB (mungkin diperlukan jika beberapa nilai bidang dihitung dalam DB, atau jika Anda ingin mengisi hubungan realtionship).

Sekarang, untuk 1. dan 2. Anda mungkin mengembalikan instance yang sama, tetapi untuk 3. dan 4. Anda akan mengembalikan instance baru. Jika dari prinsip Anda menetapkan dalam API Anda bahwa nilai "nyata" akan menjadi yang dikembalikan oleh metode, Anda memiliki lebih banyak kebebasan.

Satu-satunya argumen nyata yang dapat saya pikirkan adalah bahwa, dengan menggunakan implementatios 1. atau 2., orang yang menggunakan API Anda mungkin mengabaikan penetapan hasil dan kode mereka akan rusak jika Anda berubah menjadi 3. atau 4. implementasi).

Bagaimanapun, seperti yang Anda lihat, preferensi saya didasarkan pada preferensi pribadi lainnya (seperti melarang setter untuk Id), jadi mungkin itu tidak berlaku untuk semua orang.

SJuan76
sumber
0

Saat melakukan pengkodean di Java, kita harus mencoba membuat penggunaan setiap objek dengan tipe yang bisa berubah sesuai dengan satu dari dua pola:

  1. Satu entitas yang dianggap sebagai "pemilik" objek yang dapat berubah dan menganggap status objek sebagai bagian dari miliknya. Entitas lain mungkin memegang referensi, tetapi harus menganggap referensi sebagai mengidentifikasi objek yang dimiliki oleh orang lain.

  2. Meskipun objek dapat berubah, tidak ada orang yang memegang referensi padanya diizinkan untuk memutasinya, sehingga membuat instance secara efektif tidak dapat diubah (perhatikan bahwa karena kebiasaan dalam model memori Java, tidak ada cara yang baik untuk membuat objek yang dapat diubah secara efektif memiliki thread- semantik abadi yang aman tanpa menambahkan tingkat tipuan ekstra untuk setiap akses). Setiap entitas yang merangkum negara menggunakan referensi ke objek seperti itu dan ingin mengubah negara yang dienkapsulasi harus membuat objek baru yang berisi keadaan yang tepat.

Saya tidak suka memiliki metode bermutasi objek yang mendasarinya dan mengembalikan referensi, karena mereka memberikan penampilan yang pas dengan pola kedua di atas. Ada beberapa kasus di mana pendekatannya mungkin membantu, tetapi kode yang akan bermutasi objek akan terlihat seperti itu akan melakukannya; kode seperti myThing = myThing.xyz(q);terlihat jauh lebih sedikit seperti itu bermutasi objek yang diidentifikasi myThingdaripada hanya myThing.xyz(q);.

supercat
sumber