Управление областью действия в компоновщике Kotlin DSL - PullRequest
0 голосов
/ 15 апреля 2019

Я пытаюсь найти идеальное решение моей проблемы с областью, и мне бы очень хотелось, чтобы вы высказали свое мнение.

У меня есть некоторые сторонние классы, которые я не могу изменить:

class Employee {
    var id = 0
    var name = ""
    var card : Card? = null
    // ...
}

class Card {
    var cardId = 0
}

МойЦель состоит в том, чтобы иметь возможность построить Employee следующим образом:

val built = employee {
     id = 5
     name = "max"
     addCard {
          cardId = 5
     }
}

В исходных компонентах нет метода addCard .Поэтому я придумал следующий компоновщик:

@DslMarker
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
annotation class Scoped

@Scoped
object Builder {
    inline fun employee (init: (@Scoped Employee).() -> Unit): Employee {
        val e = Employee()
        e.init()
        return e
    }

    inline fun Employee.addCard(init: (@Scoped Card).() -> Unit) {
        val c = Card()
        c.init()
        card = c
    }
}

К сожалению, теперь я получаю печально известную ошибку:

error: 'inline fun Employee.addCard (init: (Scratch_1.Card). () -> Unit): Unit 'не может быть вызван в этом контексте неявным получателем.При необходимости используйте явное

Я понимаю причину ошибки и хотел бы подумать о ее решениях.

  1. Удалите аннотацию DSLMarker, чтобыв состоянии наследовать родительскую область.К сожалению, это допускает нелегальное использование компоновщика:

    with(Builder) {
            val built = employee {
                id = 5
                name = "max"
                addCard {
                    employee {
                      // ...
                    }
                cardId = 5
            }
        }
    }   
    
  2. Используйте квалифицированное this для доступа к родительской области.Но тогда мы должны использовать другого квалифицированного это, чтобы получить правильный приемник.Это довольно многословно.

    with(Builder) {
            val built = employee {
                id = 5
                name = "max"
                with(this@with) {
                    this@employee.addCard {
                        cardId = 5
                    }
                }
            }
        }
    
  3. Унаследовать сотрудника, чтобы иметь возможность добавить в него функцию расширения (делегирование здесь невозможно, потому что у меня много свойств в Employee,и там не все определяется интерфейсом).Это не всегда может работать, если класс третьей стороны является окончательным.

    class EmployeeEx : Employee() {
        inline fun addCard(init: (@Scoped Card).() -> Unit) {
            val c = Card()
            c.init()
            card = c
       }
    }      
    

    и строитель:

    @Scoped
    object Builder {
        inline fun employee (init: (@Scoped EmployeeEx).() -> Unit): Employee {
            val e = EmployeeEx()
            e.init()
            return e
        }
    }
    

Так что является лучшим решением??Я что-то пропустил ?Большое спасибо за чтение всего этого!

Ответы [ 3 ]

1 голос
/ 16 апреля 2019
  1. Вы можете определить функцию расширения , не создавая новый класс, это работает и для иностранных неприкасаемых источников:
@DslMarker
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
annotation class Scoped

@Scoped
object Builder {
    inline fun employee(init: (@Scoped Employee).() -> Unit) = Employee().apply(init)
}

fun Employee.addCard(init: (@Scoped Card).() -> Unit) = run { card = Card().apply(init) }
  1. Существует два классических инструмента для управления областью dsl:
    • @DSLMarker для достижимого кода, который вы используете, и
    • @Deprecated (level = ERROR) для всех других случаев, когда первый подход не работает.

Например, в настоящее время вы можете создавать встроенных сотрудников:

val built = employee {
       id = 5
       name = "max"
       addCard {
           cardId = 6
       }
       employee {  }  // <--- compilable, but does not have sense
   }

И вы можете прямо запретить это посредством устаревания:

@Deprecated(level = DeprecationLevel.ERROR, message = "No subcontractors, please.")
fun Employee.employee(init: (@Scoped Employee).() -> Unit) = Unit

Теперь следующий пример не компилируется:

 val built = employee {
        //...
        employee {  }  // <--- compilation error with your message
    }
  1. Вы можете найти это полезным: Пример Kotlin DSL
0 голосов
/ 24 апреля 2019

Хорошо, я думаю, что теперь у меня есть хороший обзор.

Сначала я подумал, что причиной проблемы был IScoped на объекте строителя.При удалении это работает.Однако он по-прежнему допускает «недопустимый» синтаксис:

val built = employe {
        id = 5
        name = "max"
        addCard {
            employe {

            }
            cardId = 5
        }
    }

Решение

Сохраняйте только методы расширения в своем объекте компоновщика и не добавляйте аннотации к последующим.

В моем случае я должен представить другого застройщика, чтобы начать строительство

object EmployeBuilder {   
}

object Builder {
    inline fun EmployeBuilder.employe(init: (@Scoped Employee).() -> Unit): Employee {
        val e = Employee()
        e.init()
        return e
    }

    inline fun Employee.addCard(init: (@Scoped Card).() -> Unit) {
        val c = Card()
        c.init()
        card = c
    }

 }

 fun main() {
    with(Builder) {
        val built = EmployeBuilder.employe {
            id = 5
            name = "max"
            addCard {
                cardId = 5
            }
        }
    }
}

И теперь у нас это есть:

  1. нет "загрязнения"контексты класса, потому что методы расширения доступны только в объекте построителя.
  2. Нельзя использовать недопустимый синтаксис, поскольку все параметры заблокированы аннотацией DslMarker.
0 голосов
/ 18 апреля 2019

Я бы предложил следующий дизайн, он довольно классический, и в результате получается короткий код.

  1. Поскольку Builder добавляет дополнительную область и предотвращает нас от уродливого импорта, мы просто прекращаемиспользуя его вместо with конструкции и оператора перегрузки invoke.

  2. Используйте оба @DslMarker для редактируемого кода и @Deprecated для стороннего для управления областью

@Scoped
object builder {
    operator fun invoke(init: BuildingContext.() -> Unit) = BuildingContext().init()
}

@Scoped
class BuildingContext {
    fun employee(init: Employee.() -> Unit) = Employee().apply { init() }

    fun Employee.addCard(init: Card.() -> Unit) = run {card = Card().apply { init() }}

    @Deprecated(level = DeprecationLevel.ERROR, message = "Employee is forbidden here.")
    fun Employee.employee(init: (@Scoped Employee).() -> Unit) { }

    @Deprecated(level = DeprecationLevel.ERROR, message = "Card is forbidden here.")
    fun Card.addCard(init: (@Scoped Card).() -> Unit) { }
}

fun main(args: Array<String>) {
    builder {
        val crafted = employee {
            //employee {}  <-- deprecated, causes compilation error
            id = 5
            name = "max"
            addCard {
                // addCard {} <-- deprecated too
                cardId = 7
            }
        }
        println(crafted.card?.cardId)
    }
}

Полная версия работает здесь: https://pl.kotl.in/ICLYZyetU

...