Bir Oluşturucu deseni uygularken neden bir Oluşturucu sınıfına ihtiyacımız var?


31

Oluşturucu deseninin birçok uygulamasını gördüm (çoğunlukla Java'da). Hepsinde bir varlık sınıfı (bir Personsınıf diyelim ) ve bir üretici sınıfı var PersonBuilder. Oluşturucu, çeşitli alanları "istifler" new Personve argümanları a ile döndürür . Tüm builder yöntemlerini Personsınıfın içine koymak yerine neden açıkça bir builder sınıfına ihtiyacımız var ?

Örneğin:

class Person {

  private String name;
  private Integer age;

  public Person() {
  }

  Person withName(String name) {
    this.name = name;
    return this;
  }

  Person withAge(int age) {
    this.age = age;
    return this;
  }
}

Basitçe söyleyebilirim Person john = new Person().withName("John");

Neden bir PersonBuildersınıfa ihtiyaç duyuluyor?

Gördüğüm tek yarar, Personalanları şöyle ilan edip finaldeğişmezliği sağlamaktır.


4
Not: Bu kalıp için normal isim şudur chainable setters: D
Mooing Duck

4
Tek sorumluluk prensibi. Mantığı, Person sınıfına koyarsanız, yapacak birden fazla işi vardır
reggaeguitar

1
Avantajınız iki şekilde de olabilir: withNameKişinin bir kopyasını yalnızca ad alanı değiştirilmiş olarak iade edebilirim . Başka bir deyişle, değişmez Person john = new Person().withName("John");olsa bile çalışabilir Person(ve bu, işlevsel programlamada yaygın bir kalıptır).
Brian McCutchon

9
@ MoooDuck: Bunun için başka bir ortak terim akıcı bir arayüz .
Mac

2
@Mac, akıcı arayüzün biraz daha genel olduğunu düşünüyorum - voidmetot kullanmaktan kaçının . Örneğin Person, ismini basan bir metot varsa, hala bir Fluent Arayüzü ile zincirleyebilirsiniz person.setName("Alice").sayName().setName("Bob").sayName(). Bu arada, JavaDoc'taki önerileri tam olarak sizin önerinizle ekliyorum @return Fluent interface- return thisyürütmenin sonunda uygulanan herhangi bir yönteme uygulandığında genel ve yeterince açık ve oldukça açık. Böylece, bir Oluşturucu da akıcı bir arayüz yapıyor olacak.
VLAZ

Yanıtlar:


27

Olmak, böylece var değişmez VE adlandırılan parametreleri simüle aynı anda.

Person p = personBuilder
    .name("Arthur Dent")
    .age(42)
    .build()
;

Bu durum, durum belirlenene ve bir kez ayarlandıktan sonra, değiştirmenize izin vermeyecek şekilde, ancak her alan açıkça etiketlenene kadar el değneklerinizi kişiden uzak tutar. Bunu Java'da yalnızca bir sınıfla yapamazsınız.

Josh Blochs Builder Pattern hakkında konuşuyorsun . Bu , Dört Oluşturucu Deseni Çetesi ile karıştırılmamalıdır . Bunlar farklı hayvanlar. Her ikisi de inşaat problemlerini çözüyor, fakat oldukça farklı şekillerde.

Elbette, nesneyi başka bir sınıf kullanmadan yapılandırabilirsin. Ama sonra seçmek zorundasın. Adlandırılmış parametreleri kendilerine sahip olmayan dillerde (Java gibi) taklit etme becerisini kaybedersiniz ya da nesnelerin ömrü boyunca değişmez kalma özelliğini kaybedersiniz.

Immutable örnek, parametreler için isim yok

Person p = new Person("Arthur Dent", 42);

Burada her şeyi basit ve basit bir kurucu ile inşa ediyorsunuz. Bu değişmez kalmanıza izin verecek ancak adlandırılmış parametrelerin simülasyonunu kaybedeceksiniz. Pek çok parametreyle okumak zorlaşıyor. Bilgisayarlar umrunda değil ama insanlar için zor.

Geleneksel ayarlayıcılarla adlandırılmış parametre örneği simülasyonu. Değişmez

Person p = new Person();
p.name("Arthur Dent");
p.age(42);

Burada her şeyi ayarlayıcılarla oluşturuyorsunuz ve adlandırılmış parametreleri simüle ediyorsunuz ancak artık değişken değilsiniz. Bir ayarlayıcının her kullanımı nesne durumunu değiştirir.

Yani sınıfı ekleyerek elde ettiğiniz her ikisini de yapabilirsiniz.

build()Eksik bir yaş alanı için bir çalışma zamanı hatası sizin için yeterliyse, doğrulama yapılabilir . Bunu yükseltebilir ve age()derleyici hatasıyla çağrılan zorlayabilirsiniz . Sadece Josh Bloch builder deseni ile değil.

Bunun için dahili bir Etki Alanı Özel Diline (iDSL) ihtiyacınız var.

Bu, aradıklarından age()ve name()aramadan önce talep etmenizi sağlar build(). Ancak bunu thisher seferinde geri döndürerek yapamazsınız . Dönen her şey, bir sonraki şeyi çağırmaya zorlayan farklı bir şey döndürür.

Kullanımı şöyle görünebilir:

Person p = personBuilder
    .name("Arthur Dent")
    .age(42)
    .build()
;

Ama bu:

Person p = personBuilder
    .age(42)
    .build()
;

derleyici hatasına neden olur çünkü age()yalnızca döndürülen türün çağrılması için geçerlidir name().

Bu iDSL'ler son derece güçlüdür ( örneğin JOOQ veya Java8 Streams ) ve özellikle de kod tamamlama özelliğine sahip bir IDE kullanıyorsanız, ancak kurulumları yapmak için oldukça zor bir iştir. Onlara, kendilerine karşı yazılmış adil bir miktar kaynak kodu olacak şeyler için kaydetmenizi öneririm.


3
Ve sonra birisi neden yaşını belirtmeden önce adı vermeniz gerektiğini sorar ve tek cevap "birleşimsel patlama" dır. C ++ gibi uygun şablonlara sahipse ya da her şey için farklı bir tür kullanmaya devam ederse, kaynakta bundan kaçınabileceğinden eminim.
Deduplicator

1
Bir sızdıran soyutlama nasıl kullanılacağını bilmek edebilmek için altta yatan bir karmaşıklık bilgi gerektirir. Burada gördüğüm şey bu. Kendini nasıl inşa edeceğini bilmeyen herhangi bir sınıf, zorunlu olarak başka bağımlılıkları olan Oluşturucu ile birleştirilir (bu, Oluşturucu kavramının amacı ve niteliğidir). Bir keresinde (grup kampında değil), sadece böyle bir kümelenme kabiliyetini geri almak için 3 ay boyunca gereken bir nesneyi somutlaştırmak için basit bir ihtiyaç. Şaka yapmıyorum.
radarbob

Yapım kurallarından hiçbirini bilmeyen sınıfların avantajlarından biri, bu kurallar değiştiğinde umursamadıklarıdır. Sadece AnonymousPersonBuilderkendi kurallarina sahip bir tane ekleyebilirim .
candied_orange

Sınıf / sistem tasarımı nadiren "uyumu en üst düzeye çıkarma ve kaplini en aza indirgeme" nin derinlemesine uygulanmasını gösterir. Tasarım ve çalışma zamanı lejyonları genişletilebilir ve kusurlar yalnızca OO maksimuma ve kılavuzlarına fraktal olma duygusuyla veya "tamamen kaplumbağadır" uygulandığında düzeltilebilir.
radarbob

1
@ radarbob İnşa edilmekte olan sınıf hala kendini nasıl inşa edeceğini bilir. Yapıcı, nesnenin bütünlüğünü sağlamaktan sorumludur. Oluşturucu sınıfı yalnızca yapıcı için yardımcıdır, çünkü yapıcı çok hoş bir API için yapılmayan çok sayıda parametre alır.
Hayır U

57

Neden bir oluşturucu sınıfı kullanıyor / sağlıyorsunuz:

  • Değişmez nesneler yapmak için - zaten belirlediğiniz fayda. İnşaat birden fazla adım atıyorsa kullanışlıdır. FWIW, değişmezlik, sürdürülebilir ve hatasız programlar yazma arayışımızda önemli bir araç olarak görülmelidir.
  • Son (muhtemelen değişmez) nesnenin çalışma zamanı temsili, okuma ve / veya alan kullanımı için optimize edilmiş ancak güncelleme için optimize edilmemişse. String ve StringBuilder burada iyi örneklerdir. Dizeleri sırayla bitiştirmek çok verimli değildir; bu nedenle StringBuilder, eklemek için iyi olan ancak alan kullanımı için iyi olmayan ve normal String sınıfı kadar okumak ve kullanmak için iyi olmayan farklı bir iç gösterimi kullanır.
  • Yapılı nesneleri, yapım aşamasında olan nesnelerden açıkça ayırmak Bu yaklaşım yapım aşamasından inşaata açık bir geçiş gerektirir. Tüketici için, yapım aşamasında bir nesneyi yapılı bir nesneyle karıştırmanın bir yolu yoktur: tür sistemi bunu uygular. Bu, bazen, bu yaklaşımı olduğu gibi "başarının çukuruna düşmek" için kullanabileceğimiz anlamına gelir ve başkalarının (veya kendimizin) soyutlamasını yaparken (bir API veya katman gibi), bu çok iyi olabilir şey.

4
Son kurşun noktasına gelince. Bu nedenle, fikir sahibi PersonBuilderolmayanların olduğu düşüncesi ve mevcut değerleri incelemenin tek yolu .Build()a Person. Bu yolla. Doğru bir şekilde inşa edilmiş bir nesneyi doğrulayabilirsiniz, doğru mu? Yani bu bir "yapım aşamasında" nesnenin kullanılmasını önlemeye yönelik mekanizma mı?
AaronLS

@AaronLS, evet, kesinlikle; Karmaşık doğrulama, Buildhatalı ve / veya yapım aşamasında nesnelerin önlenmesi için bir işleme sokulabilir .
Erik Eidt

2
Nesnenizi yapıcısının içinde de doğrulayabilirsiniz, tercih kişisel olabilir veya karmaşıklığa bağlı olabilir. Ne seçiliyse seçilsin, asıl mesele değişmez nesnesine sahip olduğun zaman, doğru yapıldığını ve beklenmedik bir değeri olmadığını biliyorsun. Yanlış durumlu değişmez bir nesne olmamalıdır.
Walfrat

2
@AaronLS bir inşaatçıda alıcılar olabilir; konu o değil. Buna gerek yok, ancak bir inşaatçı alıcılara sahipse, bu özellik henüz belirtilmemişse getAge, inşaatçıyı çağırmanın geri dönebileceğinin farkında olmalısınız null. Buna karşılık, Personsınıf, yaşın hiçbir zaman olamayacağı değişmezleri zorlayabilir null, bu da OP ile yapılan bir new Person() .withName("John")örneklemede olduğu gibi, üretici ve gerçek nesne karıştırıldığı zaman imkansızdır Person. Bu, değişkenler bile değişmez sınıflar için geçerlidir, çünkü belirleyiciler değişmezleri zorlayabilir, ancak ilk değerleri zorlayamaz.
Holger

1
@ Puanlarınız doğru olsun, ancak temel olarak bir Oluşturucu'nun alıcılardan kaçınması gerektiği argümanını destekleyin.
user949300,

21

Bunun bir nedeni, aktarılan tüm verilerin iş kurallarına uymasını sağlamak olacaktır.

Örneğiniz bunu dikkate almıyor, ancak diyelim ki birisinin boş bir dize veya özel karakterlerden oluşan bir dize geçirdiğini söyleyelim. İsimlerinin gerçekten geçerli bir isim olduğundan emin olmak için bir tür mantık yapmak istersiniz (bu aslında çok zor bir iştir).

Tüm bunları, özellikle mantık çok küçükse (örneğin, bir yaşın negatif olmadığından emin olmak için), ancak mantık büyüdükçe, onu ayırmanın bir anlamı yoktur.


7
john
Sorudaki

6
+1 Ayrıca, oluşturucu sınıfı olmadan aynı anda iki alan için kural yapmak çok zordur (alanlar X + Y, 10'dan büyük olamaz).
Reginald Blue,

7
@ReginaldBlue "x + y bile olsa" ile bağlıysa daha da zordur. (0,0) ile olan ilk durumumuz tamam, Durum (1,1) 'de de sorun yok, ancak hem durumu geçerli tutan hem de (0,0)' den (1) 'e kadar alan tek değişkenli güncelleme dizisi yok. , 1).
Michael Anderson

Bunun en büyük avantaj olduğuna inanıyorum. Diğerlerinin hepsi güzel.
Giacomo Alzetta

5

Bu konuda diğer cevaplarda gördüğümden biraz farklı bir açı.

Buradaki withFooyaklaşım problemlidir, çünkü belirleyiciler gibi davranırlar ancak sınıfın değişmezliği destekleyecek şekilde tanımlanırlar. Java sınıflarında, bir yöntem bir özelliği değiştirirse, yöntemi 'set' ile başlatmak normaldir. Bunu asla standart olarak sevmedim ama başka bir şey yaparsanız, insanları şaşırtacak ve bu iyi değil. Burada sahip olduğunuz temel API ile değişmezliği desteklemenin başka bir yolu var. Örneğin:

class Person {
  private final String name;
  private final Integer age;

  private Person(String name, String age) {
    this.name = name;
    this.age = age;
  }  

  public Person() {
    this.name = null;
    this.age = null;
  }

  Person withName(String name) {
    return new Person(name, this.age);
  }

  Person withAge(int age) {
    return new Person(this.name, age);
  }
}

Uygun olmayan şekilde kısmen oluşturulmuş nesneleri önleme yolunda pek bir şey sağlamaz, fakat mevcut nesnelerdeki değişiklikleri önler. Bu tür bir şey için muhtemelen saçma (ve JB Builder da öyle). Evet, daha fazla nesne yaratacaksınız ama bu düşündüğünüz kadar pahalı değil .

Çoğunlukla, CopyOnWriteArrayList gibi eşzamanlı veri yapılarında kullanılan bu tür yaklaşımları göreceksiniz . Bu da değişmezliğin neden önemli olduğunu gösteriyor. Kod iş parçacığınızı güvenli hale getirmek istiyorsanız, değişkenlik neredeyse her zaman dikkate alınmalıdır. Java'da, her iş parçacığının değişken bir yerel önbellek tutmasına izin verilir. Bir iş parçacığının diğer iş parçacıklarında yapılan değişiklikleri görebilmesi için senkronize bir blok ya da diğer eşzamanlılık özelliğinin kullanılması gerekir. Bunlardan herhangi biri koda biraz yük ekleyecektir. Fakat değişkenleriniz kesinse, yapacak hiçbir şey yok. Değer her zaman için başlatılan değer olacaktır ve bu nedenle tüm iplikler ne olursa olsun aynı şeyi görür.


2

Burada daha önce açıkça belirtilmemiş olan bir başka neden, build()yöntemin, tüm alanların 'alanların geçerli değerler içerdiğini doğrulayabilmesidir (ya doğrudan ayarlanır ya da diğer alanların diğer değerlerinden türetilir), muhtemelen en büyük olasılıkla başarısızlık modudur. Aksi takdirde ortaya çıkar.

Başka bir yararı, Personnesnenizin daha basit bir ömre ve basit bir değişmez dizisine sahip olmasıdır. Biliyorsun ki, bir kere sahip olduktan sonra Person p, sende p.nameve de geçerli olur p.age. Yöntemlerinizden hiçbiri "yaş ayarlanmış, ancak ad belirtilmemişse veya ad ayarlanmış, ancak yaş belirtilmemişse" ne olacak? Bu, sınıfın genel karmaşıklığını azaltır.


"[...] tüm alanların ayarlandığını doğrulayabilir [...]" Oluşturucu deseninin amacı, tüm alanları kendiniz ayarlamak zorunda olmadığınız ve mantıklı varsayılanlara geri dönebileceğinizdir. Yine de tüm alanları ayarlamanız gerekiyorsa, belki de bu deseni kullanmamalısınız!
Vincent Savard,

@VincentSavard Gerçekten, ama benim tahminime göre, en yaygın kullanım budur. Bazı "çekirdek" değer kümelerinin ayarlandığını doğrulamak ve bazı isteğe bağlı olanların etrafında bazı süslü işlemler yapmak (varsayılanları ayarlamak, değerlerini diğer değerlerden türetmek vb.).
Alexander - Monica

'Tüm alanların ayarlandığını doğrulayın' demek yerine, bunu 'tüm alanların geçerli değerleri içerdiğini doğrulamak [bazen diğer alanların değerlerine bağlı olarak]' (yani bir alan yalnızca değere bağlı olarak belirli değerlere izin verebilir) olarak değiştirirdim. başka bir alanın). Bu, her ayarlayıcıda yapılabilir, ancak nesne tamamlandıktan ve kuruluma hazır oluncaya kadar istisnalar atmadan birisinin nesneyi kurması daha kolaydır.
yitzih

İnşa edilmekte olan sınıfın yapıcısı sonuçta iddialarının geçerliliğinden sorumludur. Yapıcıdaki onaylama en iyi şekilde gereksizdir ve yapıcının gereksinimleriyle kolayca senkronize edilemez.
Hayır U

@TKK Gerçekten. Ancak farklı şeyleri doğrularlar. Yapıcı’nın içinde kullandığım birçok durumda, yapıcının işi, en sonunda yapıcıya verilen girdileri oluşturmaktı. Örneğin, yapıcı, a URI, a Fileveya a ile yapılandırılmıştır FileInputStreamve FileInputStreamsonuçta yapıcı çağrısına bir argüman olarak giren bir sonuç elde etmek için sağlanan her şeyi kullanır
Alexander - Reinstate Monica,

2

Bir kurucu ayrıca bir arayüz veya soyut bir sınıf döndürmek için tanımlanabilir. Oluşturucu, nesneyi tanımlamak için kullanabilirsiniz ve oluşturucu, hangi özelliklerin ayarlandığına veya örneğin neye ayarlandıklarına bağlı olarak hangi somut alt sınıfın geri döneceğini belirleyebilir.


2

Oluşturucu deseni, özellikleri ayarlayarak nesneyi adım adım oluşturmak / oluşturmak için kullanılır ve gerekli tüm alanlar ayarlandığında, daha sonra derleme yöntemini kullanarak son nesneyi döndürün. Yeni oluşturulan nesne değişmez. Burada dikkat edilmesi gereken ana nokta, nesnenin yalnızca son derleme yöntemi çağrıldığında döndürüldüğüdür. Bu, tüm özelliklerin nesneye ayarlandığından ve dolayısıyla oluşturucu sınıf tarafından döndürüldüğünde nesnenin tutarsız durumda olmadığından emin olur.

Oluşturucu sınıfını kullanmazsak ve doğrudan tüm oluşturucu sınıf yöntemlerini Person sınıfının kendisine koyarsak, önce Nesneyi yaratmalı ve sonra yaratılan nesnenin yaratım arasındaki tutarsız durumuna götürecek olan yaratılan nesneye ayarlayıcı yöntemleri çağırmalıyız. Nesne ve özellikleri ayarlama.

Bu nedenle, oluşturucu sınıfını (yani, Person sınıfının kendisinden başka bir harici varlık) kullanarak, nesnenin asla tutarsız durumda olmayacağına dair güvence veririz.


@DawidO benim fikrim var. Buraya koymaya çalıştığım asıl vurgu tutarsız durumda. Ve bu, Builder desenini kullanmanın ana avantajlarından biridir.
Shubham Bondarde

2

Oluşturucu nesnesini yeniden kullanın

Diğerlerinin de belirttiği gibi, değişmezlik ve nesneyi doğrulamak için tüm alanların iş mantığını doğrulama, ayrı bir oluşturucu nesnenin ana nedenleridir.

Ancak yeniden kullanılabilirlik bir başka faydadır. Çok benzer pek çok nesneyi başlatmak istersem, oluşturucu nesnede küçük değişiklikler yapabilir ve başlatmaya devam edebilirim. Oluşturucu nesnesini yeniden oluşturmaya gerek yok. Bu yeniden kullanım, yapıcının birçok değişmez nesne oluşturmak için bir şablon görevi görmesini sağlar. Küçük bir avantaj, ancak yararlı olabilir.


1

Aslında, sınıfın kendisinde oluşturucu yöntemleri olabilir ve yine de değişmezliğe sahip olabilirsiniz. Bu, yalnızca yapıcı yöntemlerin mevcut olanı değiştirmek yerine yeni nesneler döndüreceği anlamına gelir.

Bu, yalnızca bir başlangıç ​​(geçerli / faydalı) nesne (örneğin, tüm gerekli alanları ayarlayan bir kurucudan veya varsayılan değerleri ayarlayan bir fabrika yönteminden) elde etmenin bir yolu varsa ve ek oluşturucu yöntemleri daha sonra değiştirilmiş nesneleri temel alarak döndürürse işe yarar. Mevcut olanı. Bu oluşturucu yöntemlerinin yolda geçersiz / tutarsız nesneler alamadığınızdan emin olmanız gerekir.

Tabii ki bu, çok fazla sayıda yeni nesneye sahip olacağınız ve nesnelerinizi oluşturmak pahalı ise bunu yapmamalısınız.

Bunu , iş nesnelerimden biri için Hamcrest eşleştiricileri oluşturmak için test kodunda kullandım . Tam kodu hatırlamıyorum, fakat şunun gibi görünüyordu (basitleştirilmiş):

public class CustomerMatcher extends TypeSafeMatcher<Customer> {
    private final Matcher<? super String> nameMatcher;
    private final Matcher<? super LocalDate> birthdayMatcher;

    @Override
    protected boolean matchesSafely(Customer c) {
        return nameMatcher.matches(c.getName()) &&
               birthdayMatcher.matches(c.getBirthday());
    }

    private CustomerMatcher(Matcher<? super String> nameMatcher,
                            Matcher<? super LocalDate> birthdayMatcher) {
        this.nameMatcher = nameMatcher;
        this.birthdayMatcher = birthdayMatcher;
    }

    // builder methods from here on

    public static CustomerMatcher isCustomer() {
        // I could return a static instance here instead
        return new CustomerMatcher(Matchers.anything(), Matchers.anything());
    }

    public CustomerMatcher withBirthday(Matcher<? super LocalDate> birthdayMatcher) {
        return new CustomerMatcher(this.nameMatcher, birthdayMatcher);
    }

    public CustomerMatcher withName(Matcher<? super String> nameMatcher) {
        return new CustomerMatcher(nameMatcher, this.birthdayMatcher);
    }
}

Daha sonra bunu birim testlerimde kullanırdım (uygun statik ithalat ile):

assertThat(result, is(customer().withName(startsWith("Paŭlo"))));

1
Bu doğru - ve bence bu çok önemli bir nokta, ama son sorunu çözmüyor - Size, inşa edildiğinde nesnenin tutarlı bir durumda olduğunu onaylama şansı vermiyor. Oluşturulduktan sonra, arayan kişinin tüm yöntemleri (korkunç bir uygulama olacaktır) ayarladığından emin olmak için ticari yöntemlerden herhangi birinden önce doğrulayıcıyı aramak gibi bir kandırmaya ihtiyacınız olacak. Yapıcı paternini neden yaptığımız gibi kullandığımızı görmek için bu tür şeyleri düşünmenin iyi olacağını düşünüyorum.
Bill K

@BillK true - temelde değişmez ayarlayıcıları olan (her seferinde yeni bir nesne döndüren) bir nesne, döndürülen her nesnenin geçerli olması gerektiği anlamına gelir. Geçersiz nesneler (ör., nameAncak sahibi olmayan kişi age) döndürüyorsanız , konsept çalışmaz. Aslında, PartiallyBuiltPersongeçerli olmayan bir şeyi iade ediyor olabilirsiniz, ancak Oluşturucu'yu maskelemek için bir saldırı gibi görünüyor.
VLAZ

@BillK, "ilk (geçerli / kullanışlı) bir nesneyi elde etmenin bir yolu varsa" ile demek istediğim şeydi - Her ilk oluşturucu işlevinden döndürülen hem ilk nesnenin hem de nesnenin geçerli / tutarlı olduğunu varsayıyorum. Böyle bir sınıfın yaratıcısı olarak göreviniz, yalnızca bu tutarlı değişiklikleri yapan oluşturucu yöntemler sağlamaktır.
Paŭlo Ebermann
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.