どのようにkotlinでDSLを書きます(進級編)

9228 ワード

「kotlinでDSLを書く方法」では、前回に続きます.ここではAddress属性を持つPersonオブジェクトを初期化するために非常に簡単な分野専用言語を作成した.今、これは開発者が現実生活でDSLを作成すべきものではないかもしれません.DSLは、複雑さを低減し、可読性を向上させるために使用されるべきである.私たちが作成したDSLにはあまり付加的なものはありません.しかし、これは簡単な例で、私たちはここから3つのことを説明し続けます.
  • ビルダーモード
  • 構造集合
  • DslMarkerを用いる役割ドメイン
  • を縮小する.
    その後、GsonBuilderにDSLを作成することで、実際の例を示します.
    コンストラクタモードの適用
    だから前の部分では2つの非常に基本的なデータクラスがあり、これは最初の例にとってメリットがあります.しかしクラスには可変変数があり、その属性を簡単に変更することができます.可変valに変更できると、Valが再割り当てできないというコンパイルエラーが発生します.このような状況を回避するために、クラスのコンストラクタを作成できます.このモデルを見てみましょう.
    
    data class Person(val name: String,
                      val dateOfBirth: Date,
                      var address: Address?)
    
    data class Address(val street: String,
                       val number: Int,
                       val city: String)
    

    これらのオブジェクトを作成するには、コンストラクション関数のプロパティ値を使用する必要があります.ご覧のように、Dateプロパティも追加しました.これは後で議論します.
    私たちが書くコンストラクタはとても簡単です.データのみを構築し、最後に構築関数を呼び出し、構築関数を使用してオブジェクトを作成します.
    fun person(block: PersonBuilder.() -> Unit): Person = PersonBuilder().apply(block).build()
    
    
    class PersonBuilder {
      
        var name: String = ""
    
        private var dob: Date = Date()
        var dateOfBirth: String = ""
            set(value) {
                dob = SimpleDateFormat("yyyy-MM-dd").parse(value)
            }
    
        private var address: Address? = null
    
        fun address(block: AddressBuilder.() -> Unit) {
            address = AddressBuilder().apply(block).build()
        }
    
        fun build(): Person = Person(name, dob, address)
    
    }
    
    class AddressBuilder {
    
        var street: String = ""
        var number: Int = 0
        var city: String = ""
    
        fun build() : Address = Address(street, number, city)
    
    }
    
    

    ご覧の通りです.追加の文字列属性dateOfBirthを追加し、dobのプライベート属性とカスタムsetterメソッドを追加したので、より読み取り可能な方法で日付を設定することができます.私たちはコンストラクタを使っているので、もっと多くのものを把握しています.結果は次のとおりです.
    
    val person = person {
        name = "John"
        dateOfBirth = "1980-12-01"
        address {
            street = "Main Street"
            number = 12
            city = "London"
        }
    }
    

    コレクションについて
    これらのコンストラクタを持っています.Collectionをモデルに追加しましょう.例えば、これからは、Listに格納された1人に複数のアドレスを有することができる.これは簡単です.コンストラクタのアドレス属性をMutableList に変更し、アドレス関数に新しいアドレスを追加し、Listをコンストラクタに渡すだけです.次のように変化します.
    data class Person(val name: String,
                      val dateOfBirth: Date,
                      val addresses: List
    )
    class PersonBuilder {
      
      // ... other properties
      
      private val addresses = mutableListOf
    () fun address(block: AddressBuilder.() -> Unit) { addresses.add(AddressBuilder().apply(block).build()) } fun build(): Person = Person(name, dob, addresses) }
    // And the result
    val person = person {
        name = "John"
        dateOfBirth = "1980-12-01"
        address {
            street = "Main Street"
            number = 12
            city = "London"
        }
        address {
            street = "Dev Avenue"
            number = 42
            city = "Paris"
        }
    }
    

    だからこれは簡単です.しかし、DSLが好きではないかもしれませんが、アドレスブロック内で見たいと思っています.
    これを行うには、アドレスアシストクラスを作成します.これにより、PersonBuilderアドレス関数lambdaに提供される受信機として使用することができる.この補助クラスをArrayListで拡張したので、アドレスオブジェクトを簡単に追加できます.
    fun person(block: PersonBuilder.() -> Unit): Person = PersonBuilder().apply(block).build()
    
    class PersonBuilder {
    
        var name: String = ""
        private var dob: Date = Date()
        var dateOfBirth: String = ""
            set(value) { dob = SimpleDateFormat("yyyy-MM-dd").parse(value) }
    
        private val addresses = mutableListOf
    () fun addresses(block: ADDRESSES.() -> Unit) { addresses.addAll(ADDRESSES().apply(block)) } fun build(): Person = Person(name, dob, addresses) } class ADDRESSES: ArrayList
    () { fun address(block: AddressBuilder.() -> Unit) { add(AddressBuilder().apply(block).build()) } } class AddressBuilder { var street: String = "" var number: Int = 0 var city: String = "" fun build() : Address = Address(street, number, city) }

    これは補助クラスであることを強調するために、大文字で名前を付けることができます.これはDSLでは表示されません.これは良い構造を得ることができます.
    val person = person {
        name = "John"
        dateOfBirth = "1980-12-01"
        addresses {
            address {
                street = "Main Street"
                number = 12
                city = "London"
            }
            address {
                street = "Dev Avenue"
                number = 42
                city = "Paris"
            }
        }
    }
    
    

    役割ドメインの縮小
    この結果はよさそうです.その可読性、メンテナンス性、およびセキュリティの使用は、受信者付きlambda式のおかげです.でも一つ問題があります
    DSLを使用すると、コンテキストで多くの関数を呼び出すことができるという問題が発生する可能性があります.lambda式の内部で利用可能な各暗黙的受信者の方法を呼び出すことができ、それによって不一致な結果を得ることができます.
    val person = person {
        name = "John"
        dateOfBirth = "1980-12-01"
        addresses {
            address {
                addresses { 
                    name = "Mary"
                }
                street = "Dev Avenue"
                number = 42
                city = "Paris"
            }
        }
    }
    
    

    これを実行すると、person.nameは「John」ではなく「Mary」です.幸いなことに、Kotlin 1.1から@DslMarker注釈でこのような状況を避けることができます.この注釈はカスタム注釈クラスに適用され、DSLクラスに注釈されます.
    この注釈を追加すると、Kotlinコンパイラは、どの暗黙的な受信者が同じDSLの一部であるかを認識し、最近のレイヤの受信者のメンバーのみを呼び出すことができます.
    @DslMarker
    annotation class PersonDsl
    
    @PersonDsl
    class PersonBuilder {
      //...
    }
    
    @PersonDsl
    class ADDRESSES: ArrayList
    () { //... } @PersonDsl class AddressBuilder { //... }

    コンパイラは最後の例のエラーを与えました.アドレスと名前は、このコンテキストで暗黙的な受信機によって呼び出されません.必要に応じて、内部lambda式で明示的な受信者:[email protected] = “Mary”を使用することができます.
    例:GsonBuilderの簡略化
    この最後の例では、GsonBuilderの実際の例として独自の内部DSLを作成した迅速な例を示します.
    シーケンス化と逆シーケンス化のgsonインスタンスを作成する場合は、ライブラリはGsonBuilderを提供してインスタンスを簡単に構成します.ただし、(de)シーケンス化フィールドまたはクラスをスキップしたい場合は、(de)シーケンス化フィールドまたはクラスをスキップします.かなり混乱していますこの例では
    val gson = GsonBuilder()
              .addDeserializationExclusionStrategy(object: ExclusionStrategy {
                  override fun shouldSkipClass(clazz: Class?): Boolean {
                      return clazz?.equals(Address::class.java) ?: false
                  }
    
                  override fun shouldSkipField(f: FieldAttributes?): Boolean {
                      return f?.let { it.name == "internalId" } ?: false
                  }
    
              })
              .addSerializationExclusionStrategy(object: ExclusionStrategy {
                  override fun shouldSkipClass(clazz: Class?): Boolean {
                      return false
                  }
    
                  override fun shouldSkipField(f: FieldAttributes?): Boolean {
                      return f?.let { it.declaringClass == Person::class.java && it.name == "address" } ?: false
                  }
    
              })
              .serializeNulls()
              .create()
    

    このGsonオブジェクトは、逆シーケンス化されたAddressクラスおよびinternalIdというフィールドを排除する.また、シーケンス化Person.addressも排除される.問題はその可読性にある.
    関数名addDeserializationExlusionStrategyが単純に見えても.しかし、elvisオペレータ?:のようなKotlinの特性はJavaよりも短いが、よりよくできる.本明細書の最後の2つのセクションで説明したKotlin言語機能を使用することで、DSLを作成し、以下に示すように同じコードを持つことができます.
    val  gson  = gson {
        dontDeserialize {
            whenField {name ==  “ internalId ” }
            whenClass {  (  :: 。 JAVA)}
        }
        dontSerialize {
            whenField {declaringClass == Person :: class。java && name ==“ address ”}
        }
        serializeNulls()
    }
    

    結果は、より読み取りやすく、理解しやすく、簡潔になります.実装方法を見てみましょう.
    fun gson(block: GsonBuilder.() -> Unit): Gson = GsonBuilder().apply(block).create()
    
    fun GsonBuilder.dontDeserialize(block: ExclusionStrategyBuilder.() -> Unit) {
        val strategy = ExclusionStrategyBuilder().apply(block).build()
        this.addDeserializationExclusionStrategy(strategy)
    }
    
    fun GsonBuilder.dontSerialize(block: ExclusionStrategyBuilder.() -> Unit) {
        val strategy = ExclusionStrategyBuilder().apply(block).build()
        this.addSerializationExclusionStrategy(strategy)
    }
    
    class ExclusionStrategyBuilder {
    
        private var field: (FieldAttributes) -> Boolean = { false }
        private var clazz: (Class) -> Boolean? = { false }
    
        fun whenField(block: FieldAttributes.() -> Boolean) {
            field = block
        }
    
        fun whenClass(block: Class.() -> Boolean) {
            clazz = block
        }
    
        fun build(): ExclusionStrategy {
            return object : ExclusionStrategy {
                override fun shouldSkipClass(clazz: Class?): Boolean {
                    return clazz?.let { clazz(it) } ?: false
                }
    
                override fun shouldSkipField(f: FieldAttributes?): Boolean {
                    return f?.let { field(it) } ?: false
                }
    
            }
        }
    }
    
    

    残念なことに、@DslMarker注記をここに追加することはできません.GsonBuilderは可変であり、拡張することもできません.それでも、Gsonインスタンスの構築が容易になります.