Zincirleme ayarlayıcıları neden sıra dışı?


46

Fasulyelerde zincirleme yapılması çok kullanışlıdır: aşırı yüklenicilere, mega yapıcılara, fabrikalara gerek yoktur ve okunabilirliği arttırır. Nesnenizin değişmez olmasını istemediğiniz sürece herhangi bir olumsuz tarafı düşünemiyorum, bu durumda zaten herhangi bir ayarlayıcı bulunmayacak. Öyleyse bunun bir OOP sözleşmesi olmaması için bir neden var mı?

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

32
Çünkü Java, ayarlayıcıların insanlara kötü davranmadığı tek dil olabilir ...
Telastyn

11
Bu kötü bir stil çünkü dönüş değeri anlamsal olarak anlamlı değil. Yanıltıcıdır. Sadece tersi çok az tuşa basmaktır.
usr

58
Bu model nadir değildir. Bunun için bir isim bile var. Buna akıcı arayüz denir .
Philipp,

9
Daha fazla okunabilir bir sonuç için bu oluşturucuyu bir oluşturucuda da soyutlayabilirsiniz. myCustomDTO = DTOBuilder.defaultDTO().withFoo("foo").withBar("bar").Build();Bunu yapardım, böylece belirleyicilerin geçersiz olduğu genel fikrine çelişmemek için.

9
@Philipp, teknik olarak haklıyken, bunun new Foo().setBar('bar').setBaz('baz')çok "akıcı" olduğunu söylemez . Yani, tam olarak aynı şekilde uygulanabileceğinden eminim, ama daha çok benzer bir şeyler okumayı beklerdimFoo().barsThe('bar').withThe('baz').andQuuxes('the quux')
Wayne Werner

Yanıtlar:


50

Öyleyse bu neden bir OOP sözleşmesi olmasın?

En iyi tahminim: CQS'yi ihlal ettiği için

Aynı yönteme karıştırılmış bir komut (nesnenin durumunu değiştirme) ve bir sorgu (durum kopyasını döndürme - bu durumda nesnenin kendisi) var. Bu mutlaka bir sorun değil, ancak temel kuralların bazılarını ihlal ediyor.

Örneğin, C ++ 'da std :: stack :: pop (), void döndüren bir komuttur ve std :: stack :: top (), yığındaki en üst öğeye başvuru döndüren bir sorgudur. Klasik olarak, ikisini birleştirmek istersiniz, ancak bunu yapamazsınız ve istisna güvenli olamazsınız . (Java'daki bir atama değil, çünkü Java'daki atama operatörü atmıyor).

DTO bir değer türü olsaydı, bununla benzer bir sonuç elde edebilirsiniz.

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

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

Ayrıca, zincirleme geri dönüş değerleri, kalıtımla uğraşırken devasa bir acıdır. "Merakla yinelenen şablon kalıbı" konusuna bakın.

Son olarak, varsayılan kurucunun sizi geçerli bir durumda olan bir nesneyle bırakması gerektiği sorunu var. Eğer varsa gereken bir geçerli duruma nesneyi geri komutları bir demet çalıştırın şey çok yanlış gitti.


4
Sonunda, burada gerçek bir kaptan. Kalıtım asıl problemdir. Zincirlemenin, kompozisyonda kullanılan veri gösterimi nesneleri için geleneksel bir belirleyici olabileceğini düşünüyorum.
Ben

6
"devlet bir kopyasını bir nesneden döndürme" - bunu yapmaz
user253751

2
Bu yönergeleri izlemenin en büyük nedeninin (yineleyiciler gibi bazı istisnalar dışında), kodun nedenini çok daha kolay hale getirdiğini iddia ediyorum .
jpmc26

1
@MatthieuM. Hayır. Temelde Java dili konuşuyorum ve buradaki gelişmiş "akışkan" API'lerde yaygındır - Diğer dillerde de var olduğundan eminim. Temel olarak, türünüzü kendi kendine uzanan bir tür üzerinde genel hale getirirsiniz . Daha sonra abstract T getSelf()genel türü döndüren bir yöntem eklersiniz . Şimdi, döndürmek yerine this, sizin belirleyiciler sizi return getSelf()ve ardından herhangi bir basan sınıf sadece kendisi ve getiri jenerik türünü yapar thisden getSelf. Bu şekilde ayarlayıcılar bildiren tipten değil gerçek tipten geri döner.
Örümcek Boris,

1
@MatthieuM. Gerçekten mi? C ++ için CRTP'ye benziyor ...
Yararsız

33
  1. Birkaç tuş vuruşunu kaydetmek zorlayıcı değildir. Güzel olabilir, ancak OOP sözleşmeleri tuşlara basmadan değil, kavramlara ve yapılara daha çok önem veriyor.

  2. Dönüş değeri anlamsız.

  3. Anlamsız olmaktan bile öte, geri dönüş değeri yanıltıcıdır, çünkü kullanıcılar geri dönüş değerinin anlam kazanmasını bekleyebilirler. Bunun “değişmez bir düzenleyici” olmasını bekleyebilirler

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

    Gerçekte, pasörünüz nesneyi mutasyona uğratır.

  4. Miras ile iyi çalışmaz.

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

    yazabilirim

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

    Ama değil

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

Benim için, # 1'den 3'e kadar olanlar önemli olanlar. İyi yazılmış bir kod, hoş bir şekilde düzenlenmiş metinlerden ibaret değildir. İyi yazılmış kod, temel kavramlar, ilişkiler ve yapı ile ilgilidir. Metin, kodun gerçek anlamının yalnızca dışa yansımasıdır.


2
Yine de bu argümanların çoğu akıcı / zincirlenebilir bir arayüz için de geçerlidir.
Casey,

@Ceyey, evet. İnşaatçılar (yani ayarlayıcılar ile) zincirleme gördüğüm en büyük durumdur.
Paul Draper

10
Akıcı bir arayüz fikrine aşina iseniz, # 3 geçerli değildir, çünkü geri dönüş türünden akıcı bir arayüz olduğundan şüpheleniyorsunuzdur. Ayrıca "İyi yazılmış kod hoş bir şekilde düzenlenmiş metinle ilgili değil" ifadesine katılmıyorum. Bunların hepsi bu kadar değil, ama güzel düzenlenmiş metin, programın insan okuyucusunun daha az çabayla anlamasını sağladığı için oldukça önemlidir.
Michael Shaw,

1
Setter zincirleme, metni hoş bir şekilde düzenlemek veya tuş vuruşlarını kaydetmekle ilgili değildir. Tanımlayıcı sayısını azaltmakla ilgili. Oluşturmak ve bir değişkene kaydetmek için olmayabilir demektir Tek bir ifadede nesneyi ayarlayabilirsiniz zincirleme ayarlayıcı ile - bir değişken isme gerekecek ve bu kapsamda sonuna kadar orada kalacak.
Idan Arye

3
Nokta # 4 hakkında, Java’da şu şekilde çözülebilecek bir şey var: public class Foo<T extends Foo> {...}setter geri dönüyor Foo<T>. Ayrıca bu 'ayarlayıcı' yöntemleri, onları 'beraber' yöntemleri olarak adlandırmayı tercih ediyorum. Aşırı yükleme iyi çalışıyorsa, o zaman sadece Foo<T> with(Bar b) {...}, aksi takdirde Foo<T> withBar(Bar b).
YoYo,

12

Bunun bir OOP sözleşmesi olduğunu sanmıyorum, daha çok dil tasarımı ve sözleşmeleriyle ilgili.

Java'yı kullanmak istiyor gibisin. Java, boş bırakılacak ayarlayıcının dönüş türünü belirten bir JavaBeans özelliğine sahiptir , yani ayarlayıcıların zincirlenmesiyle çakışmaktadır. Bu özellik, çeşitli araçlarda geniş çapta kabul görmekte ve uygulanmaktadır.

Elbette sorabilirsiniz, neden şartnamenin bir kısmını zincirlemiyorsunuz. Cevabı bilmiyorum, belki de bu desen o zamanlar bilinmiyor / popüler değildi.


Evet, JavaBeans aklımdaki tam olarak buydu.
Ben

JavaBeans kullanan çoğu uygulamanın özen gösterdiğini sanmıyorum, ancak olabilirler ('set ...' adlı yöntemleri kapmak için yansıma kullanmak, tek bir parametreye sahiptirler ve geri dönüşleri boşa çıkarırlar ). Birçok statik analiz programı, bir şeyi döndüren bir yöntemden bir getiri değerini kontrol etmemekten şikayet eder ve bu çok can sıkıcı olabilir.

@MichaelT yansıtıcı çağrıları, Java yöntemlerini almak için dönüş türünü belirtmiyor. Bu şekilde geri dönen bir yöntem çağırmak Object, bu Voidyöntem voidgeri dönüş türü olarak belirtilirse , tekton türünün döndürülmesine neden olabilir . Diğer yandan, görünüşe göre "yorum bırakıp oy kullanma", iyi bir yorum olsa bile, "ilk yazı" denetiminin yapılmamasına sebep oluyor. Bunun için övünmekle suçluyorum.

7

Diğer insanların söylediği gibi, buna genellikle akıcı bir arayüz denir .

Normalde ayarlayıcılar bir uygulamadaki mantık koduna cevaben değişkenlerde çağrı yapmaktır ; DTO sınıfınız buna bir örnektir. Ayarlayıcılar hiçbir şey döndürmediğinde geleneksel kod bunun için en iyisidir. Diğer cevaplar yolu açıkladı.

Bununla birlikte, akıcı ara yüzün iyi bir çözüm olabileceği birkaç durum vardır , bunlar ortaktır.

  • Sabitler belirleyicilere geçiyor
  • Program mantığı ayarlayıcılara iletileni değiştirmez.

Konfigürasyon ayarlama, örneğin akıcı-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");

Özel TestDataBulderClasses (Object Anneleri) kullanarak birim testlerinde test verileri ayarlama

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

Ancak, iyi akıcı bir arayüz oluşturmak çok zordur , bu yüzden sadece “statik” bir kurulum yaptığınızda buna değer. Ayrıca, akıcı arayüz “normal” sınıflarla karıştırılmamalıdır; bu yüzden yapıcı desen sıklıkla kullanılır.


5

Bence bir ayarlayıcıyı birbiri ardına zincirlemenin bir nedeni olmama nedeni, bu durumlarda bir kurucuda bir seçenek nesnesi ya da parametresi görmenin daha tipik olmasıdır. C # da bir başlatıcı sözdizimine sahiptir.

Onun yerine:

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

Bir yazabilir:

(JS’de)

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

(C # ile)

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

(Java’da)

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

setFoove setBardaha sonra başlatma için artık gerekli değildir ve daha sonra mutasyon için kullanılabilir.

Zincirleme özelliği bazı durumlarda faydalı olsa da, sadece yeni satır karakterlerini azaltmak uğruna her şeyi tek bir satırda yapmaya çalışmamak önemlidir.

Örneğin

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

ne olduğunu okumak ve anlamak zorlaştırır. Yeniden biçimlendirme:

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

Anlaması çok daha kolaydır ve ilk versiyondaki "hatayı" daha belirgin hale getirir. Kodu bu formata yeniden ekledikten sonra, bunun üzerinde gerçek bir avantaj yoktur:

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

3
1. Okunabilirlik konusuna gerçekten katılıyorum, her aramadan önce bir örnek eklemek, onu daha net hale getirmiyor. 2. js ve C # ile gösterdiğiniz başlatma desenlerinin yapıcılarla hiçbir ilgisi yoktur: js'de yaptığınız tek bir argümandan geçer ve C #'da yaptığınız şey alıcıları ve ayarlayıcıları sahne arkası ve java olarak adlandırır. C # gibi bir alıcı-setter shugar yok.
Ben

1
@Benedictus, OOP dillerinin bu sorunu zincirlemeden ele almasının çeşitli yollarını gösteriyordum. Mesele aynı kod sağlamak değildi, mesele zincirlemeyi gereksiz kılan alternatifler göstermekti.
zzzzBov

2
"her çağrıdan önce örnek eklemek hiç de netleşmiyor" Her çağrıdan önce örnek eklemenin hiçbir şeyi netleştirmemiş olduğunu iddia etmedim, sadece göreceli olarak eşdeğer olduğunu söyledim.
zzzzBov

1
VB.NET ayrıca, derleyici-geçici bir referans oluşturan, örneğin [ /satır sonunu temsil etmek için kullanmanın ] With Foo(1234) / .x = 23 / .y = 47eşdeğer olması için kullanılabilir bir "With" anahtar kelimesine sahiptir Dim temp=Foo(1234) / temp.x = 23 / temp.y = 47. Böyle bir sözdizimi belirsizliği yaratmaz, çünkü .xkendi başına hemen çevreleyen "With" ifadesine bağlanmaktan başka bir anlamı olamaz çünkü [eğer yok ise veya nesnenin bir üyesi yoksa x, o .xzaman anlamsızdır]. Oracle, Java'ya böyle bir şey eklememiştir, ancak böyle bir yapı dile sorunsuz bir şekilde uyar.
supercat,

5

Bu teknik aslında Oluşturucu düzeninde kullanılır.

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

Ancak, genel olarak bundan kaçınılır, çünkü belirsizdir. Dönüş değerinin nesne olup olmadığı (yani diğer ayarlayıcıları çağırabilirsiniz) veya dönüş nesnesinin yeni atanan değer olup olmadığı açıktır (ayrıca ortak bir kalıp). Buna göre, En Az Sürpriz Prensibi, nesnenin tasarımında temel olmadıkça, kullanıcının bir çözümü ya da diğerini görmek istediğini varsaymaya çalışmamanızı önerir.


6
Aradaki fark, oluşturucu desenini kullanırken, genellikle sonunda salt okunur (değişmez) bir nesneyi (oluşturduğunuz sınıf ne olursa olsun) oluşturan salt okunur bir nesne (Oluşturucu) yazıyor olmanızdır. Bu bakımdan, uzun bir yöntem çağrısı zincirine sahip olmak arzu edilir, çünkü tek bir ifade olarak kullanılabilir.
Darkhogg

"ya da return nesnesi henüz atanmış olan değerse" Bundan nefret ediyorum, sadece geçtiğim değeri saklamak istiyorsam önce bir değişkene koyacağım. Ancak daha önce atanmış olan değeri elde etmek faydalı olabilir (bazı konteyner arayüzleri bunu yapıyor sanırım).
JAB

@JAB Katılıyorum, bu gösterimin hayranı değilim, ama onun yerleri var. Akla gelen , popülerliğin iyi bir şekilde kurulduğuna ikna olmadığım Obj* x = doSomethingToObjAndReturnIt(new Obj(1, 2, 3)); için yansıttığım için popülerlik kazandığını düşünüyorum a = b = c = d. Bazı atomik işlem kütüphanelerinde, önceki değeri döndürerek bahsettiğinizi gördüm. Hikayeden çıkarılacak ders? Kulağa verdiğimden daha da kafa karıştırıcı =)
Cort Ammon

Akademisyenlerin biraz bilgiçlik kazandığına inanıyorum: Bu deyimle doğal olarak yanlış bir şey yok. Bazı durumlarda netlik katmada çok faydalıdır. Örneğin, bir dizginin her satırını girintili ve etrafına bir asal sanat kutusu çizen aşağıdaki metin biçimlendirme sınıfını alın new BetterText(string).indent(4).box().print();. Bu durumda, bir avuç goblengook atladım ve bir ip alıp girdim, kutuladım ve çıktısını aldım. .copy()Aşağıdaki tüm işlemlerin orijinali artık değiştirmemesine izin vermek için basamağın içinde (örneğin, gibi) bir çoğaltma yöntemi bile isteyebilirsiniz .
tgm1024

2

Bu bir cevaptan çok bir yorumdur, ama yorum yapamam, o yüzden ...

sadece bu sorunun beni şaşırttığından bahsetmek istedim çünkü bunu hiç nadir görmüyorum . Aslında benim çalışma ortamımda (web geliştirici) çok yaygındır.

Oluşturmak: Örneğin, bu Symfony'nin doktrini nasıl kişiler otomatik oluşturduğu komuta tüm setters , varsayılan olarak .

jQuery tür çok benzer bir şekilde zincirler onun yöntemlerin çoğu.


Js bir abomination
Ben

@Benedictus PHP'nin daha büyük bir suistimal olduğunu söyleyebilirim. JavaScript mükemmel bir dildir ve ES6 özelliklerinin eklenmesiyle oldukça hoş bir hale gelmiştir (bazı kişilerin hala CoffeeScript veya türevlerini tercih ettiğinden eminim; kişisel olarak, CoffeeScript'in dış kapsamı denetlerken değişken kapsamayı nasıl ele aldığına dair bir hayran değilim. öncelikle Python'un yaptığı gibi yerel olmayan / global olarak belirtilmediği sürece yerel kapsamda yerel olarak atanmış değişkenleri tedavi etmek yerine).
JAB

@Benedictus ile aynı fikirdeyim JAB. Üniversite yıllarımda ben bir iğrenç wanna-kodlama dili olarak JS aşağı bakmak için kullanılır, ancak onunla bir kaç yıl çalışma ve öğrenme sonra SAĞ onu kullanmanın yolunu, ben aslında hiç ... geldiniz seviyorum onu . Ve ES6 ile gerçekten olgun bir dil olacağını düşünüyorum . Ama beni başımı döndüren şey ... cevabımın ilk başta JS ile ne ilgisi var? ^^ U
xDaizu

Aslında birkaç yıl js ile çalıştım ve bunu öğrendiğimi söyleyebilirim. Yine de bu dili bir hatadan başka bir şey olarak görmüyorum. Ama katılıyorum, Php çok daha kötü.
Ben

@Benedictus Konumunuzu anlıyorum ve saygı duyuyorum. Yine de hoşuma gitti. Tabii ki, tuhaflıkları ve perksleri var (hangi dilde değil?) Fakat doğru bir şekilde adım adım ilerliyor. Ama ... Aslında onu o kadar sevmiyorum ki savunmam gerekiyor. İnsanları sevmekten veya nefret etmekten hiçbir şey kazanmıyorum ... hahaha Ayrıca, bunun için bir yer yok, cevabım JS hakkında bile değildi.
xDaizu
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.