Mengapa itu tidak menjadi pola umum untuk menggunakan setter di konstruktor?

23

Accessor dan modifier (alias setter dan getter) berguna untuk tiga alasan utama:

  1. Mereka membatasi akses ke variabel.
    • Misalnya, variabel dapat diakses, tetapi tidak dimodifikasi.
  2. Mereka memvalidasi parameter.
  3. Mereka dapat menyebabkan beberapa efek samping.

Universitas, kursus online, tutorial, artikel blog, dan contoh kode di web semuanya menekankan tentang pentingnya pengakses dan pengubah, mereka hampir merasa seperti "harus memiliki" untuk kode saat ini. Jadi orang dapat menemukannya bahkan ketika mereka tidak memberikan nilai tambahan, seperti kode di bawah ini.

public class Cat {
    private int age;

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Yang telah dikatakan, sangat umum untuk menemukan pengubah yang lebih berguna, yang sebenarnya memvalidasi parameter dan melemparkan pengecualian atau mengembalikan boolean jika input yang tidak valid telah disediakan, sesuatu seperti ini:

/**
 * Sets the age for the current cat
 * @param age an integer with the valid values between 0 and 25
 * @return true if value has been assigned and false if the parameter is invalid
 */
public boolean setAge(int age) {
    //Validate your parameters, valid age for a cat is between 0 and 25 years
    if(age > 0 && age < 25) {
        this.age = age;
        return true;
    }
    return false;
}

Tetapi bahkan kemudian, saya hampir tidak pernah melihat pemodifikasi dipanggil dari sebuah konstruktor, jadi contoh paling umum dari kelas sederhana yang saya hadapi adalah:

public class Cat {
    private int age;

    public Cat(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }

    /**
     * Sets the age for the current cat
     * @param age an integer with the valid values between 0 and 25
     * @return true if value has been assigned and false if the parameter is invalid
     */
    public boolean setAge(int age) {
        //Validate your parameters, valid age for a cat is between 0 and 25 years
        if(age > 0 && age < 25) {
            this.age = age;
            return true;
        }
        return false;
    }

}

Tetapi orang akan berpikir bahwa pendekatan kedua ini jauh lebih aman:

public class Cat {
    private int age;

    public Cat(int age) {
        //Use the modifier instead of assigning the value directly.
        setAge(age);
    }

    public int getAge() {
        return this.age;
    }

    /**
     * Sets the age for the current cat
     * @param age an integer with the valid values between 0 and 25
     * @return true if value has been assigned and false if the parameter is invalid
     */
    public boolean setAge(int age) {
        //Validate your parameters, valid age for a cat is between 0 and 25 years
        if(age > 0 && age < 25) {
            this.age = age;
            return true;
        }
        return false;
    }

}

Apakah Anda melihat pola yang sama dalam pengalaman Anda atau hanya karena saya tidak beruntung? Dan jika Anda melakukannya, lalu apa yang menurut Anda menyebabkan itu? Apakah ada kerugian yang jelas untuk menggunakan pengubah dari konstruktor atau mereka hanya dianggap lebih aman? Apakah ini sesuatu yang lain?

Vlad Spreys
sumber
1
@stg, terima kasih, poin menarik! Bisakah Anda mengembangkannya dan memasukkannya ke dalam jawaban dengan contoh hal buruk yang mungkin terjadi?
Vlad Spreys
8
@stg Sebenarnya, ini merupakan anti-pola untuk menggunakan metode yang dapat ditimpa dalam konstruktor dan banyak alat kualitas kode akan menunjukkannya sebagai kesalahan. Anda tidak tahu apa yang akan dilakukan versi yang ditimpa dan mungkin melakukan hal-hal buruk seperti membiarkan "ini" melarikan diri sebelum konstruktor selesai yang dapat menyebabkan semua jenis masalah aneh.
Michał Kosmulski
1
@gnat: Saya tidak percaya ini adalah duplikat dari Haruskah metode kelas memanggil getter dan setter sendiri? , karena sifat panggilan konstruktor yang dipanggil pada objek yang tidak sepenuhnya terbentuk.
Greg Burghardt
3
Perhatikan juga bahwa "validasi" Anda hanya membuat usia tidak diinisialisasi (atau nol, atau ... sesuatu yang lain, tergantung pada bahasa). Jika Anda ingin konstruktor gagal, Anda harus memeriksa nilai kembali dan melemparkan pengecualian pada kegagalan. Seperti berdiri, validasi Anda baru saja memperkenalkan bug yang berbeda.
berguna

Jawaban:

37

Alasan filosofis yang sangat umum

Biasanya, kami meminta konstruktor memberikan (sebagai kondisi pasca) beberapa jaminan tentang keadaan objek yang dibangun.

Biasanya, kami juga berharap bahwa metode instan dapat mengasumsikan (sebagai prasyarat) bahwa jaminan ini sudah berlaku ketika mereka dipanggil, dan mereka hanya harus memastikan untuk tidak melanggarnya.

Memanggil metode instance dari dalam konstruktor berarti beberapa atau semua jaminan itu mungkin belum dibuat, yang membuat sulit untuk berpikir tentang apakah pra-kondisi metode instance terpenuhi. Bahkan jika Anda melakukannya dengan benar, itu bisa sangat rapuh di wajah, misalnya. memesan kembali panggilan metode instan atau operasi lainnya.

Bahasa juga bervariasi dalam cara mereka menyelesaikan panggilan ke metode contoh yang diwarisi dari kelas dasar / diganti oleh sub-kelas, sementara konstruktor masih berjalan. Ini menambah lapisan kompleksitas lainnya.

Contoh spesifik

  1. Contoh Anda sendiri tentang bagaimana menurut Anda seharusnya terlihat salah:

    public Cat(int age) {
        //Use the modifier instead of assigning the value directly.
        setAge(age);
    }

    ini tidak memeriksa nilai pengembalian dari setAge. Rupanya memanggil setter bukanlah jaminan kebenaran.

  2. Kesalahan yang sangat mudah seperti tergantung pada urutan inisialisasi, seperti:

    class Cat {
      private Logger m_log;
      private int m_age;
    
      public void setAge(int age) {
        // FIXME temporary debug logging
        m_log.write("=== DEBUG: setting age ===");
        m_age = age;
      }
    
      public Cat(int age, Logger log) {
        setAge(age);
        m_log = log;
      }
    };

    di mana penebangan sementara saya merusak segalanya. Aduh!

Ada juga bahasa seperti C ++ di mana memanggil setter dari konstruktor berarti inisialisasi default yang terbuang (yang untuk beberapa variabel anggota setidaknya layak dihindari)

Usulan sederhana

Memang benar bahwa sebagian besar kode tidak ditulis seperti ini, tetapi jika Anda ingin menjaga konstruktor Anda tetap bersih dan dapat diprediksi, dan masih menggunakan kembali logika pra dan pasca kondisi Anda, solusi yang lebih baik adalah:

class Cat {
  private int m_age;
  private static void checkAge(int age) {
    if (age > 25) throw CatTooOldError(age);
  }

  public void setAge(int age) {
    checkAge(age);
    m_age = age;
  }

  public Cat(int age) {
    checkAge(age);
    m_age = age;
  }
};

atau bahkan lebih baik, jika mungkin: menyandikan kendala dalam tipe properti, dan minta nilai validasinya sendiri pada penugasan:

class Cat {
  private Constrained<int, 25> m_age;

  public void setAge(int age) {
    m_age = age;
  }

  public Cat(int age) {
    m_age = age;
  }
};

Dan akhirnya, untuk kelengkapan lengkap, pembungkus validasi diri dalam C ++. Perhatikan bahwa meskipun masih melakukan validasi yang membosankan, karena kelas ini tidak melakukan hal lain , ini relatif mudah untuk diperiksa

template <typename T, T MAX, T MIN=T{}>
class Constrained {
    T val_;
    static T validate(T v) {
        if (MIN <= v && v <= MAX) return v;
        throw std::runtime_error("oops");
    }
public:
    Constrained() : val_(MIN) {}
    explicit Constrained(T v) : val_(validate(v)) {}

    Constrained& operator= (T v) { val_ = validate(v); return *this; }
    operator T() { return val_; }
};

OK, itu tidak benar-benar selesai, saya telah meninggalkan berbagai salinan dan memindahkan konstruktor dan tugas.

Tak berguna
sumber
4
Jawaban undervotated berat! Izinkan saya menambahkan hanya karena OP telah melihat situasi yang dijelaskan dalam buku teks tidak menjadikannya solusi yang baik. Jika ada dua cara di kelas untuk mengatur atribut (dengan constructor dan setter), dan seseorang ingin menegakkan batasan pada atribut itu, semua kedua cara harus memeriksa kendala, jika tidak itu adalah bug desain .
Doc Brown
Jadi, apakah Anda mempertimbangkan menggunakan metode penyetel dalam praktik buruk konstruktor jika tidak ada 1) tidak ada logika di setter, atau 2) logika di setter tidak tergantung pada negara? Saya tidak sering melakukannya tetapi saya pribadi telah menggunakan metode penyetel dalam sebuah konstruktor tepat untuk alasan yang terakhir (ketika ada beberapa jenis kondisi di dalam penyetel yang harus selalu berlaku untuk variabel anggota
Chris Maggiulli
Jika ada beberapa jenis kondisi yang harus selalu berlaku, itu adalah logika di setter. Saya tentu berpikir lebih baik jika memungkinkan untuk menyandikan logika ini di anggota, sehingga dipaksa untuk mandiri, dan tidak dapat memperoleh dependensi pada seluruh kelas.
berguna
13

Ketika suatu objek sedang dibangun, itu secara definisi tidak sepenuhnya terbentuk. Pertimbangkan bagaimana setter yang Anda berikan:

public boolean setAge(int age) {
    //Validate your parameters, valid age for a cat is between 0 and 25 years
    if(age > 0 && age < 25) {
        this.age = age;
        return true;
    }
    return false;
}

Bagaimana jika bagian dari validasi setAge()termasuk cek terhadap ageproperti untuk memastikan bahwa objek hanya dapat bertambah umur? Kondisi itu mungkin terlihat seperti:

if (age > this.age && age < 25) {
    //...

Itu sepertinya perubahan yang sangat tidak bersalah, karena kucing hanya dapat bergerak melalui waktu dalam satu arah. Satu-satunya masalah adalah bahwa Anda tidak dapat menggunakannya untuk menetapkan nilai awal this.agekarena mengasumsikan this.agesudah memiliki nilai yang valid.

Menghindari setter dalam konstruktor membuatnya jelas bahwa satu satunya hal yang dilakukan konstruktor adalah mengatur variabel instan dan melewatkan perilaku lain yang terjadi pada setter.

Caleb
sumber
OKE ... tetapi, jika Anda tidak benar-benar menguji konstruksi objek dan mengekspos bug potensial, ada bug potensial baik cara . Dan sekarang Anda memiliki dua dua masalah: Anda memiliki bug potensial yang tersembunyi dan Anda harus memberlakukan pemeriksaan rentang usia di dua tempat.
svidgen
Tidak ada masalah, karena di Java / C # semua bidang di-zeroed sebelum konstruktor-doa.
Deduplicator
2
Ya, memanggil metode instance dari konstruktor mungkin sulit untuk dipikirkan, dan umumnya tidak disukai. Sekarang, jika Anda ingin memindahkan logika validasi Anda ke metode kelas / statis statis pribadi, dan memanggilnya dari konstruktor dan setter, itu akan baik-baik saja.
berguna
1
Tidak, metode non-instance menghindari rumitnya metode instance, karena mereka tidak menyentuh instance yang dibangun sebagian. Anda tentu saja masih perlu memeriksa hasil validasi untuk memutuskan apakah konstruksi berhasil.
berguna
1
Jawaban ini adalah IMHO melewatkan intinya. "Ketika suatu objek sedang dibangun, itu secara definisi tidak sepenuhnya terbentuk" - di tengah-tengah proses konstruksi, tentu saja, tetapi ketika konstruktor dilakukan, segala kendala yang dimaksudkan pada atribut harus dipegang, jika tidak seseorang dapat menghindari kendala itu. Saya akan menyebutnya cacat desain yang parah.
Doc Brown
6

Beberapa jawaban yang baik sudah ada di sini, tetapi sejauh ini belum ada yang menyadari bahwa Anda bertanya di sini dua hal dalam satu, yang menyebabkan kebingungan. IMHO pos Anda paling baik dilihat sebagai dua pertanyaan terpisah :

  1. jika ada validasi kendala dalam setter, bukankah harus juga di konstruktor kelas untuk memastikan itu tidak dapat dilewati?

  2. mengapa tidak memanggil setter langsung untuk tujuan ini dari konstruktor?

Jawaban untuk pertanyaan pertama jelas "ya". Konstruktor, ketika tidak membuang pengecualian, harus meninggalkan objek dalam keadaan valid, dan jika nilai-nilai tertentu dilarang untuk atribut tertentu, maka sama sekali tidak masuk akal untuk membiarkan ctor mengelak dari ini.

Namun, jawaban untuk pertanyaan kedua biasanya "tidak", selama Anda tidak mengambil tindakan untuk menghindari mengesampingkan setter di kelas turunan. Oleh karena itu, alternatif yang lebih baik adalah menerapkan validasi kendala dalam metode pribadi yang dapat digunakan kembali dari setter maupun dari konstruktor. Saya tidak akan mengulangi contoh @Useless di sini, yang menunjukkan persis desain ini.

Doc Brown
sumber
Saya masih tidak mengerti mengapa lebih baik untuk mengekstrak logika dari setter sehingga konstruktor dapat menggunakannya. ... Bagaimana ini benar-benar berbeda dari sekadar memanggil setter dalam urutan yang masuk akal ?? Terutama mengingat bahwa, jika setter Anda sesederhana "seharusnya", satu-satunya bagian dari setter konstruktor Anda, pada titik ini, tidak meminta adalah pengaturan aktual dari bidang yang mendasarinya ...
svidgen
2
@viden: lihat contoh JimmyJames: singkatnya, itu bukan ide yang baik untuk menggunakan metode yang dapat ditimpa dalam sebuah ctor, yang akan menyebabkan masalah. Anda dapat menggunakan setter di ctor jika final dan tidak memanggil metode lain yang bisa ditimpa. Selain itu, memisahkan validasi dari cara menanganinya ketika gagal menawarkan Anda kesempatan untuk menanganinya secara berbeda di ctor dan di setter.
Doc Brown
Yah, aku bahkan kurang yakin sekarang! Tapi, terima kasih telah mengarahkan saya ke jawaban yang lain ...
svidgen
2
Dengan dalam urutan yang wajar yang Anda maksud dengan rapuh sebuah, tak terlihat ketergantungan pemesanan mudah rusak oleh tampak tak berdosa perubahan , kan?
berguna
@tidak baik, tidak ... Maksudku, itu lucu bahwa Anda akan menyarankan urutan di mana kode Anda dieksekusi seharusnya tidak masalah. tapi, itu bukan intinya. Di luar contoh buat, aku tidak bisa mengingat sebuah contoh dalam pengalaman saya di mana saya sudah berpikir itu adalah ide yang baik untuk memiliki validasi silang properti di setter - hal semacam itu mengarah tentu untuk "tak terlihat" persyaratan doa-order . Terlepas dari apakah doa-doa itu terjadi di konstruktor ..
svidgen
2

Berikut adalah sedikit kode Java konyol yang menunjukkan jenis masalah yang dapat Anda hadapi dengan menggunakan metode non-final dalam konstruktor Anda:

import java.util.regex.Pattern;

public class Problem
{
  public static final void main(String... args)
  {
    Problem problem = new Problem("John Smith");
    Problem next = new NextProblem("John Smith", " ");

    System.out.println(problem.getName());
    System.out.println(next.getName());
  }

  private String name;

  Problem(String name)
  {
    setName(name);
  }

  void setName(String name)
  {
    this.name = name;
  }

  String getName()
  {
    return name;
  }
}

class NextProblem extends Problem
{
  private String firstName;
  private String lastName;
  private final String delimiter;

  NextProblem(String name, String delimiter)
  {
    super(name);

    this.delimiter = delimiter;
  }

  void setName(String name)
  {
    String[] parts = name.split(Pattern.quote(delimiter));

    this.firstName = parts[0];
    this.lastName = parts[1];
  }

  String getName()
  {
    return firstName + " " + lastName;
  }
}

Ketika saya menjalankan ini, saya mendapatkan NPE di konstruktor NextProblem. Ini adalah contoh sepele tentu saja tetapi hal-hal dapat menjadi rumit dengan cepat jika Anda memiliki beberapa tingkat warisan.

Saya pikir alasan yang lebih besar ini tidak menjadi umum adalah karena itu membuat kode sedikit lebih sulit untuk dipahami. Secara pribadi, saya hampir tidak pernah memiliki metode setter dan variabel anggota saya hampir selalu final (pun intended). Karena itu, nilai harus ditetapkan dalam konstruktor. Jika Anda menggunakan objek yang tidak dapat diubah (dan ada banyak alasan bagus untuk melakukannya) pertanyaannya bisa diperdebatkan.

Bagaimanapun, itu adalah tujuan yang baik untuk menggunakan kembali validasi atau logika lain dan Anda dapat memasukkannya ke metode statis dan memintanya dari konstruktor dan setter.

JimmyJames
sumber
Bagaimana ini sama sekali masalah menggunakan setter dalam konstruktor alih-alih masalah warisan yang diimplementasikan dengan buruk ??? Dengan kata lain, jika Anda secara efektif membatalkan konstruktor dasar, mengapa Anda bahkan menyebutnya !?
svidgen
1
Saya pikir intinya adalah, bahwa mengesampingkan seorang setter, seharusnya tidak membatalkan konstruktor.
Chris Wohlert
@ChrisWohlert Oke ... tetapi, jika Anda mengubah logika setter Anda menjadi konflik dengan logika konstruktor Anda, itu masalah terlepas dari apakah konstruktor Anda menggunakan setter. Entah itu gagal pada waktu konstruksi, gagal nanti, atau gagal tak terlihat (b / c Anda telah melewati aturan bisnis Anda).
svidgen
Anda sering tidak tahu bagaimana Konstruktor kelas dasar diimplementasikan (mungkin di perpustakaan pihak ke-3 di suatu tempat). Meng-override metode overridable seharusnya tidak memutus kontrak implementasi basis, (dan bisa dibilang contoh ini melakukannya dengan tidak menyetel namebidang).
Hulk
@viden masalahnya di sini adalah bahwa memanggil metode yang dapat ditimpa dari konstruktor mengurangi kegunaan menimpa mereka - Anda tidak dapat menggunakan bidang apa pun dari kelas anak dalam versi yang diganti karena mereka mungkin belum diinisialisasi. Yang lebih buruk, Anda tidak boleh melewatkan this-referensi ke beberapa metode lain, karena Anda belum dapat memastikan sepenuhnya diinisialisasi.
Hulk
1

Tergantung!

Jika Anda melakukan validasi sederhana atau mengeluarkan efek samping nakal di setter yang perlu dipukul setiap kali properti disetel: gunakan setter. Alternatifnya adalah dengan mengekstrak logika setter dari setter dan memanggil metode baru diikuti oleh set aktual dari setter dan konstruktor - yang secara efektif sama dengan menggunakan setter! (Kecuali sekarang Anda mempertahankan, setter dua baris yang diakui, kecil di dua tempat.)

Namun , jika Anda perlu melakukan operasi kompleks untuk mengatur objek sebelum setter tertentu (atau dua!) Berhasil dieksekusi, jangan gunakan setter.

Dan dalam kedua kasus, ada kasus uji . Terlepas dari rute mana yang Anda tuju, jika Anda memiliki unit test, Anda akan memiliki peluang bagus untuk mengekspos perangkap yang terkait dengan salah satu opsi.


Saya juga akan menambahkan, jika ingatan saya dari masa lalu itu bahkan jauh akurat, ini adalah semacam alasan saya diajarkan di perguruan tinggi juga. Dan, itu sama sekali tidak sejalan dengan proyek-proyek sukses yang saya punya hak istimewa untuk dikerjakan.

Jika saya harus menebak, saya melewatkan memo "kode bersih" di beberapa titik yang mengidentifikasi beberapa bug khas yang dapat timbul dari menggunakan setter dalam konstruktor. Tapi, untuk bagian saya, saya belum menjadi korban bug seperti itu ...

Jadi, saya berpendapat bahwa keputusan khusus ini tidak perlu menyapu dan dogmatis.

svidgen
sumber