Metot zincirleme kullanırken, nesneyi yeniden mi kullanırım yoksa bir tane mi yaratırım?


37

Gibi yöntem zincirleme kullanırken:

var car = new Car().OfBrand(Brand.Ford).OfModel(12345).PaintedIn(Color.Silver).Create();

iki yaklaşım olabilir:

  • Bunun gibi aynı nesneyi tekrar kullanın:

    public Car PaintedIn(Color color)
    {
        this.Color = color;
        return this;
    }
  • CarHer adımda bunun gibi yeni bir tür nesne oluşturun :

    public Car PaintedIn(Color color)
    {
        var car = new Car(this); // Clone the current object.
        car.Color = color; // Assign the values to the clone, not the original object.
        return car;
    }

İlki yanlış mı yoksa geliştiricinin kişisel bir seçimi mi?


İlk yaklaşımının hızla sezgisel / yanıltıcı koda neden olabileceğine inanıyorum. Örnek:

// Create a car with neither color, nor model.
var mercedes = new Car().OfBrand(Brand.MercedesBenz).PaintedIn(NeutralColor);

// Create several cars based on the neutral car.
var yellowCar = mercedes.PaintedIn(Color.Yellow).Create();
var specificModel = mercedes.OfModel(99).Create();

// Would `specificModel` car be yellow or of neutral color? How would you guess that if
// `yellowCar` were in a separate method called somewhere else in code?

Düşüncesi olan var mı?


1
Neyin var var car = new Car(Brand.Ford, 12345, Color.Silver);?
James

12
@James teleskopik yapıcı, akıcı desen, isteğe bağlı ve gerekli parametreler arasında ayrım yapmanıza yardımcı olabilir (eğer isteğe bağlı değilse yapıcı gerekliyse). Ve akıcı okumak oldukça güzel.
NimChimpsky

8
@NimChimpsky eski moda (C # için) özelliklere ve gerekli alanlara sahip bir yapıcıya ne oldu - Fluent API'leri patlattığım için değil, büyük bir hayranıyım ama sık sık aşırı kullanılıyorlar
Chris S

8
@ChrisS, ayarlayıcılara güvenirseniz (java'lıyım), nesnelerinizi yapmak istemeyeceğiniz değişken hale getirmeniz gerekir. Ayrıca akıcı kullanırken daha iyi intellitext elde edersiniz - daha az düşünmeyi gerektirir, ide neredeyse sizin için nesneyi oluşturur.
NimChimpsky

1
@NimChimpsky yeh Java için ne kadar akıcı bir ilerleme olduğunu görebiliyorum
Chris S

Yanıtlar:


41

Ben koyardım akıcı api buna yaratıyor nesneden kendi "oluşturucu" sınıfı ayrı var etmek. Bu şekilde, müşteri akıcı API'yi kullanmak istemiyorsa yine de manuel olarak kullanabilirsiniz ve etki alanı nesnesini kirletmez (tek bir sorumluluk ilkesine bağlı kalarak). Bu durumda, aşağıdakiler oluşturulacaktır:

  • Car etki alanı nesnesi hangisi
  • CarBuilder akıcı API tutan

Kullanımı şöyle olurdu:

var car = CarBuilder.BuildCar()
    .OfBrand(Brand.Ford)
    .OfModel(12345)
    .PaintedIn(Color.Silver)
    .Build();

CarBuilderSınıf (burada C # adlandırma kuralını kullanıyorum) şu şekilde görünecektir:

public class CarBuilder {

    private Car _car;

    /// Constructor
    public CarBuilder() {
        _car = new Car();
        SetDefaults();
    }

    private void SetDefaults() {
        this.OfBrand(Brand.Ford);
          // you can continue the chaining for 
          // other default values
    }

    /// Starts an instance of the car builder to 
    /// build a new car with default values.
    public static CarBuilder BuildCar() {
        return new CarBuilder();
    }

    /// Sets the brand
    public CarBuilder OfBrand(Brand brand) {
        _car.SetBrand(brand);
        return this;
    }

    // continue with OfModel(...), PaintedIn(...), and so on...
    // that returns "this" to allow method chaining

    /// Returns the built car
    public Car Build() {
        return _car;
    }

}

Bu sınıfın iş parçacığı güvenli olmayacağını unutmayın (her iş parçacığının kendi CarBuilder örneğine ihtiyacı olacaktır). Ayrıca, akıcı api gerçekten harika bir kavram olsa da, basit etki alanı nesneleri oluşturmak için büyük olasılıkla overkill olduğuna dikkat edin.

Bu anlaşma, çok daha soyut bir şey için bir API oluşturuyorsanız ve daha karmaşık bir kurulum ve yürütme işlemine sahipseniz daha faydalı olur, bu nedenle birim test ve DI çerçevelerinde harika çalışıyor. Kalıcılık, tarih işleme ve alay konusu nesnelerle wikipedia Fluent Interface makalesinin Java bölümünde bazı diğer örnekleri görebilirsiniz .


DÜZENLE:

Yorumlardan da belirtildiği gibi; Builder sınıfını statik bir iç sınıf (Car içinde) yapabilir ve Car değişmez hale getirilebilir. Car'in değişmez kalmasına izin veren bu örnek biraz saçma görünüyor; ancak, daha karmaşık bir sistemde, yerleşik nesnenin içeriğini kesinlikle değiştirmek istemediğiniz durumlarda, bunu yapmak isteyebilirsiniz.

Aşağıda hem statik iç sınıfın nasıl yapılacağına hem de oluşturduğu değişmez bir nesne oluşturmanın nasıl ele alınacağına bir örnek verilmiştir:

// the class that represents the immutable object
public class ImmutableWriter {

    // immutable variables
    private int _times; private string _write;

    // the "complex" constructor
    public ImmutableWriter(int times, string write) {
        _times = times;
        _write = write;
    }

    public void Perform() {
        for (int i = 0; i < _times; i++) Console.Write(_write + " ");
    }

    // static inner builder of the immutable object
    protected static class ImmutableWriterBuilder {

        // the variables needed to construct the immutable object
        private int _ii = 0; private string _is = String.Empty;

        public void Times(int i) { _ii = i; }

        public void Write(string s) { _is = s; }

        // The stuff is all built here
        public ImmutableWriter Build() {
            return new ImmutableWriter(_ii, _is);
        }

    }

    // factory method to get the builder
    public static ImmutableWriterBuilder GetBuilder() {
        return new ImmutableWriterBuilder();
    }
}

Kullanım aşağıdaki gibi olacaktır:

var writer = ImmutableWriter
                .GetBuilder()
                .Write("peanut butter jelly time")
                .Times(2)
                .Build();

writer.Perform();
// console writes: peanut butter jelly time peanut butter jelly time 

Düzenleme 2: Pete , yorumlarda lambda işlevli inşaatçıların karmaşık etki alanı nesneleriyle yazılan birim testleri bağlamında kullanılması hakkında bir blog yazısı yayınladı . Yapıcıyı biraz daha anlamlı kılmak ilginç bir alternatiftir.

Durumda CarBuilderbunun yerine bu yöntemi olması gerekir:

public static Car Build(Action<CarBuilder> buildAction = null) {
    var carBuilder = new CarBuilder();
    if (buildAction != null) buildAction(carBuilder);
    return carBuilder._car;
}

Bu da kullanılabilir:

Car c = CarBuilder
    .Build(car => 
        car.OfBrand(Brand.Ford)
           .OfModel(12345)
           .PaintedIn(Color.Silver);

3
@Baqueta, Josh Bloch'un etkin java'sını ana hatlarıyla
açıkladı

6
@ Baqueta, Java dev, imho için okuma gerekli
NimChimpsky

3
IMHO'nun çok büyük bir avantajı, yapım aşamasında tamamlanmamış olan nesnenin inşaatçıdan kaçmasını önlemek için bu modeli (uygun şekilde değiştirilmişse) kullanabilmenizdir. Örneğin, tanımlanmamış bir renkte hiçbir araç olmayacağından emin olabilirsiniz.
eşarp

1
Hmm ... Ben her zaman oluşturucu modelinin son yöntemini çağırdım build()(veya Build()), oluşturduğu türün adını değil ( Car()örneğinizde). Ayrıca, Cargerçekten değişmez bir nesne ise (örneğin, tüm alanları readonly), o zaman bile inşaatçı onu mutasyona uğratamayacak, böylece Build()yöntem yeni örneği oluşturmaktan sorumlu olacaktır. Bunu yapmanın bir yolu Car, bir Oluşturucuyu argümanı olarak alan tek bir kurucuya sahip olmaktır ; o zaman Build()yöntem sadece olabilir return new Car(this);.
Daniel Pryden

1
Lambdalara dayalı inşaatçılar yaratmada farklı bir yaklaşım hakkında blog yazdım. Gönderi muhtemelen biraz düzenleme gerektiriyor. Bağlamım çoğunlukla birim test kapsamı içindeydi, ancak uygulanabilirse diğer alanlara da uygulanabilir. Burada bulunabilir: petesdotnet.blogspot.com/2012/05/…
Pete

9

Bu bağlıdır.

Sizin Araç bir mi Varlık veya Değer nesnesi ? Araba bir varlık ise, nesne kimliği önemlidir, bu yüzden aynı referansı geri vermelisiniz. Nesne bir değer nesnesiyse, değişmez olmalıdır, yani tek yol her seferinde yeni bir örnek döndürmektir.

İkincisinin bir örneği, bir değer nesnesi olan .NET'te DateTime sınıfı olabilir.

var date1 = new DateTime(2012,1,1);
var date2 = date1.AddDays(1);
// date2 now refers to Jan 2., while date1 remains unchanged at Jan 1.

Bununla birlikte, model bir varlık ise, Spoike'ın size nesne oluşturmak için bir oluşturucu sınıf kullanma konusundaki cevabını beğendim. Başka bir deyişle, verdiğiniz bu örnek yalnızca IMHO anlamına gelir, eğer Araba bir değer nesnesiyse.


1
'Varlık' vs 'Değer' sorusu için +1 Sınıfınızın değişebilir mi yoksa değişmez bir tür mü olduğu (bu nesne değiştirilmeli mi?) Ve tasarımınızı etkileyecek olsa da tamamen size kalmış bir soru. Genellikle, yöntem yeni bir nesne döndürmedikçe, yöntem zincirinin değişken bir tür üzerinde çalışmasını beklemem.
Casey Kuball

6

Ayrı bir statik iç oluşturucu oluşturun.

Gerekli parametreler için normal yapıcı değişkenlerini kullanın. Ve isteğe bağlı olarak akıcı api.

NewCarInColour yöntemini veya benzeri bir şeyi yeniden adlandırmazsanız, renk ayarlarken yeni bir nesne oluşturmayın.

Gerektiği gibi marka ve isteğe bağlı geri kalan isteğe bağlı olarak böyle bir şey yapardım (bu java, ancak sizinki javascript gibi görünüyor, ancak bir miktar nitelemeyle değiştirilebileceklerinden emin olabilirsiniz):

Car yellowMercedes = new Car.Builder(Brand.MercedesBenz).PaintedIn(Color.Yellow).create();

Car specificYellowModel =new Car.Builder(Brand.MercedesBenz).WithModel(99).PaintedIn(Color.Yellow).create();

4

En önemlisi, seçtiğiniz karar ne olursa olsun, yöntem adı ve / veya yorumunda açıkça belirtilmiş olmasıdır.

Standart yoktur, bazen yöntem yeni bir nesne döndürür (String yöntemlerinin çoğu bunu yapar) veya bu nesneyi zincirleme amacıyla veya bellek verimliliği için döndürür).

Bir keresinde bir 3B Vector nesnesi tasarladım ve her matematik işleminde her iki yöntemi de uyguladım. Anlık ölçek yöntemi için:

Vector3D scaleLocal(float factor){
    this.x *= factor; 
    this.y *= factor; 
    this.z *= factor; 
    return this;
}

Vector3D scale(float factor){
    Vector3D that = new Vector3D(this); // clone this vector
    return that.scaleLocal(factor);
}

3
+1. Çok iyi bir nokta. Bunun neden aşağıya indirildiğini anlamadım. Ancak, seçtiğiniz isimlerin çok net olmadığını not edeceğim. Onlara scale(mutasyon) ve scaledBy(jeneratör) derdim .
back2dos

İyi mesele, isimler daha net olabilirdi. Adlandırma, kütüphaneden kullandığım diğer matematiksel sınıfların bir düzenlemesini takip etti. Etki, karışıklığı önlemek için kullanılan yöntemin javadoc yorumlarında da belirtildi.
XGouchet

3

Burada kafa karıştırıcı olabileceğini düşündüğüm birkaç problem görüyorum ... Sorunun ilk sırası:

var car = new Car().OfBrand(Brand.Ford).OfModel(12345).PaintedIn(Color.Silver).Create();

Bir yapıcı (yeni) ve bir yaratma yöntemi çağırıyorsunuz ... Bir yaratma () yöntemi hemen hemen her zaman statik bir yöntem veya oluşturucu bir yöntem olur ve derleyici bunu bir uyarı veya hatayla yakalamalıdır; bu sözdiziminde ya yanlış ya da bazı korkunç isimler var. Fakat daha sonra ikisini de kullanmazsınız, o yüzden şuna bakalım.

// Create a car with neither color, nor model.
var mercedes = new Car().OfBrand(Brand.MercedesBenz).PaintedIn(NeutralColor);

// Create several cars based on the neutral car.
var yellowCar = mercedes.PaintedIn(Color.Yellow).Create();
var specificModel = mercedes.OfModel(99).Create();

Yine yarattıktan sonra, sadece yeni bir kurucu ile değil. Şey, bunun yerine bir copy () yöntemini aradığınızı düşünüyorum. Öyleyse durum buysa ve sadece zayıf bir isim, hadi bir şeye bakalım ... mercedes.Paintedin (Color.Yellow) .Copy () diyorsunuz - Şuna bakmak kolay ve boyandığını söylemek kolay olmalı 'kopyalanmadan önce - bana sadece normal bir mantık akışı. Öyleyse önce kopyayı koy.

var yellowCar = mercedes.Copy().PaintedIn(Color.Yellow)

Bana göre, orada kopyayı boyadığınızı, sarı arabanızı yaptığınızı görmek kolaydır.


New ve Create () arasındaki uyumsuzluğu işaret etmek için +1;
Joshua Drake

1

İlk yaklaşımda bahsettiğiniz dezavantajı var, ancak dokümanlardaki tüm açık kodlayıcıların sorunu çözmemesi gerektiğinden emin olduğunuzda. Şahsen birlikte çalıştığım tüm yöntem zincirleme kodu bu şekilde çalıştı.

İkinci yaklaşımın daha fazla iş yapmanın sakıncaları olduğu açık. Ayrıca, iade ettiğiniz kopyaların sığ mı yoksa derin kopyalar mı yapılacağına karar vermelisiniz: en iyisi sınıftan sınıfa veya yöntemden yönteme değişebilir, bu nedenle tutarsızlık ya da en iyi davranıştan ödün vermeyeceksiniz. Bunun, dizeler gibi değişmez nesneler için tek seçenek olduğunu kaydetmeye değer.

Ne yaparsanız yapın, aynı sınıf içinde karıştırmayın ve eşleştirmeyin!


1

Tıpkı "Uzatma Yöntemleri" mekanizması gibi düşünmeyi tercih ederim.

public Car PaintedIn(this Car car, Color color)
{
    car.Color = color;
    return car;
}

0

Bu, yukarıdaki yöntemlerde bir varyasyondur. Aradaki fark, Car sınıfında, Builder'daki yöntem adlarıyla eşleşen statik yöntemler olduğudur, bu nedenle açıkça bir Oluşturucu oluşturmanız gerekmez:

Car car = Car.builder().ofBrand(Brand.Ford).ofColor("Green")...

Zincirleme üretici aramalarında kullandığınız aynı yöntem adlarını kullanabilirsiniz:

Car car = Car.ofBrand(Brand.Ford).ofColor("Green")...

Ayrıca, sınıftaki geçerli örnekdeki tüm değerlerle doldurulmuş bir oluşturucu döndüren bir .copy () yöntemi vardır, böylece bir tema üzerinde bir varyasyon oluşturabilirsiniz:

Car red = car.copy().paintedIn("Red").build();

Son olarak, yapıcının .build () yöntemi, gerekli tüm değerlerin sağlanmış olup olmadığını kontrol eder ve eksikse fırlatır. Yapıcının yapıcısı üzerinde bazı değerler talep etmek ve geri kalanının isteğe bağlı olmasına izin vermek tercih edilebilir; Bu durumda, diğer cevaplardaki kalıplardan birini istersiniz.

public enum Brand {
    Ford, Chrysler, GM, Honda, Toyota, Mercedes, BMW, Lexis, Tesla;
}

public class Car {
    private final Brand brand;
    private final int model;
    private final String color;

    public Car(Brand brand, int model, String color) {
        this.brand = brand;
        this.model = model;
        this.color = color;
    }

    public Brand getBrand() {
        return brand;
    }

    public int getModel() {
        return model;
    }

    public String getColor() {
        return color;
    }

    @Override public String toString() {
        return brand + " " + model + " " + color;
    }

    public Builder copy() {
        Builder builder = new Builder();
        builder.brand = brand;
        builder.model = model;
        builder.color = color;
        return builder;
    }

    public static Builder ofBrand(Brand brand) {
        Builder builder = new Builder();
        builder.brand = brand;
        return builder;
    }

    public static Builder ofModel(int model) {
        Builder builder = new Builder();
        builder.model = model;
        return builder;
    }

    public static Builder paintedIn(String color) {
        Builder builder = new Builder();
        builder.color = color;
        return builder;
    }

    public static class Builder {
        private Brand brand = null;
        private Integer model = null;
        private String color = null;

        public Builder ofBrand(Brand brand) {
            this.brand = brand;
            return this;
        }

        public Builder ofModel(int model) {
            this.model = model;
            return this;
        }

        public Builder paintedIn(String color) {
            this.color = color;
            return this;
        }

        public Car build() {
            if (brand == null) throw new IllegalArgumentException("no brand");
            if (model == null) throw new IllegalArgumentException("no model");
            if (color == null) throw new IllegalArgumentException("no color");
            return new Car(brand, model, color);
        }
    }
}
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.