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
"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'). –