2016-09-17 6 views
6

私は建築家を書いており、明白で完了する必要があります一連の手順が欲しいです。流暢なビルダーでは、すべてのステップを提示できますが、実際に実行する順序を設定することはできません。また、前のステップに基づいてどのステップを使用可能にするかを変更することもできません。だから、:コトリンでは、どのように設定の異なるフォークのための流暢なビルダーの選択を制限することができます

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

は結構ですが、その後、SSL証明書のようなオプションを追加:

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

今は何もwithKeystoreや他のオプションを持っていることから非SSLバージョンを防ぎません。

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

そして、それは私が唯一の他人一部のオブジェクトが存在していないしたい道路のより多くのフォークを持つより複雑かもしれません。そこ最初withSsl()をオンにすることなく、このSSLメソッドを呼び出すときにエラーでなければなりません。

ビルダーロジックの各フォークでどの機能を使用できるかを制限するにはどうすればよいですか?これは建築業者にとって不可能なのですか?代わりにDSLでなければなりませんか?

注:慣用的な答えは、一般的に尋ねすることKotlinのトピックがSOに存在しているように、この質問は、意図的に、作者(Self-Answered Questions)によって書かれ、答えています。

答えて

3

ビルダーは、1つのクラスではなく、一連のクラスを持つDSLのように考える必要があります。たとえビルダーパターンを貼っていても。ビルダークラスが現在アクティブな文法の変更のコンテキスト。

ユーザーは、ビルダーの感触を保ち、HTTP(デフォルト)とHTTPSの間で選択した場合にのみ、フォークビルダークラスという単純なオプションを指定して起動するのをしてみましょう:

私たちが作るために使用します迅速な拡張機能きれい流暢な方法:

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

今メインコード:

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

とヘルパーのfu私たちは、ビルダーのいくつかの値を操作し続け、必要に応じて独自の値を運ぶとユニークなことができ、内部クラスを使用するこのモデルでは

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

:上記の質問に、あなたのコードに合わせて、ビルダーをオフに開始するnction最終的なbuild()の検証。ビルダーはwithSsl()呼び出しでこの内部クラスにユーザーのコンテキストを移行します。

したがって、ユーザーは、「道路内の分岐」で許可されるオプションのみに制限されます。 withSsl()の前にwithKeystore()を呼び出すことはできません。あなたはあなたが望むエラーがあります。

ここで問題となるのは、引き続き動作させたい設定を内部クラスから外部クラスに手動で委譲する必要があることです。これが多数だった場合、これは迷惑になる可能性があります。代わりに、インターフェイスに共通の設定を行い、class delegationを使用して、ネストされたクラスから外部クラスに委譲することができます。外側には、このインタフェースを介して、ネストされたクラスの委譲で

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 
} 

:だからここ

は共通のインタフェースを使用するリファクタリングビルダーです

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

私たちは、同じ正味の効果で終わります。追加の分岐がある場合は、引き続き継承のインターフェイスを開き、各レベルの新しい子孫の各レベルの設定を追加できます。

設定の数が少ないため、最初の例は小さくなる可能性がありますが、はるかに多くの設定がある場合は逆になる可能性がありますが、道路にはさらに設定が増えていますインターフェイス+委任モデルは多くのコードを保存することはできませんが、委任する特定のメソッドを忘れたり、予想よりも異なるメソッドシグネチャを忘れる可能性が低くなります。

2つのモデルの主観的な違いです。代わりに、DSLスタイルビルダーの使用について

あなたの代わりにDSLモデルを使用した場合、例えば:

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

あなたが設定を委任を心配する必要がないという利点を持っていますDSL内では部分ビルダーのスコープに入り込んだり終了したりする傾向があるため、すでにコンテキストがシフトしているため、メソッド呼び出しの順番が異なります。ここでの問題は、DSLの各部分に暗黙的な受信者を使用しているため、スコープが外部オブジェクトから内部オブジェクトに流出する可能性があることです。これは可能です:

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

HTTPプロパティの一部がHTTPSスコープに流出するのを防ぐことはできません。これはKT-11551で修正されています。詳細はこちらを参照してください。Kotlin - Restrict extension method scope

+0

"HTTPプロパティの一部がHTTPSスコープに流出するのを防ぐことはできません。 - 誰もが参照できるように、これはKotlin 1.1で修正されています( '@ DslMarker'アノテーションを使います)。 –

関連する問題