Java: Ayarlayıcıların sırasının önemli olmadığı bir adım oluşturucu nasıl uygulanır?


10

Düzenleme: Bu sorunun teorik bir sorunu açıkladığını belirtmek isterim ve zorunlu parametreler için yapıcı bağımsız değişkenlerini kullanabilir veya API yanlış kullanılırsa bir çalışma zamanı özel durumu atabilirim. Ancak, ben bir çözüm için arıyorum değil yapıcı argümanları veya çalışma zamanı denetimini gerektirir.

Bunun Cargibi bir arayüzünüz olduğunu düşünün :

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional
}

Comments öneririm gibi, bir Carbir olmalı Engineve Transmissionancak Stereoisteğe bağlıdır. Bu, build()bir Carörneğin gerçekleştirilebileceği bir Oluşturucu'nun yalnızca oluşturucu örneğine build()bir Engineve Transmissiondaha önce verilmiş olması durumunda bir yöntemi olması gerektiği anlamına gelir . Bu şekilde tür denetleyicisi, veya Carolmadan örnek oluşturmaya çalışan herhangi bir kodu derlemeyi reddeder .EngineTransmission

Bu bir Adım Oluşturucu gerektirir . Genellikle böyle bir şey uygularsınız:

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional

    public class Builder {
        public BuilderWithEngine engine(Engine engine) {
            return new BuilderWithEngine(engine);
        }
    }

    public class BuilderWithEngine {
        private Engine engine;
        private BuilderWithEngine(Engine engine) {
            this.engine = engine;
        }
        public BuilderWithEngine engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            return new CompleteBuilder(engine, transmission);
        }
    }

    public class CompleteBuilder {
        private Engine engine;
        private Transmission transmission;
        private Stereo stereo = null;
        private CompleteBuilder(Engine engine, Transmission transmission) {
            this.engine = engine;
            this.transmission = transmission;
        }
        public CompleteBuilder engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        public CompleteBuilder stereo(Stereo stereo) {
            this.stereo = stereo;
            return this;
        }
        public Car build() {
            return new Car() {
                @Override
                public Engine getEngine() {
                    return engine;
                }
                @Override
                public Transmission getTransmission() {
                    return transmission;
                }
                @Override
                public Stereo getStereo() {
                    return stereo;
                }
            };
        }
    }
}

Farklı kurucu sınıflarına bir zincir vardır ( Builder, BuilderWithEngine, CompleteBuilder), bu eklenti, bir sıra tüm isteğe bağlı ayarlayıcı yöntemleri içeren son sınıfı ile, birbiri ardına ayarlayıcı bir yöntem gereklidir.
Bu, bu adım oluşturucunun kullanıcılarının , yazarın zorunlu ayarlayıcıları kullanılabilir hale getirdiği sırayla sınırlı olduğu anlamına gelir . İşte olası kullanımlara bir örnek (hepsinin kesinlikle sipariş edildiğini unutmayın: engine(e)önce, ardından transmission(t)ve son olarak isteğe bağlı stereo(s)).

new Builder().engine(e).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).engine(e).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).engine(e).build();
new Builder().engine(e).transmission(t).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).stereo(s).build();

Bununla birlikte, özellikle üreticinin sadece ayarlayıcıları değil, aynı zamanda toplayıcıları varsa veya kullanıcı, oluşturucu için belirli özelliklerin kullanılabilir olacağı sırayı kontrol edemiyorsa, bunun üreticinin kullanıcısı için ideal olmadığı birçok senaryo vardır.

Bunun için düşünebildiğim tek çözüm çok kıvrımlıdır: Belirlenmiş veya henüz belirlenmemiş her zorunlu özellik kombinasyonu için, diğer zorunlu ayarlayıcıların devlet burada build()yöntem mevcut olmalıdır, ve bu belirleyiciler her biri bir adım daha ihtiva eden için kurucu madde daha kapsamlı bir türü verir build()yöntem.
Ben aşağıdaki kodu eklendi, ama senden bir oluşturmanıza olanak sağlayan bir FSM oluşturmak için tip sistemini kullanıyorum söyleyebiliriz Builderya bir dönüştürülebilir, BuilderWithEngineya BuilderWithTransmissionikisi daha sonra dönüştürülebilir hangi CompleteBuilder, hangi uygularbuild()yöntem. Bu oluşturucu örneklerinin herhangi birinde isteğe bağlı ayarlayıcılar çağrılabilir. resim açıklamasını buraya girin

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional

    public class Builder extends OptionalBuilder {
        public BuilderWithEngine engine(Engine engine) {
            return new BuilderWithEngine(engine, stereo);
        }
        public BuilderWithTransmission transmission(Transmission transmission) {
            return new BuilderWithTransmission(transmission, stereo);
        }
        @Override
        public Builder stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class OptionalBuilder {
        protected Stereo stereo = null;
        private OptionalBuilder() {}
        public OptionalBuilder stereo(Stereo stereo) {
            this.stereo = stereo;
            return this;
        }
    }

    public class BuilderWithEngine extends OptionalBuilder {
        private Engine engine;
        private BuilderWithEngine(Engine engine, Stereo stereo) {
            this.engine = engine;
            this.stereo = stereo;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            return new CompleteBuilder(engine, transmission, stereo);
        }
        public BuilderWithEngine engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        @Override
        public BuilderWithEngine stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class BuilderWithTransmission extends OptionalBuilder {
        private Transmission transmission;
        private BuilderWithTransmission(Transmission transmission, Stereo stereo) {
            this.transmission = transmission;
            this.stereo = stereo;
        }
        public CompleteBuilder engine(Engine engine) {
            return new CompleteBuilder(engine, transmission, stereo);
        }
        public BuilderWithTransmission transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        @Override
        public BuilderWithTransmission stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class CompleteBuilder extends OptionalBuilder {
        private Engine engine;
        private Transmission transmission;
        private CompleteBuilder(Engine engine, Transmission transmission, Stereo stereo) {
            this.engine = engine;
            this.transmission = transmission;
            this.stereo = stereo;
        }
        public CompleteBuilder engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        @Override
        public CompleteBuilder stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
        public Car build() {
            return new Car() {
                @Override
                public Engine getEngine() {
                    return engine;
                }
                @Override
                public Transmission getTransmission() {
                    return transmission;
                }
                @Override
                public Stereo getStereo() {
                    return stereo;
                }
            };
        }
    }
}

Anlayacağınız gibi, gerekli farklı oluşturucu sınıflarının sayısı O (2 ^ n) olacaktır, burada n zorunlu ayarlayıcıların sayısıdır.

Dolayısıyla sorum: Bu daha zarif bir şekilde yapılabilir mi?

(Scala da kabul edilebilir olsa da, Java ile çalışan bir cevap arıyorum)


1
Tüm bu bağımlılıkları desteklemek için sadece bir IoC kapsayıcısı kullanmanızı engeller? Ayrıca, neden iddia ettiğiniz gibi sipariş önemli değilse, sadece geri dönen sıradan ayarlayıcı yöntemlerini kullanamazsınız this?
Robert Harvey

.engine(e)Bir inşaatçı için iki kez çağırmak ne anlama geliyor ?
Erik Eidt

3
Her kombinasyon için elle bir sınıf yazmadan statik olarak doğrulamak istiyorsanız, büyük olasılıkla makrolar veya şablon meta programlaması gibi boyun bağı düzeyinde şeyler kullanmanız gerekir. Java, benim için yeterince ifade edici değil ve çaba, diğer dillerde dinamik olarak doğrulanmış çözümler üzerinde muhtemelen buna değmez.
Karl Bielefeldt

1
Robert: Amaç, tip kontrolörünün hem bir motorun hem de bir şanzımanın zorunlu olması gerçeğini güçlendirmesini sağlamak; Hatta arayamam bu şekilde build()aradığınız değil eğer engine(e)ve transmission(t)daha önce.
derabbink

Erik: Varsayılan bir Engineuygulamayla başlayıp daha sonra daha spesifik bir uygulamayla üzerine yazmak isteyebilirsiniz . Ama muhtemelen engine(e)bir pasör değil, toplayıcı olsaydı daha mantıklı olurdu addEngine(e). Bu Car, birden fazla motor / motora sahip hibrid otomobiller üretebilen bir inşaatçı için yararlı olacaktır . Bu anlaşmalı bir örnek olduğu için, neden bunu yapmak isteyebileceğinizle ilgili ayrıntılara girmedim - kısaca.
derabbink

Yanıtlar:


3

Verdiğiniz yöntem çağrılarına bağlı olarak iki farklı gereksiniminiz var gibi görünüyor.

  1. Sadece bir (gerekli) motor, sadece bir (gerekli) şanzıman ve sadece bir (isteğe bağlı) stereo.
  2. Bir veya daha fazla (gerekli) motor, bir veya daha fazla (gerekli) şanzıman ve bir veya daha fazla (isteğe bağlı) stereo.

Sanırım burada ilk sorun , sınıfın ne yapmasını istediğinizi bilmemeniz. Bunun bir kısmı, inşa edilen nesnenin neye benzediğini bilmemenizdir.

Bir otomobilin sadece bir motoru ve bir şanzımanı olabilir. Hibrit otomobillerin bile sadece bir motoru vardır (belki de a GasAndElectricEngine)

Her iki uygulamaya da değineceğim:

public class CarBuilder {

    public CarBuilder(Engine engine, Transmission transmission) {
        // ...
    }

    public CarBuilder setStereo(Stereo stereo) {
        // ...
        return this;
    }
}

ve

public class CarBuilder {

    public CarBuilder(List<Engine> engines, List<Transmission> transmission) {
        // ...
    }

    public CarBuilder addStereo(Stereo stereo) {
        // ...
        return this;
    }
}

Bir motor ve şanzıman gerekiyorsa, bunlar kurucuda olmalıdır.

Hangi motorun veya şanzımanın gerekli olduğunu bilmiyorsanız, henüz bir motor ayarlamayın; oluşturucuyu yığının çok ötesinde oluşturduğunuzu gösteren bir işarettir.


2

Neden null nesne modelini kullanmıyorsunuz? Bu kurucudan kurtulun, yazabileceğiniz en zarif kod aslında yazmak zorunda olmadığınız koddur.

public final class CarImpl implements Car {
    private final Engine engine;
    private final Transmission transmission;
    private final Stereo stereo;

    public CarImpl(Engine engine, Transmission transmission) {
        this(engine, transmission, new DefaultStereo());
    }

    public CarImpl(Engine engine, Transmission transmission, Stereo stereo) {
        this.engine = engine;
        this.transmission = transmission;
        this.stereo = stereo;
    }

    //...

}

Ben de öyle düşünmüştüm. üç parametre yapıcıya sahip olmamama rağmen, sadece zorunlu öğelere sahip iki parametre yapıcısı ve daha sonra isteğe bağlı olarak stereo için bir ayarlayıcı.
Encaitar

1
Bunun gibi basit (çelişkili) bir örnekte Car, c'tor argümanlarının sayısı çok az olduğu için bu mantıklı olacaktır. Bununla birlikte, orta derecede karmaşık bir şeyle (> = 4 zorunlu argüman) ilgilenir ilgilenmez, her şeyin ele alınması / daha az okunabilir olması zorlaşır ("Motor veya şanzıman önce mi geldi?"). Bu yüzden bir oluşturucu kullanırsınız: API, neyi yapılandırdığınız konusunda daha açık olmaya zorlar.
derabbink

1
@derabbink Neden bu durumda sınıfınızı daha küçük olanlarda kırmıyorsunuz? Bir yapıcı kullanmak, sınıfın çok fazla şey yaptığını ve sürdürülemez hale geldiğini gizleyecektir.
Benekli

1
Desen deliliğini bitirmek için şeref.
Robert Harvey

@Spotted bazı sınıflar çok fazla veri içerir. Örneğin, HTTP isteği ile ilgili tüm bilgileri tutan ve verileri CSV veya JSON biçiminde çıktılayan bir erişim günlüğü sınıfı oluşturmak istiyorsanız. Çok fazla veri olacak ve derleme zamanında bazı alanların mevcut olmasını zorunlu kılmak istiyorsanız, çok uzun olmayan bir argüman yapıcısı olan ve iyi görünmeyen bir oluşturucu desenine ihtiyacınız olacaktır.
ssgao

1

Öncelikle, çalıştığım herhangi bir mağazadan çok daha fazla zamanınız yoksa, muhtemelen herhangi bir operasyon sırasına izin vermeye veya sadece birden fazla radyo belirtebileceğiniz gerçeğiyle yaşamaya değmez. Kullanıcı girdisi değil kod hakkında konuştuğunuzu unutmayın, böylece derleme zamanında bir saniye yerine birim testiniz sırasında başarısız olacak iddialara sahip olabilirsiniz.

Ancak, kısıtlamanız, yorumlarda belirtildiği gibi, bir motora ve şanzımana sahip olmanız gerekiyorsa, tüm zorunlu özellikleri üreticinin kurucusudur.

new Builder(e, t).build();                      // ok
new Builder(e, t).stereo(s).build();            // ok
new Builder(e, t).stereo(s).stereo(s).build();  // exception on second call to stereo as stereo is already set 

Sadece isteğe bağlı olan stereo ise, o zaman geliştiricilerin alt sınıflarını kullanarak son adımı yapmak mümkündür, ancak bunun ötesinde, testten ziyade derleme zamanında hatayı almanın kazancı muhtemelen çabaya değmez.


0

gereken farklı oluşturucu sınıflarının sayısı O (2 ^ n) olacaktır, burada n zorunlu ayarlayıcıların sayısıdır.

Bu soru için doğru yönü zaten tahmin ettiniz.

Derleme zamanı denetimi almak istiyorsanız, (2^n)türlere ihtiyacınız olacaktır . Çalışma zamanı denetimi almak istiyorsanız, (2^n)durumları depolayabilen bir değişkene ihtiyacınız olacaktır ; Bir n-bit tamsayı yapacağız.


C ++ destekler çünkü olmayan tip şablon parametresi (örneğin değerleri bir tam sayı) bir C ++ sınıfı şablon içine örneği için, mümkün olduğu O(2^n)kadar benzer bir şema kullanılarak, farklı türde bu .

Ancak, tür olmayan şablon parametrelerini desteklemeyen dillerde, O(2^n)farklı türleri somutlaştırmak için tür sistemine güvenemezsiniz .


Bir sonraki fırsat Java ek açıklamaları (ve C # öznitelikleri). Bu ek meta veriler, ek açıklama işlemcileri kullanıldığında derleme zamanında kullanıcı tanımlı davranışı tetiklemek için kullanılabilir. Ancak, bunları uygulamak sizin için çok fazla iş olacaktır. Bu işlevi sizin için sağlayan çerçeveler kullanıyorsanız, onu kullanın. Aksi takdirde, bir sonraki fırsatı kontrol edin.


Son olarak, O(2^n)farklı durumları çalışma zamanında bir değişken olarak (kelimenin tam anlamıyla, en azından nbit genişliğinde bir tam sayı olarak) depolamanın çok kolay olduğuna dikkat edin. Bu nedenle, en çok oylanan cevaplar, bu kontrolü çalışma zamanında gerçekleştirmenizi önerir, çünkü derleme zamanı kontrolünü uygulamak için gereken çaba, potansiyel kazanca kıyasla çok fazladır.

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.