Uzun parametre listelerine sahip yapıcılar kullanmadan büyük, değişmez nesneler oluşturmak


96

Değişmez olabilen ve olması gereken bazı büyük (3'ten fazla alan) nesnem var. Bu vakayla her karşılaştığımda, uzun parametre listeleri ile kurucu iğrençlikleri yaratma eğilimindeyim.

Doğru hissettirmiyor, kullanımı zor ve okunabilirlik zarar görüyor.

Alanların listeler gibi bir tür koleksiyon türü olması daha da kötüdür. Basit addSibling(S s), nesne yaratmayı çok kolaylaştırır, ancak nesneyi değişebilir hale getirir.

Böyle durumlarda ne kullanıyorsunuz?

Scala ve Java kullanıyorum, ancak bence dil nesne yönelimli olduğu sürece sorunun dilden bağımsız olduğunu düşünüyorum.

Aklıma gelen çözümler:

  1. "Uzun parametre listeleri olan kurucu iğrençlikleri"
  2. Oluşturucu Modeli

5
İnşaatçı modelinin buna en iyi ve en standart çözüm olduğunu düşünüyorum.
Zachary Wright

@Zachary : Savunduğunuz şey , Joshua Bloch tarafından burada açıklandığı gibi, yalnızca oluşturucu modelinin özel bir biçimi için işe yarar : drdobbs.com/java/208403883?pgno=2 Aramak için ilgili "akıcı arayüz" terimini tercih ederim bu "kurucu kalıbının özel formu" (cevabıma bakın).
SyntaxT3rr0r

Yanıtlar:


76

Peki, hem okunması daha kolay hem de oluşturulduktan sonra değişmez bir nesne mi istiyorsunuz?

DOĞRU YAPILMIŞ akıcı bir arayüzün size yardımcı olacağını düşünüyorum .

Şöyle görünecektir (tamamen uydurma örnek):

final Foo immutable = FooFactory.create()
    .whereRangeConstraintsAre(100,300)
    .withColor(Color.BLUE)
    .withArea(234)
    .withInterspacing(12)
    .build();

"DOĞRU YAPILDI" yazısını kalın harflerle yazdım çünkü çoğu Java programcısı akıcı arayüzleri yanlış anlıyor ve nesneyi oluşturmak için gerekli yöntemle nesnelerini kirletiyor, ki bu elbette tamamen yanlış.

İşin püf noktası, yalnızca build () yönteminin aslında bir Foo oluşturmasıdır (bu nedenle Foo, değişmez olabilirsiniz).

FooFactory.create () , buradaXXX (..) ve withXXX (..) "başka bir şey" yaratır.

Başka bir şeyin bir FooFactory olabileceği, işte bunu yapmanın bir yolu ...

Siz FooFactory şöyle görünürdü:

// Notice the private FooFactory constructor
private FooFactory() {
}

public static FooFactory create() {
    return new FooFactory();
}

public FooFactory withColor( final Color col ) {
    this.color = color;
    return this;
}

public Foo build() {
    return new FooImpl( color, and, all, the, other, parameters, go, here );
}

11
@ all: lütfen "FooImpl" deki "Impl" sonekinden şikayet etmeyin: bu sınıf bir fabrikanın içinde gizlidir ve akıcı arayüzü yazan kişi dışında kimse onu görmez. Kullanıcının tek umursadığı bir "Foo" almasıdır. "FooImpl" "FooPointlessNitpick" i de
çağırabilirdim

5
Önleyici mi hissediyorsunuz? ;) Geçmişte bu konuda eleştirildiniz. :)
Greg D

3
Onun bahsediyordur yaygın hata insanlar "withXXX" (vs) yöntemleri eklemek oluşuna bağlıyor içinFoo oldukça ayrı olmasındansa nesne FooFactory.
Dean Harding

3
Hala FooImpl8 parametreye sahip yapıcıya sahipsiniz . Gelişme nedir?
Mot

4
Bu kod immutabledemezdim ve insanların fabrika nesnelerini yeniden kullanmalarından korkarım çünkü öyle olduğunu düşünüyorlar. Demek istediğim: FooFactory people = FooFactory.create().withType("person"); Foo women = people.withGender("female").build(); Foo talls = people.tallerThan("180m").build();nerede tallsşimdi sadece kadınları içerecek? Bu, değişmez bir API ile olmamalıdır.
Thomas Ahle

60

Scala 2.8'de, adlandırılmış ve varsayılan parametrelerin yanı sıra copybir vaka sınıfındaki yöntemi de kullanabilirsiniz . İşte bazı örnek kod:

case class Person(name: String, age: Int, children: List[Person] = List()) {
  def addChild(p: Person) = copy(children = p :: this.children)
}

val parent = Person(name = "Bob", age = 55)
  .addChild(Person("Lisa", 23))
  .addChild(Person("Peter", 16))

31
Scala dilini icat etmek için +1. Evet, itibar sisteminin kötüye kullanılması ama ... aww ... Scala'yı o kadar çok seviyorum ki yapmak zorunda kaldım. :)
Malax

1
Oh, adamım ... Az önce neredeyse aynı bir şeyi yanıtladım! Ben iyi bir arkadaşım. :-) Cevabınızı daha önce görmediğimi merak ediyorum ... <shrug>
Daniel C. Sobral

20

Peki, bunu Scala 2.8'de düşünün:

case class Person(name: String, 
                  married: Boolean = false, 
                  espouse: Option[String] = None, 
                  children: Set[String] = Set.empty) {
  def marriedTo(whom: String) = this.copy(married = true, espouse = Some(whom))
  def addChild(whom: String) = this.copy(children = children + whom)
}

scala> Person("Joseph").marriedTo("Mary").addChild("Jesus")
res1: Person = Person(Joseph,true,Some(Mary),Set(Jesus))

Elbette bunun sorunları da var. Örneğin, yapmayı deneyin espouseve Option[Person]ve sonra birbiriyle evli iki kişi alıyor. Bir private varve / veya bir privatekurucu artı bir fabrikaya başvurmadan bunu çözmenin bir yolunu düşünemiyorum .


11

İşte birkaç seçenek daha:

seçenek 1

Uygulamanın kendisini değiştirilebilir hale getirin, ancak değişken ve değişmez hale getirdiği arayüzleri ayırın. Bu, Swing kütüphane tasarımından alınmıştır.

public interface Foo {
  X getX();
  Y getY();
}

public interface MutableFoo extends Foo {
  void setX(X x);
  void setY(Y y);
}

public class FooImpl implements MutableFoo {...}

public SomeClassThatUsesFoo {
  public Foo makeFoo(...) {
    MutableFoo ret = new MutableFoo...
    ret.setX(...);
    ret.setY(...);
    return ret; // As Foo, not MutableFoo
  }
}

seçenek 2

Uygulamanız büyük ancak önceden tanımlanmış bir dizi değişmez nesne (örneğin, yapılandırma nesneleri) içeriyorsa, Spring çerçevesini kullanmayı düşünebilirsiniz .


3
1. Seçenek zekice (ama çok da zekice değil ), bu yüzden hoşuma gitti.
Timothy

4
Bunu daha önce yaptım, ama bence bu iyi bir çözüm olmaktan çok uzak çünkü nesne hala değişebilir, sadece mutasyon yöntemleri "gizli". Belki bu konuda çok seçici
davranıyorum

Seçenek 1'deki bir varyant, Değişmez ve Değişebilir varyantlar için ayrı sınıflara sahip olmaktır. Bu, arayüz yaklaşımından daha iyi güvenlik sağlar. Muhtemelen API tüketicisine, değiştirilebilir arayüzüne dönüştürülmesi gereken Değişmez adlı değiştirilebilir bir nesne verdiğinizde yalan söylüyorsunuz. Ayrı sınıf yaklaşımı, yöntemlerin ileri geri dönüştürülmesini gerektirir. JodaTime API bu kalıbı kullanır. DateTime ve MutableDateTime bakın.
toolbear

6

Farklı değişmezlik türleri olduğunu hatırlamaya yardımcı olur . Sizin durumunuz için, "buzlu şeker" değişmezliğinin gerçekten işe yarayacağını düşünüyorum:

Popsicle değişmezliği: tuhaf bir şekilde bir kez yazılabilen değişmezliğin hafif bir zayıflaması dediğim şeydir. Başlatma sırasında bir süre değişebilen ve sonra sonsuza kadar "donmuş" bir nesne veya alan hayal edilebilir. Bu tür bir değişmezlik, birbirine dairesel olarak gönderme yapan değişmez nesneler için veya diske serileştirilmiş ve serileştirmeden sonra serileştirmeden sonra "akışkan" olması gereken değişmez nesneler için özellikle yararlıdır, bu noktada tüm nesnelerin dondurulmuş.

Böylece nesnenizi başlatırsınız, ardından artık yazılabilir olmadığını belirten bir tür "donma" bayrağı ayarlarsınız. Tercihen, mutasyonu bir işlevin arkasına gizlersiniz, böylece işlev API'nizi kullanan istemciler için hala saf olur.


1
Olumsuz oy mu? Bunun neden iyi bir çözüm olmadığı konusunda yorum yapmak isteyen var mı?
Juliet

+1. Belki birisi clone()yeni örnekler türetmek için kullanımını ima ettiğiniz için buna olumsuz oy verdi.
finnw

: O Java iplik güvenliğini tehlikeye çünkü Ya da belki de oldu java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5
finnw

Bir dezavantajı, gelecekteki geliştiricilerin "donma" bayrağını kullanmada dikkatli olmalarını gerektirmesidir. Yöntemin çözülmediğini iddia etmeyi unutan bir mutatör yöntem daha sonra eklenirse, sorunlar olabilir. Benzer şekilde, freeze()yöntemi çağırması gereken, ancak çağırmayan yeni bir kurucu yazılırsa , işler çirkinleşebilir.
stalepretzel

5

Ayrıca, değişmez nesnelerin, mutatörlere benzeyen (addSibling gibi) yöntemleri ortaya çıkarmasını sağlayabilirsiniz, ancak yeni bir örnek döndürmelerine izin verebilirsiniz. Değişmez Scala koleksiyonlarının yaptığı budur.

Olumsuz yanı, gerekenden daha fazla örnek oluşturabilmenizdir. Ayrıca, kısmen oluşturulmuş nesnelerle uğraşmak istemediğiniz sürece, yalnızca ara geçerli yapılandırmalar olduğunda (çoğu durumda uygun olan bazı düğümler gibi) kullanılabilir.

Örneğin, henüz hedefi olmayan bir grafik kenarı geçerli bir grafik kenarı değildir.


İddia edilen dezavantajı - gerekenden daha fazla örnek oluşturmak - aslında o kadar da sorun değil. Kısa ömürlü nesnelerin çöp toplama gibi nesne tahsisi de çok ucuzdur. Kaçış analizi varsayılan olarak etkinleştirildiğinde, bu tür "ara nesneler" büyük olasılıkla yığın tahsis edilir ve tam anlamıyla hiçbir maliyet oluşturmaz.
gustafc

2
@gustafc: Evet. Cliff Click bir keresinde Rich Hickey'nin Clojure Ant Colony simülasyonunu büyük kutularından birinde (864 çekirdek, 768 GB RAM) nasıl kıyasladıklarını anlattı: 700 çekirdek üzerinde çalışan 700 paralel iş parçacığı, her biri% 100 saniyede geçici çöp . Genel Sekreter ter bile atmadı.
Jörg W Mittag

5

Dört olasılığı düşünün:

new Immutable(one, fish, two, fish, red, fish, blue, fish); /*1 */

params = new ImmutableParameters(); /*2 */
params.setType("fowl");
new Immutable(params);

factory = new ImmutableFactory(); /*3 */
factory.setType("fish");
factory.getInstance();

Immutable boringImmutable = new Immutable(); /* 4 */
Immutable lessBoring = boringImmutable.setType("vegetable");

Bana göre 2, 3 ve 4'ün her biri farklı bir duruma uyarlanmıştır. İlki, OP tarafından belirtilen nedenlerden ötürü sevilmesi zordur ve genellikle biraz sürünen ve biraz yeniden düzenleme gerektiren bir tasarımın belirtisidir.

(2) olarak listelediğim şey, 'fabrikanın' arkasında devlet olmadığında iyidir, oysa (3), devlet olduğunda tercih edilen tasarımdır. İş parçacıkları ve senkronizasyon konusunda endişelenmek istemediğimde kendimi (3) yerine (2) kullanırken buluyorum ve birçok nesnenin üretimi için bazı pahalı kurulumları amorti etme konusunda endişelenmeme gerek yok. (3) ise, fabrikanın inşasına gerçek iş girdiğinde (bir SPI'dan kurulum, konfigürasyon dosyalarını okuma, vb.) Çağrılır.

Son olarak, başka birinin cevabı, pek çok küçük değişmez nesneye sahip olduğunuz ve tercih edilen modelin eskilerden haber almak olduğu seçenekten (4) bahsetti.

'Model fan klübü'nün bir üyesi olmadığıma dikkat edin - elbette, bazı şeyler taklit etmeye değer, ancak insanlar onlara isimler ve komik şapkalar verdikten sonra kendilerine yararsız bir hayat sürdüklerini düşünüyorum.


6
Bu İnşaatçı Kalıbı (seçenek 2)
Simon Nickerson

Bu, değişmez nesnelerini yayan bir fabrika ('kurucu') nesnesi olmaz mıydı?
bmargulies

bu ayrım oldukça anlamsal görünüyor. Fasulye nasıl yapılır? Bunun bir inşaatçıdan farkı nedir?
Carl

Bir oluşturucu için (veya daha fazlası için) Java Bean kuralları paketini istemezsiniz.
Tom Hawtin - tackline

4

Diğer bir olası seçenek, daha az yapılandırılabilir alana sahip olmak için yeniden düzenleme yapmaktır. Alan grupları yalnızca birbirleriyle (çoğunlukla) çalışırsa, onları kendi küçük değişmez nesnelerinde toplayın. Bu "küçük" nesnenin kurucuları / oluşturucuları, bu "büyük" nesne için kurucu / oluşturucu gibi daha yönetilebilir olmalıdır.


1
not: bu cevap için kilometre, soruna, kod tabanına ve geliştirici becerisine göre değişebilir.
Carl

2

C # kullanıyorum ve bunlar benim yaklaşımlarım. Düşünmek:

class Foo
{
    // private fields only to be written inside a constructor
    private readonly int i;
    private readonly string s;
    private readonly Bar b;

    // public getter properties
    public int I { get { return i; } }
    // etc.
}

Seçenek 1. İsteğe bağlı parametrelere sahip kurucu

public Foo(int i = 0, string s = "bla", Bar b = null)
{
    this.i = i;
    this.s = s;
    this.b = b;
}

Örneğin kullanılır new Foo(5, b: new Bar(whatever)). 4.0'dan önceki Java veya C # sürümleri için değil. ama yine de göstermeye değer, çünkü tüm çözümlerin dilden bağımsız olmadığına bir örnek.

Seçenek 2. Yapıcı tek bir parametre nesnesi alıyor

public Foo(FooParameters parameters)
{
    this.i = parameters.I;
    // etc.
}

class FooParameters
{
    // public properties with automatically generated private backing fields
    public int I { get; set; }
    public string S { get; set; }
    public Bar B { get; set; }

    // All properties are public, so we don't need a full constructor.
    // For convenience, you could include some commonly used initialization
    // patterns as additional constructors.
    public FooParameters() { }
}

Kullanım örneği:

FooParameters fp = new FooParameters();
fp.I = 5;
fp.S = "bla";
fp.B = new Bar();
Foo f = new Foo(fp);`

3.0'dan itibaren C #, bunu nesne başlatıcı sözdizimi ile daha zarif hale getirir (anlamsal olarak önceki örneğe eşdeğerdir):

FooParameters fp = new FooParameters { I = 5, S = "bla", B = new Bar() };
Foo f = new Foo(fp);

Seçenek 3:
Sınıfınızı bu kadar çok sayıda parametreye ihtiyaç duymayacak şekilde yeniden tasarlayın. Sorumluluklarını birden çok sınıfa ayırabilirsiniz. Veya parametreleri kurucuya değil, yalnızca talep üzerine belirli yöntemlere iletin. Her zaman uygulanabilir değil, ama olduğu zaman yapmaya değer.

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.