2016-09-17 10 views
6

W Kotlin, piszę konstruktora i chcę serii kroków, które są oczywiste i muszą zostać zakończone. Dzięki płynnemu konstruktorowi mogę przedstawić wszystkie kroki, ale nie mogę ustawić kolejności, w jakiej muszą wystąpić, ani nie mogę zmienić, które z nich są dostępne na podstawie poprzedniego kroku. Więc:W Kotlin, w jaki sposób można ograniczyć wybory w płynnym budowniczym dla różnych rozwidleń ustawień

serverBuilder().withHost("localhost") 
     .withPort(8080) 
     .withContext("/something") 
     .build() 

jest w porządku, ale potem dodanie opcji jak CERT SSL:

serverBuilder().withHost("localhost") 
     .withSsl() 
     .withKeystore("mystore.kstore") 
     .withContext("/secured") 
     .build() 

Teraz nic nie stoi wersję non-SSL od posiadanie withKeystore i inne opcje. Nie powinno być błąd przy wywołaniu tej metody SSL bez wcześniejszego włączania withSsl():

serverBuilder().withHost("localhost") 
     .withPort(8080) 
     .withContext("/something") 
     .withKeystore("mystore.kstore") <------ SHOULD BE ERROR! 
     .build() 

I to może być bardziej skomplikowane z kilku widelce w drodze gdzie chcę tylko niektóre obiekty obecne a nie innym.

Jak ograniczyć funkcje dostępne dla każdego z rozwidleń w logice budowniczej? Czy to niemożliwe dla konstruktora, a zamiast tego powinien być DSL?

Uwaga:kwestia ta jest celowo napisany i odpowiedział przez autora (Self-Answered Questions), tak że idiomatyczne odpowiedzi na najczęściej zadawane tematy Kotlin są obecne w SO.

Odpowiedz

3

Musisz myśleć o swoim konstruktorze jako o DSL z serią zajęć, a nie tylko z jedną klasą; nawet jeśli trzymasz się wzorca budowniczego. Kontekst zmian gramatycznych, które klasa budowniczych jest obecnie aktywna.

Zacznijmy od prostych opcji, które widelce klasa budowniczy tylko wtedy, gdy użytkownik wybiera między HTTP (domyślnie) i HTTPS, utrzymując atmosferę producentów:

Szybka funkcja rozszerzenie, które użyjemy do dokonywania metody biegle ładniejsza:

fun <T: Any> T.fluently(func:()->Unit): T { 
    return this.apply { func() } 
} 

teraz kod główny:

// our main builder class 
class HttpServerBuilder internal constructor() { 
    private var host: String = "localhost" 
    private var port: Int? = null 
    private var context: String = "/" 

    fun withHost(host: String) = fluently { this.host = host } 
    fun withPort(port: Int) = fluently { this.port = port } 
    fun withContext(context: String) = fluently { this.context = context } 

    // !!! transition to another internal builder class !!! 
    fun withSsl(): HttpsServerBuilder = HttpsServerBuilder() 

    fun build(): Server = Server(host, port ?: 80, context, false, null, null) 

    // our context shift builder class when configuring HTTPS server 
    inner class HttpsServerBuilder internal constructor() { 
     private var keyStore: String? = null 
     private var keyStorePassword: String? = null 

     fun withKeystore(keystore: String) = fluently { this.keyStore = keyStore } 
     fun withKeystorePassword(password: String) = fluently { this.keyStorePassword = password } 

     // manually delegate to the outer class for withPort and withContext 
     fun withPort(port: Int) = fluently { [email protected] = port } 
     fun withContext(context: String) = fluently { [email protected] = context } 

     // different validation for HTTPS server than HTTP 
     fun build(): Server { 
      return Server(host, port ?: 443, context, true, 
        keyStore ?: throw IllegalArgumentException("keyStore must be present for SSL"), 
        keyStorePassword ?: throw IllegalArgumentException("KeyStore password is required for SSL")) 
     } 
    } 
} 

A fu pomocnik nction zacząć budowniczy, aby dopasować swój kod w pytaniu powyżej:

fun serverBuilder(): HttpServerBuilder { 
    return HttpServerBuilder() 
} 

W tym modelu możemy wykorzystać wewnętrzną klasę, która może nadal działać na pewnych wartościach budowniczego i ewentualnie przeprowadzić swoje własne unikalne wartości i wyjątkowy zatwierdzenie ostatecznego build(). Konstruktor przenosi kontekst użytkownika na tę wewnętrzną klasę wywołania withSsl().

Dlatego użytkownik jest ograniczony tylko do opcji dozwolonych w każdym "widelcu na drodze". Dzwonienie pod numer withKeystore() przed withSsl() nie jest już dozwolone. Masz błąd, którego pragniesz.

Problem polega na tym, że musisz ręcznie przekazać z wewnętrznej klasy z powrotem do zewnętrznej klasy wszelkie ustawienia, które chcesz kontynuować. Jeśli byłaby to duża liczba, może to być denerwujące. Zamiast tego możesz wprowadzić wspólne ustawienia w interfejsie i użyć class delegation do delegowania z zagnieżdżonej klasy do zewnętrznej klasy.

Więc tutaj jest budowniczym refactored aby korzystać ze wspólnego interfejsu:

private interface HttpServerBuilderCommon { 
    var host: String 
    var port: Int? 
    var context: String 

    fun withHost(host: String): HttpServerBuilderCommon 
    fun withPort(port: Int): HttpServerBuilderCommon 
    fun withContext(context: String): HttpServerBuilderCommon 

    fun build(): Server 
} 

Z zagnieżdżonej klasy delegujący za pośrednictwem tego interfejsu, aby zewnętrzna:

class HttpServerBuilder internal constructor(): HttpServerBuilderCommon { 
    override var host: String = "localhost" 
    override var port: Int? = null 
    override var context: String = "/" 

    override fun withHost(host: String) = fluently { this.host = host } 
    override fun withPort(port: Int) = fluently { this.port = port } 
    override fun withContext(context: String) = fluently { this.context = context } 

    // transition context to HTTPS builder 
    fun withSsl(): HttpsServerBuilder = HttpsServerBuilder(this) 

    override fun build(): Server = Server(host, port ?: 80, context, false, null, null) 

    // nested instead of inner class that delegates to outer any common settings 
    class HttpsServerBuilder internal constructor (delegate: HttpServerBuilder): HttpServerBuilderCommon by delegate { 
     private var keyStore: String? = null 
     private var keyStorePassword: String? = null 

     fun withKeystore(keystore: String) = fluently { this.keyStore = keyStore } 
     fun withKeystorePassword(password: String) = fluently { this.keyStorePassword = password } 

     override fun build(): Server { 
      return Server(host, port ?: 443, context, true, 
        keyStore ?: throw IllegalArgumentException("keyStore must be present for SSL"), 
        keyStorePassword ?: throw IllegalArgumentException("KeyStore password is required for SSL")) 
     } 
    } 
} 

Skończymy z tym samym efektem netto . Jeśli masz dodatkowe rozwidlenia, możesz nadal otwierać interfejs dziedziczenia i dodawać ustawienia dla każdego poziomu w nowym potomku dla każdego poziomu.

Mimo że pierwszy przykład może być mniejszy ze względu na niewielką liczbę ustawień, może być odwrotnie, gdy jest o wiele większa liczba ustawień i mieliśmy więcej widelców na drodze, które budowały coraz więcej ustawień, a następnie model interfejsu + delegacji może nie zapisywać dużej ilości kodu, ale zmniejszy szansę, że zapomnisz określonej metody delegowania lub podpisu innej metody niż oczekiwano.

Jest to subiektywna różnica między tymi dwoma modelami.

O użyciu stylu DSL budowniczy zamiast:

Jeśli używany model DSL zamiast, na przykład:

Server { 
    host = "localhost" 
    port = 80 
    context = "/secured" 
    ssl { 
     keystore = "mystore.kstore" 
     password = "[email protected]!" 
    } 
} 

mieć tę zaletę, że nie trzeba się martwić o delegowaniu ustawienia lub kolejność wywołań metod, ponieważ w DSL masz tendencję do wchodzenia i wychodzenia z zakresu częściowego budowniczego, a zatem już masz pewne przesunięcie kontekstu. Problem polega na tym, że ponieważ używasz dorozumianych odbiorników dla każdej części DSL, zakres może spływać z zewnętrznego obiektu do wewnętrznego obiektu. Byłoby to możliwe:

Server { 
    host = "localhost" 
    port = 80 
    context = "/secured" 
    ssl { 
     keystore = "mystore.kstore" 
     password = "[email protected]!" 
     ssl { 
      keystore = "mystore.kstore" 
      password = "[email protected]!" 
      ssl { 
       keystore = "mystore.kstore" 
       password = "[email protected]!" 
       port = 443 
       host = "0.0.0.0" 
      } 
     } 
    } 
} 

Nie można zatem uniemożliwić kradzieży niektórych właściwości HTTP do zakresu HTTPS. Ta funkcja ma zostać naprawiona w KT-11551, zobacz tutaj, aby uzyskać więcej informacji: Kotlin - Restrict extension method scope

+0

"Nie można więc uniemożliwić kradzieży niektórych właściwości HTTP do zakresu HTTPS." - dla odniesienia każdego, jest to ustalone w Kotlin 1.1 (użyj adnotacji '@ DslMarker'). –

Powiązane problemy