2016-09-17 2 views
6

In Kotlin, ich bin ein Baumeister und wollen eine Reihe von Schritten zu schreiben, die offensichtlich sind und müssen ausgefüllt werden. Mit einem flüssigen Builder kann ich alle Schritte darstellen, aber nicht wirklich die Reihenfolge, in der sie auftreten müssen, noch kann ich ändern, welche auf dem vorherigen Schritt verfügbar sind. Also:In Kotlin, wie können Sie die Entscheidungen in einer fließenden Builder begrenzen für verschiedene Gabeln Einstellungen

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

ist in Ordnung, aber dann Hinzufügen von Optionen wie SSL-Zertifikate:

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

nun nichts verhindert, dass die Nicht-ssl-Version von mit den withKeystore und anderen Optionen. Es sollte ein Fehler sein, wenn dieses SSL-Methode aufrufen, ohne zuerst das Einschalten withSsl():

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

Und es könnte mit mehr Gabeln in der Straße komplizierter sein, wo ich will nur einige Objekte vorhanden und andere nicht.

Wie beschränke ich welche Funktionen an jeder Gabelung in der Builder-Logik zur Verfügung steht? Ist das für einen Baumeister unmöglich und sollte stattdessen ein DSL sein?

Hinweis:diese Frage absichtlich geschrieben und beantwortete vom Autor (Self-Answered Questions), so dass die idiomatischen Antworten auf häufig gestellte Kotlin Themen in SO vorhanden sind.

Antwort

3

Sie müssen Ihren Builder als mehr von einem DSL mit einer Reihe von Klassen statt nur einer Klasse denken; auch wenn man sich an das Builder-Muster hält. Der Kontext der Grammatik ändert, welche Builder-Klasse gerade aktiv ist.

Beginnen wir mit einer einfachen Option starten, dass nur die Builder-Klasse Gabeln, wenn der Benutzer zwischen HTTP (Standard) und HTTPS auswählt, der Erbauer haptisches Gefühl behalten:

Eine schnelle Erweiterungsfunktion, die wir machen verwenden werden, um fließend Methoden Prettier:

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

Nun ist die Haupt-Code:

// 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")) 
     } 
    } 
} 

Und ein Helfer fu nction beginnt ein Builder Sie Ihren Code in der Frage oben entsprechen: wir eine innere Klasse verwenden

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

In diesem Modell, das auf einigen Werten des Bauherrn weiterarbeiten kann und gegebenenfalls seine eigenen einzigartigen Werte tragen und einzigartig Validierung des endgültigen build(). Der Builder übergibt den Kontext des Benutzers an diese innere Klasse unter dem Aufruf withSsl().

Daher wird der Benutzer nur die Optionen an jeder „Weggabelung“ erlaubt begrenzt. Der Aufruf withKeystore() vor withSsl() ist nicht mehr erlaubt. Sie haben den Fehler, den Sie wünschen.

Eine Ausgabe hier ist, dass Sie manuell von der inneren Klasse zurück auf die äußere Klasse delegieren müssen alle Einstellungen, die Sie arbeiten fortsetzen wollen. Wenn das eine große Anzahl wäre, könnte das nervig sein. Stattdessen könnten Sie allgemeine Einstellungen in eine Schnittstelle machen, und class delegation aus der geschachtelten Klasse auf die äußere Klasse zu delegieren.

So, hier ist der Erbauer Refactoring eine gemeinsame Schnittstelle zu verwenden:

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 
} 

Mit der verschachtelten Klasse delegierenden über diese Schnittstelle an den Außen:

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")) 
     } 
    } 
} 

Wir sind mit dem gleichen Nettoeffekt am Ende . Wenn Sie zusätzliche Gabeln haben, können Sie weiterhin die Schnittstelle für die Vererbung öffnen und Einstellungen für jede Ebene in einem neuen Nachkommen für jede Ebene hinzufügen.

Obwohl das erste Beispiel aufgrund einer kleinen Anzahl von Einstellungen kleiner sein könnte, könnte es umgekehrt sein, wenn es viel mehr Einstellungen gibt und wir mehr Gabeln auf der Straße hatten, die immer mehr Einstellungen aufbauten Das Interface + Delegationsmodell speichert zwar nicht viel Code, aber es verringert die Wahrscheinlichkeit, dass Sie eine bestimmte Methode zum Delegieren vergessen oder eine andere Methodensignatur als erwartet haben.

Es ist ein subjektiver Unterschied zwischen den beiden Modellen.

über DSL Stil Builder verwenden statt:

Wenn Sie ein DSL-Modell stattdessen verwendet, zum Beispiel:

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

Sie haben den Vorteil, dass Sie zu Einstellungen keine Sorgen über das Delegieren oder die Reihenfolge der Methodenaufrufe, weil Sie innerhalb einer DSL den Bereich eines partiellen Builders betreten und verlassen und daher bereits eine Kontextverschiebung haben. Das Problem hierbei ist, dass der Bereich von einem äußeren Objekt zu einem inneren Objekt bluten kann, da Sie implizierte Empfänger für jeden Teil der DSL verwenden. Dies wäre möglich:

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" 
      } 
     } 
    } 
} 

So können Sie nicht in den HTTPS Umfang von Blutungen einige HTTP-Eigenschaften verhindern. Dies soll in KT-11551 behoben werden, siehe hier für weitere Details: Kotlin - Restrict extension method scope

+0

"So können Sie nicht verhindern, dass einige HTTP-Eigenschaften in den HTTPS-Bereich gelangen." - Für jeden Hinweis wird dies in Kotlin 1.1 behoben (verwenden Sie die Annotation '@ DslMarker'). –

Verwandte Themen