21 Temmuz 2020 Salı

GoF - Builder Örüntüsü

Giriş
Builder yerine bazı kodlarda Provider isminin kullanıldığını da gördüm. Bence Builder ismi çok daha anlaşılır.

Builder'ın Amacı Nedir
Açıklaması şöyle
Let's remember the purpose of Builder: set only necessary fields in some object and keep remaining fields set to default values. For example, if we're preparing a configuration object, then it's convenient to change only the necessary parameters and keep other parameters set to default values.
Builder'ın Yanlış Kullanılması
Açıklaması şöyle
Unfortunately, many developers pick only part of the Builder pattern — the ability to set fields individually. The second part — presence of reasonable defaults for remaining fields — is often ignored. As a consequence, it's quite easy to get incomplete (partially initialized) POJO. In an attempt to mitigate this issues we add checks to the build() method and get a (false) feeling of safety. Unfortunately, by this moment, the main damage is already done: checks are shifted to run time. And to make sure that everything is OK, we need to add dedicated tests to cover all execution paths in code where POJO is created.
Builder ve Static Metodlar Arasındaki Fark
Bazı sınıflar constructor yerine static metodlar vererek yaratılmalarını isterler. Bu yöntemi kabaca Builder örüntüsüne benzetiyorum. Ancak Builder'a göre daha basit bir kullanım şekli sunuyor. Direkt constructor kullanımına göre şu avantaja sahip. Static metod bir hata durumunda null dönebilir veya exception atabilir. Constructor kullanımında null döndürmek zaten imkansız. Constructor içinden nesneyi belirsiz bir durumda bırakmak pahasına exception atmak ise tehlikeli. Bu yüzden çok daha iyi bir kullanım şekli ortaya koyuyor.

Builder ve Factory Arasındaki Fark
Builder ve Factory benzer işleri yapıyorlar. Factory örüntüsünde daha çok polymorphism öne çıkarken (yani yaratması daha basit nesneler vardır), Builder çok sayıda parametre gerektiren nesnelerin yaratılmasını kolaylaştırmayı hedefliyor.

Aşağıdaki kod parçasında Pizza için bir çok seçenek sunuluyor. Bu seçenekler birbirlerinden bağımsız olarak Pizza'ya eklenebilir. Factory bu tür nesneleri yaratmak için uygun değil!
Pizza(int size) { ... }       
Pizza(int size, boolean cheese) { ... }   
Pizza(int size, boolean cheese, boolean pepperoni) { ... }   
Pizza(int size, boolean cheese, boolean pepperoni, boolean bacon) { ... }
Builder Örüntüsü Method Chaining Kullanabilir
Method Chaining okunurluğu kolaylaştıran bir yöntem. Açıklaması şöyle. Yani hem Fluent Interface hem de Builder örüntüsü için Method Chaining kullanılır
Method chaining pattern has been around for years in Java and other languages. It is widely used to implement builders or fluent interfaces
Örnek
Aşağıdaki örnekte karmaşık bir nesne yaratılmıyor. Sadece nesneye aynı işlem birçok defa uygulanıyor. Dolayısıyla örnek bir Builder kabul edilmeyebilir
Map<String, Integer> m = new HashMap<String, Integer>()
  .put("a", 1)
  .put("b", 2)
  .put("c", 3);
Örnek
Method Chaining'e çok benzeyen ancak halen builder kabul edilebilecek örnekler var.
new XmlBuilder(body)
    .start("ProductId").append(product.getId()).end()
    .start("Size").append(product.getSize()).end();
Builder Zorunlu Parametreleri Constructor İçine Alır
Builder zorunlu parametreleri mutlaka constructor'ı içine alarak başlamalıdır. Böylece zorunlu parametreleri girmeden nesne yaratılamaz ve half constructed/illegal bir nesne yaratılması önlenir.
new Builder(requiredA, requiredB).setOptionalA("optional").Build();
Örnek
Elimizde şöyle bir kod olsun. name ve surname girilmeden yaratılırsa, invalid state'te kalan bir nesne var. Eğer bu nesnenin getCompleteName() metodu çağrılırsa, hata alırız.
public void String name;
public void String surname;

public String getCompleteName() {return name + " " + surname;}

public void displayCompleteNameOnInvoice() {
    String completeName = getCompleteName();
    //do something with it....
}
Örnekte Builder constructor metodu size parametresini içine alıyor.
public class Pizza {

  private int size;
  private boolean cheese;
  private boolean pepperoni;
  private boolean bacon;

  public static class Builder {
    //required
    private final int size;

    //optional
    private boolean cheese = false;

    public Builder(int size) {
      this.size = size;
    }

    public Builder cheese(boolean value) {
      cheese = value;
      return this;
    }

    public Pizza build() {
      return new Pizza(this);
    }
  }

  private Pizza(Builder builder) {
    size = builder.size;
    cheese = builder.cheese;
    pepperoni = builder.pepperoni;
    bacon = builder.bacon;
  }
}
Builder Zorunlu Olmayan Parametreler İçin Setter Metodlar Sunar
Seçime bağlı parametreler içinse default bir değer ile başlanır. Bu değerleri değiştirmek için metodlar sunulduğu için istenirse değiştirilebilirler.

Builder ve Değişmeyen (Immutable) Nesneler
Örnek
Değişmeyen nesneyi kullanarak yeni bir nesne yaratmak için şöyle yaparız. Zorunlu parametre olarak eski nesne kullanılmıyor ama Builder from() metodu ile dolduruluyor.
Book oldBook = new Book.Builder()//
        .withTitle("Hello")//
        .withYear(1900)//
        .withAuthors(Arrays.asList("Alice"))//
        .build();
Book newBook = new Book.Builder()//
        .from(oldBook)//
        .withTitle("World")//
        .withYear(1901)//
        .build();
Builder ve Çağrı Sırası
Burada amaç belli setter() metodların sırasıyla çağrıldıktan sonra build() metodunun çağrılabilmesini sağlamak.

Örnek
Şöyle yaparız. Burada builder() metodu biraz karışık. Lambda döndüren lambda gibi. Aslında anonymous sınıf döndüren metod gibi düşünmek anlamayı daha kolay yapabilir.

SimpleBean.builder().index(1).name("name") gibi kullanılır.
public class SimpleBean {

  private final int index;
  private final String name;

  private SimpleBean(final int index, final String name) {
    this.index = index;
    this.name = name;
  }

  public int index() {
    return index;
  }

  public String name() {
    return name;
  }

  public static Builder builder() {
    return index -> name -> new SimpleBean(index, name);
  }

  public interface Builder {
    Stage1 index(int index);
      interface Stage1 {
        SimpleBean name(final String name);
      }
  }
}
Blind Builder
Java İçin Builder Örüntüsü Örnekleri yazısına taşıdım.

Bloch Builder
Java İçin Builder Örüntüsü Örnekleri yazısına taşıdım.

Complex Nesneler İçin Builder
Bazı nesneler complex yapıdadır ve iki farklı nesnenin aynı anda yaratılması gerekir. Bu tür complex nesneler için her nesneye mahsus ayrı bir builder kullanılabilir

Örnek 
Elimizde şöyle bir JobBuilder kodu olsun
public interface IJobBuilder {
    IJobBuilder WithCompanyName(string companyName);
    IJobBuilder WithSalary(int salary);
}

public class JobBuilder : IJobBuilder {
    private readonly Job job;

    public JobBuilder() {
        job = new Job();
    }

    public IJobBuilder WithCompanyName(string companyName) {
        job.CompanyName = companyName;
        return this;
    }

    public IJobBuilder WithSalary(int amount) {
        job.Salary = amount;
        return this;
    }

    internal Job Build() => job;
}
 Bir de PersonBuilder kodu olsun. PersonBuilder aynı zamanda JobBuilder nesnesini de kullanıyor.
public class PersonBuilder {
    protected Person Person;
    
    private PersonBuilder() { Person = new Person(); }

    public static PersonBuilder Create() => new PersonBuilder();

    public PersonBuilder WithName(string name) {
        Person.Name = name;
        return this;
    }

    public PersonBuilder HavingJob(Action<IJobBuilder> configure) {
        var builder = new JobBuilder();
        configure(builder);
        Person.Jobs.Add(builder.Build());
        return this;
    }

    public Person Build() => Person;

}
Bu ikisini kullanmak için şöyle yaparız
var p = PersonBuilder
            .Create()
                .WithName("My Name")
                .HavingJob(builder => builder
                    .WithCompanyName("First Company")
                    .WithSalary(100)
                )
                .HavingJob(builder => builder
                    .WithCompanyName("Second Company")
                    .WithSalary(200)
                )
            .Build();
Fluent Interface Kullanımı
Builder örüntüsü genellikle Fluent Interface kullanımına dayanır. Fluent kullanım tercih edilmemişse aşağıdaki gibi ayrı ayrı setter metodlar ile de kullanılabilir.
Person.Builder builder = new Person.Builder();
builder.setName(name);
builder.setAge(age);
Person p = builder.build();
Builder örüntüsünde çok sayıdaki parametre okunurluğu azaltıyorsa belli composite yapılar şeklinde kullanmak hedeflenebilir. Bu kullanıma Parameter Object deniliyor.

Örneğin
MyBuilder().setWidth(float width).setHeight (float heigth) 
yerine
MyBuilder().setDimension (Dimension d)
şeklinde bir kullanım seçilebilir.

Builder ve Hata Yakalama
Hatalı geçilen parametreleri yakalamak için 3 yöntem var.  Doğru yöntem yapılan işe göre değişebilir. Hataları yakalamak, veriyi doğrulamak için ayrı bir validator sınıfı yazılabilir.

1. Builder'ın Setter metodları içinde.
Örneğin sadece pozitif sayıları kabul eden bir setter metoduna, negatif bir sayı girilirse IllegalArgument() atılabilir. Eğer parametreler birbirlerine bağımlı ise tüm parametreleri görmeden bu yöntem kullanılamayabilir.

2. Builder'ın build() metodu içinde
Sınıfın Single Responsibility kuralını biraz ihlal edebilir.

3. Builder'ın yarattığı nesnenin validate() metodu içinde.
Burada nesne kendi kendini validate eder.

Hiç yorum yok:

Yorum Gönder