Mengapa chaining setters tidak konvensional?

46

Memiliki rantai yang diimplementasikan pada kacang sangat berguna: tidak perlu untuk membebani konstruktor, konstruktor besar, pabrik, dan memberi Anda peningkatan keterbacaan. Saya tidak bisa memikirkan kelemahan, kecuali jika Anda ingin objek Anda tidak berubah , dalam hal ini tidak akan memiliki setter. Jadi adakah alasan mengapa ini bukan konvensi OOP?

public class DTO {

    private String foo;
    private String bar;

    public String getFoo() {
         return foo;
    }

    public String getBar() {
        return bar;
    }

    public DTO setFoo(String foo) {
        this.foo = foo;
        return this;
    }

    public DTO setBar(String bar) {
        this.bar = bar;
        return this;
    }

}

//...//

DTO dto = new DTO().setFoo("foo").setBar("bar");
Ben
sumber
32
Karena Jawa mungkin satu-satunya bahasa di mana setter bukan kekejian bagi manusia ...
Telastyn
11
Ini gaya yang buruk karena nilai kembali tidak bermakna secara semantik. Itu menyesatkan. Satu-satunya keuntungan adalah menyimpan sedikit penekanan tombol.
usr
58
Pola ini tidak jarang sama sekali. Bahkan ada nama untuk itu. Ini disebut antarmuka yang lancar .
Philipp
9
Anda juga dapat mengabstraksi ciptaan ini dalam builder untuk hasil yang lebih mudah dibaca. myCustomDTO = DTOBuilder.defaultDTO().withFoo("foo").withBar("bar").Build();Saya akan melakukannya, agar tidak bertentangan dengan gagasan umum bahwa setter kosong.
9
@ Pilip, sementara secara teknis Anda benar, tidak akan mengatakan itu new Foo().setBar('bar').setBaz('baz')terasa sangat "lancar". Maksudku, tentu saja bisa diimplementasikan dengan cara yang persis sama, tetapi saya sangat berharap untuk membaca sesuatu yang lebih miripFoo().barsThe('bar').withThe('baz').andQuuxes('the quux')
Wayne Werner

Jawaban:

50

Jadi adakah alasan mengapa ini bukan konvensi OOP?

Tebakan terbaik saya: karena melanggar CQS

Anda telah mendapat perintah (mengubah status objek) dan kueri (mengembalikan salinan status - dalam hal ini, objek itu sendiri) dicampur ke dalam metode yang sama. Itu tidak selalu menjadi masalah, tetapi itu melanggar beberapa pedoman dasar.

Misalnya, dalam C ++, std :: stack :: pop () adalah perintah yang mengembalikan void, dan std :: stack :: top () adalah kueri yang mengembalikan referensi ke elemen teratas di stack. Klasik, Anda ingin menggabungkan keduanya, tetapi Anda tidak bisa melakukan itu dan menjadi pengecualian aman. (Tidak masalah di Jawa, karena operator penugasan di Jawa tidak melempar).

Jika DTO adalah tipe nilai, Anda mungkin mencapai akhir yang sama dengannya

public DTO setFoo(String foo) {
    return new DTO(foo, this.bar);
}

public DTO setBar(String bar) {
    return new DTO(this.foo, bar);
}

Juga, merantai nilai kembali adalah rasa sakit yang luar biasa ketika Anda berurusan dengan warisan. Lihat "Pola templat berulang yang aneh"

Akhirnya, ada masalah bahwa konstruktor default harus meninggalkan Anda dengan objek yang dalam keadaan valid. Jika Anda harus menjalankan banyak perintah untuk mengembalikan objek ke status yang valid, ada yang salah.

VoiceOfUnasonason
sumber
4
Akhirnya, menjadi nyata di sini. Warisan adalah masalah sebenarnya. Saya pikir rantai bisa menjadi setter konvensional untuk data-representasi-objek yang digunakan dalam komposisi.
Ben
6
"mengembalikan salinan negara dari objek" - tidak melakukannya.
user253751
2
Saya berpendapat bahwa alasan terbesar untuk mengikuti pedoman tersebut (dengan beberapa pengecualian penting seperti iterator) adalah membuat kode ini jauh lebih mudah untuk dipikirkan.
jpmc26
1
@ MatthieuM. nggak. Saya terutama berbicara Jawa, dan lazim di sana dalam API "cairan" lanjutan - Saya yakin itu juga ada dalam bahasa lain. Pada dasarnya Anda membuat tipe Anda generik pada tipe yang memanjang itu sendiri. Anda kemudian menambahkan abstract T getSelf()metode dengan mengembalikan tipe generik. Sekarang, alih-alih kembali thisdari setter Anda, Anda return getSelf()dan kemudian setiap kelas utama hanya membuat tipe generik dalam dirinya sendiri dan kembali thisdari getSelf. Dengan cara ini setter mengembalikan tipe aktual dan bukan tipe deklarasi.
Boris the Spider
1
@ MatthieuM. Benarkah? Kedengarannya mirip dengan CRTP untuk C ++ ...
berguna
33
  1. Menyimpan beberapa penekanan tombol tidak menarik. Mungkin bagus, tetapi konvensi OOP lebih peduli pada konsep dan struktur, bukan penekanan tombol.

  2. Nilai kembali tidak ada artinya.

  3. Bahkan lebih daripada menjadi tidak berarti, nilai kembali menyesatkan, karena pengguna dapat mengharapkan nilai kembali memiliki arti. Mereka mungkin berharap bahwa itu adalah "penyetel abadi"

    public FooHolder {
        public FooHolder withFoo(int foo) {
            /* return a modified COPY of this FooHolder instance */
        }
    }
    

    Pada kenyataannya, setter Anda mengubah objek.

  4. Itu tidak bekerja dengan baik dengan warisan.

    public FooHolder {
        public FooHolder setFoo(int foo) {
            ...
        }
    }
    
    public BarHolder extends FooHolder {
        public FooHolder setBar(int bar) {
            ...
        }
    } 
    

    Saya bisa menulis

    new BarHolder().setBar(2).setFoo(1)

    tapi tidak

    new BarHolder().setFoo(1).setBar(2)

Bagi saya, # 1 hingga # 3 adalah yang penting. Kode yang ditulis dengan baik bukan tentang teks yang disusun dengan menyenangkan. Kode yang ditulis dengan baik adalah tentang konsep, hubungan, dan struktur dasar. Teks hanyalah refleksi luar dari arti sebenarnya dari kode.

Paul Draper
sumber
2
Namun, sebagian besar argumen ini berlaku untuk antarmuka yang fasih / berantai.
Casey
@Casey, ya. Pembangun (yaitu dengan setter) adalah kasus yang paling banyak saya lihat dari chaining.
Paul Draper
10
Jika Anda terbiasa dengan gagasan antarmuka yang lancar, # 3 tidak berlaku, karena Anda mungkin mencurigai antarmuka yang lancar dari jenis pengembalian. Saya juga harus tidak setuju dengan "Kode yang ditulis dengan baik bukan tentang teks yang disusun dengan menyenangkan". Bukan hanya itu masalahnya, tetapi teks yang disusun dengan baik agak penting, karena memungkinkan pembaca manusia program untuk memahaminya dengan sedikit usaha.
Michael Shaw
1
Setter chaining bukan tentang mengatur teks dengan menyenangkan atau tentang menyimpan penekanan tombol. Ini tentang mengurangi jumlah pengidentifikasi. Dengan setter chaining, Anda dapat membuat dan mengatur objek dalam satu ekspresi, yang berarti Anda mungkin tidak harus menyimpannya dalam variabel - variabel yang harus Anda beri nama , dan itu akan tetap di sana sampai akhir ruang lingkup.
Idan Arye
3
Tentang poin # 4, itu adalah sesuatu yang dapat diselesaikan di Jawa sebagai berikut: public class Foo<T extends Foo> {...}dengan setter kembali Foo<T>. Juga metode 'setter' itu, saya lebih suka menyebutnya metode 'dengan'. Jika kelebihan beban bekerja dengan baik, maka adil Foo<T> with(Bar b) {...}, jika tidak Foo<T> withBar(Bar b).
YoYo
12

Saya tidak berpikir ini adalah konvensi OOP, ini lebih terkait dengan desain bahasa dan konvensi.

Sepertinya Anda suka menggunakan Java. Java memiliki spesifikasi JavaBeans yang menentukan tipe pengembalian setter yang batal, yaitu bertentangan dengan rantai setter. Spesifikasi ini diterima secara luas dan diimplementasikan dalam berbagai alat.

Tentu saja Anda mungkin bertanya, mengapa tidak merantai bagian dari spesifikasinya. Saya tidak tahu jawabannya, mungkin pola ini tidak dikenal / populer saat itu.

qbd
sumber
Ya, JavaBeans adalah persis apa yang ada dalam pikiran saya.
Ben
Saya tidak berpikir bahwa sebagian besar aplikasi yang menggunakan JavaBeans peduli, tetapi mereka mungkin (menggunakan refleksi untuk mengambil metode yang bernama 'set ...', memiliki parameter tunggal, dan batal kembali ). Banyak program analisis statis mengeluh tentang tidak memeriksa nilai kembali dari metode yang mengembalikan sesuatu dan ini bisa sangat menjengkelkan.
@MichaelT panggilan reflektif untuk mendapatkan metode di Java tidak menentukan jenis kembali. Memanggil metode seperti itu kembali Object, yang dapat mengakibatkan singleton tipe Voiddikembalikan jika metode menentukan voidsebagai tipe pengembaliannya. Dalam berita lain, tampaknya "meninggalkan komentar tetapi tidak memberikan suara" adalah alasan untuk kegagalan audit "posting pertama" meskipun itu adalah komentar yang baik. Saya menyalahkan shog untuk ini.
7

Seperti yang dikatakan orang lain, ini sering disebut antarmuka yang lancar .

Biasanya setter adalah panggilan lewat variabel sebagai respons terhadap kode logika dalam aplikasi; kelas DTO Anda adalah contohnya. Kode konvensional ketika setter tidak mengembalikan apa pun adalah yang terbaik untuk ini. Jawaban lain telah menjelaskan caranya.

Namun ada beberapa kasus di mana antarmuka yang lancar mungkin merupakan solusi yang baik, ini memiliki kesamaan.

  • Konstanta sebagian besar diteruskan ke setter
  • Logika program tidak mengubah apa yang diteruskan ke setter.

Menyiapkan konfigurasi, misalnya fasih-nhibernate

Id(x => x.Id);
Map(x => x.Name)
   .Length(16)
   .Not.Nullable();
HasMany(x => x.Staff)
   .Inverse()
   .Cascade.All();
HasManyToMany(x => x.Products)
   .Cascade.All()
   .Table("StoreProduct");

Menyiapkan data uji dalam unit test, menggunakan TestDataBulderClasses khusus (Object Mothers)

members = MemberBuilder.CreateList(4)
    .TheFirst(1).With(b => b.WithFirstName("Rob"))
    .TheNext(2).With(b => b.WithFirstName("Poya"))
    .TheNext(1).With(b => b.WithFirstName("Matt"))
    .BuildList(); // Note the "build" method sets everything else to
                  // senible default values so a test only need to define 
                  // what it care about, even if for example a member 
                  // MUST have MembershipId  set

Namun menciptakan antarmuka yang lancar sangat sulit , jadi itu hanya layak jika Anda memiliki banyak pengaturan "statis". Juga antarmuka yang lancar tidak boleh dicampur dengan kelas "normal"; karenanya pola pembangun sering digunakan.

Ian
sumber
5

Saya pikir banyak alasan itu bukan konvensi untuk rantai satu setter demi satu karena untuk kasus-kasus itu lebih khas untuk melihat objek opsi atau parameter dalam konstruktor. C # memiliki sintaks initializer juga.

Dari pada:

DTO dto = new DTO().setFoo("foo").setBar("bar");

Orang mungkin menulis:

(dalam JS)

var dto = new DTO({foo: "foo", bar: "bar"});

(dalam C #)

DTO dto = new DTO{Foo = "foo", Bar = "bar"};

(di Jawa)

DTO dto = new DTO("foo", "bar");

setFoodan setBarkemudian tidak lagi diperlukan untuk inisialisasi, dan dapat digunakan untuk mutasi nanti.

Sementara chainability berguna dalam beberapa keadaan, penting untuk tidak mencoba memasukkan semuanya dalam satu baris hanya demi mengurangi karakter baris baru.

Sebagai contoh

dto.setFoo("foo").setBar("fizz").setFizz("bar").setBuzz("buzz");

membuatnya lebih sulit untuk membaca dan memahami apa yang terjadi. Memformat ulang ke:

dto.setFoo("foo")
    .setBar("fizz")
    .setFizz("bar")
    .setBuzz("buzz");

Jauh lebih mudah dipahami, dan membuat "kesalahan" di versi pertama lebih jelas. Setelah Anda refactored kode ke format itu, tidak ada keuntungan nyata dari:

dto.setFoo("foo");
dto.setBar("bar");
dto.setFizz("fizz");
dto.setBuzz("buzz");
zzzzBov
sumber
3
1. Saya benar-benar tidak setuju dengan masalah keterbacaan, menambahkan contoh sebelum setiap panggilan tidak membuatnya lebih jelas sama sekali. 2. Pola inisialisasi yang Anda tunjukkan dengan js dan C # tidak ada hubungannya dengan konstruktor: apa yang Anda lakukan dalam js dilewatkan satu argumen, dan apa yang Anda lakukan dalam C # adalah shugar sintaksis yang memanggil geters dan setters di belakang layar, dan java tidak memiliki geter-setter shugar seperti C #.
Ben
1
@Benedictus, saya menunjukkan berbagai cara berbeda yang bahasa OOP menangani masalah ini tanpa rantai. Intinya bukan untuk memberikan kode yang identik, intinya adalah untuk menunjukkan alternatif yang membuat rantai tidak perlu.
zzzzBov
2
"menambahkan instance sebelum setiap panggilan sama sekali tidak membuatnya lebih jelas" Saya tidak pernah mengklaim bahwa menambahkan instance sebelum setiap panggilan membuat sesuatu menjadi lebih jelas, saya hanya mengatakan bahwa itu relatif setara.
zzzzBov
1
VB.NET juga memiliki kata kunci "Dengan" yang dapat digunakan yang membuat referensi kompiler-temporer sehingga mis. [Gunakan /untuk mewakili jeda baris] With Foo(1234) / .x = 23 / .y = 47akan setara dengan Dim temp=Foo(1234) / temp.x = 23 / temp.y = 47. Sintaks semacam itu tidak menciptakan ambiguitas karena .xdengan sendirinya tidak dapat memiliki makna selain untuk mengikat ke pernyataan "With" yang langsung mengelilingi [jika tidak ada, atau objek tidak memiliki anggota x, maka .xtidak ada artinya]. Oracle belum memasukkan hal seperti itu di Jawa, tetapi konstruksi seperti itu akan cocok dengan lancar dalam bahasa.
supercat
5

Teknik itu sebenarnya digunakan dalam pola Builder.

x = ObjectBuilder()
        .foo(5)
        .bar(6);

Namun, secara umum hal itu dihindari karena bersifat rancu. Tidak jelas apakah nilai kembali adalah objek (sehingga Anda dapat memanggil setter lain), atau jika objek kembali adalah nilai yang baru saja ditetapkan (juga merupakan pola umum). Dengan demikian, Prinsip Terkecil Least menyarankan Anda tidak boleh mencoba menganggap pengguna ingin melihat satu solusi atau yang lain, kecuali itu mendasar untuk desain objek.

Cort Ammon
sumber
6
Perbedaannya adalah bahwa, ketika menggunakan pola builder, Anda biasanya menulis objek menulis saja (Builder) yang akhirnya membangun objek read-only (immutable) (kelas apa pun yang Anda bangun). Dalam hal itu, memiliki rantai panjang pemanggilan metode diinginkan karena dapat digunakan sebagai ekspresi tunggal.
Darkhogg
"atau jika objek kembali adalah nilai yang baru saja ditetapkan" Saya benci itu, jika saya ingin menyimpan nilai yang baru saja saya lewati, saya akan memasukkannya ke dalam variabel terlebih dahulu. Akan berguna untuk mendapatkan nilai yang sebelumnya ditugaskan, (beberapa antarmuka kontainer melakukan ini, saya percaya).
JAB
@ JAB Saya setuju, saya bukan penggemar notasi itu, tetapi ada tempatnya. Salah satu yang terlintas dalam pikiran adalah Obj* x = doSomethingToObjAndReturnIt(new Obj(1, 2, 3)); saya pikir itu juga mendapatkan popularitas karena itu mencerminkan a = b = c = d, meskipun saya tidak yakin bahwa popularitas beralasan. Saya telah melihat yang Anda sebutkan, mengembalikan nilai sebelumnya, di beberapa perpustakaan operasi atom. Pesan moral dalam cerita? Ini bahkan lebih membingungkan daripada yang saya buat terdengar =)
Cort Ammon
Saya percaya bahwa akademisi menjadi sedikit sombong: Tidak ada yang salah dengan idiom itu. Ini sangat berguna dalam menambah kejelasan dalam kasus-kasus tertentu. Ambil kelas pemformatan teks berikut sebagai contoh yang indentasi setiap baris string dan menggambar kotak ascii-art di sekitarnya new BetterText(string).indent(4).box().print();. Dalam hal ini, saya telah melewati sekelompok gobbledygook dan mengambil string, menjoroknya, mengepaknya, dan mengeluarkannya. Anda bahkan mungkin ingin metode duplikasi dalam kaskade (seperti, katakanlah .copy()) untuk memungkinkan semua tindakan berikut tidak lagi mengubah yang asli.
tgm1024
2

Ini lebih merupakan komentar daripada jawaban, tapi saya tidak bisa berkomentar, jadi ...

Saya hanya ingin mengatakan bahwa pertanyaan ini mengejutkan saya karena saya tidak melihat ini sama sekali tidak umum . Sebenarnya, di lingkungan kerja saya (pengembang web) sangat umum.

Sebagai contoh, ini adalah bagaimana perintah Symfony: menghasilkan: perintah entitas menghasilkan otomatis semua setters , secara default .

jQuery agak rantai sebagian besar metodenya dengan cara yang sangat mirip.

xDaizu
sumber
Js adalah kekejian
Ben
@Benedictus Saya akan mengatakan PHP adalah kekejian yang lebih besar. JavaScript adalah bahasa yang sangat bagus dan telah menjadi sangat baik dengan dimasukkannya fitur ES6 (meskipun saya yakin beberapa orang masih lebih suka CoffeeScript atau varian; secara pribadi, saya bukan penggemar bagaimana CoffeeScript menangani pelingkupan variabel saat memeriksa lingkup luar pertama daripada memperlakukan variabel yang ditugaskan dalam lingkup lokal sebagai lokal kecuali dinyatakan secara eksplisit sebagai nonlocal / global seperti yang dilakukan Python).
JAB
@Benedictus saya setuju dengan JAB. Pada tahun-tahun kuliah saya, saya digunakan untuk melihat ke bawah di JS sebagai kekejian wanna-be bahasa scripting, tetapi setelah bekerja beberapa tahun dengan itu, dan belajar KANAN cara menggunakannya, aku datang untuk ... benar-benar mencintai itu . Dan saya pikir dengan ES6 itu akan menjadi bahasa yang benar-benar matang . Tapi, apa yang membuat saya menoleh adalah ... apa jawaban saya terkait dengan JS sejak awal? ^^ U
xDaizu
Saya benar-benar bekerja beberapa tahun dengan js dan dapat mengatakan bahwa saya telah belajar caranya. Meskipun demikian saya melihat bahasa ini sebagai kesalahan. Tapi saya setuju, Php jauh lebih buruk.
Ben
@Benedictus Saya mengerti posisi Anda dan saya menghormatinya. Saya masih menyukainya. Tentu, ia memiliki keanehan dan keistimewaannya (bahasa mana yang tidak?) Tetapi terus melangkah ke arah yang benar. Tapi ... Saya sebenarnya tidak begitu menyukainya sehingga saya harus mempertahankannya. Saya tidak mendapatkan apa-apa dari orang yang suka atau membencinya ... hahaha Juga, ini bukan tempat untuk itu, jawaban saya bahkan bukan tentang JS sama sekali.
xDaizu