В предыдущем посте Маскировка: Скрытие реализации API конструктора мы разделили три основные задачи нашей библиотеки DSL — определение, данные и процесс экспорта. В этой части мы помогаем разработчикам, использующим наш DSL, извлекать часть определений DSL в методы.

Использование DSL имеет свои преимущества, но также и некоторые недостатки. Один из них заключается в том, что компоновщики де-факто являются данными в виде кода, поэтому, например, для представления большой диаграммы нам потребуется написать много кода. В таких ситуациях очень полезно, если вы можете сохранить код СУХИМ и не повторяться.

Одним из популярных рефакторингов для удаления дубликатов является извлечение методов. Внутри вложенных сборщиков это может быть проблемой, так как нам нужен доступ к настраиваемому объекту. Решение на самом деле довольно простое, мы можем явно передать объект определения во вложенное замыкание:

@CompileStatic
private static Diagram buildDiagramDiagramUsingHelperMethods() {
    Diagram.build { DiagramDefinition diagram ->
        note 'YUML Diagram Components'

        buildDiagramRelationships(diagram)
        buildRelationshipRelationship(diagram)
    }
}

@CompileStatic
private static DiagramContentDefinition buildDiagramRelationships(DiagramDefinition diagram) {
    diagram.with {
        type 'Diagram' has one to many type 'Type'
        type 'Diagram' has zero to many type 'Note'
        type 'Diagram' has zero to many type 'Relationship'
    }
}

@CompileStatic
private static DiagramContentDefinition buildRelationshipRelationship(DiagramDefinition diagram) {
    diagram.with {
        type 'Relationship' has one type 'Type' called 'source'
        type 'Relationship' has one type 'Type' called 'destination'
        type 'Relationship' owns one type 'RelationshipType'
    }
}

В этом тривиальном примере вы можете видеть, что объект DiagramDefinition передается в качестве первого необязательного аргумента закрытия построителя, поэтому мы можем использовать его в отдельном методе. И мы можем использовать старый добрый метод with, чтобы продолжить использовать DSL внутри самого метода.

Нам нужно сделать небольшое обновление в интерфейсах, чтобы сообщить компилятору Groovy, каковы ожидаемые параметры закрытия. Вот пример метода type в DiagramDefinition:

TypeDefinition type(
    String name,
    @DelegatesTo(
        value = TypeDefinition.class, 
        strategy = Closure.DELEGATE_FIRST
    )
    @ClosureParams(
        value = SimpleType.class, 
        options = "cz.orany.yuml.model.dsl.TypeDefinition"
    )
    Closure<? extends DiagramContentDefinition> builder
);

ClosureParams аннотация помогает статическому компилятору определить типы параметров замыкания. Хотя это бесполезно в нашей ситуации, вы также можете определить несколько ожидаемых параметров. Поскольку мы по-прежнему используем метод with внутри, нам не нужны дальнейшие изменения в нашем коде, так как этот метод уже вызывает замыкание с объектом self в качестве единственного параметра.

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

type 'Order', {
    has one
}

В примере has one вернет InheritanceBuilder, который не реализует интерфейс DiagramContentDefinition, и во время статической компиляции возникнет ошибка компиляции.

Как я уже писал, эта защита мягкая, так как вы все еще можете написать следующий фрагмент кода:

type 'Order', {
    has one
    has many type 'Line'
}

Но, тем не менее, это дает разработчикам лучший намек на ожидаемое содержимое замыкания, чем просто оставить параметр типа замыкания неопределенным.

Код доступен на GitHub под тегом 05-dry:

git clone https://github.com/musketyr/yuml-dsl-builder.git
cd yuml-dsl-builder
git checkout 05-dry

В следующей части Ожидания: Важность правильной работы с владельцем замыканий мы рассмотрим одну из наиболее распространенных проблем билдера DSL — неверные ожидания относительно захваченной области замыканий.

Содержание

  1. Концепция: Основная концепция строителей
  2. Суть: Основы закрытия
  3. Помощь: Использование аннотаций для статической компиляции
  4. Маскировка: Скрытие реализации API конструктора
  5. Иссушение: Сохранение кода СУХИМ
  6. Ожидания: Важность правильного обращения с владельцем замыканий
  7. Расширение: Проектирование билдера DSL для расширения
  8. Отставка: Переписывание Groovy DSL Builder на Java
  9. Навигация: Использование аннотаций для именованных параметров
  10. Заключение: Контрольный список для разработчиков Groovy DSL