DSL на Java: пишем предметно-ориентированные языки с нуля

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Ваш код говорит на языке бизнеса? Пора создавать DSL на Java

Представьте: вы пишете не скучные if-else и for, а код, который читается как инструкция для коллеги. Не верите? Зря. Предметно-ориентированные языки (DSL) на Java — это не магия, а прагматичный инструмент, который превращает сложную логику в элегантные конструкции.

В этой статье мы разберем, как создать DSL на Java, который сделает ваш код чище, а работу с ним — понятнее для бизнес-аналитиков. Вы узнаете, чем отличается внутренний DSL от внешнего, как построить fluent API и когда стоит написать свой парсер. Поехали!

Что такое DSL и зачем он Java-разработчику?

DSL (Domain-Specific Language) — это язык, созданный для решения задач в конкретной предметной области. В отличие от общего языка программирования (Java, Python), DSL узко специализирован. Примеры из жизни: SQL для баз данных, регулярные выражения для работы со строками, HTML для веб-верстки.

В Java DSL используется, чтобы:

  • Повысить читаемость кода — бизнес-логика становится декларативной;
  • Снизить порог входа — аналитики могут править правила без глубоких знаний Java;
  • Уменьшить количество ошибок — ограниченный синтаксис не даёт сделать глупость;
  • Ускорить разработку — вместо тысяч строк кода — десяток понятных команд.

Важно: создание DSL на Java не требует написания компилятора. Чаще всего мы используем внутренние DSL — надстройки над стандартным синтаксисом Java.

Внутренний DSL vs Внешний DSL: что выбрать?

Внутренние DSL (Embedded DSL)

Это DSL, который живёт внутри Java. Вы используете методы, цепочки вызовов, лямбды и перегрузку операторов (через методы). Пример: Java Stream API — это внутренний DSL для обработки коллекций.

List<String> result = orders.stream()
    .filter(o -> o.getAmount() > 1000)
    .map(Order::getCustomerName)
    .collect(Collectors.toList());

Плюсы: не нужно писать парсер, тесно интегрирован с Java, легко дебажить. Минусы: синтаксис ограничен возможностями Java.

Внешние DSL (External DSL)

Это отдельный язык со своим синтаксисом. Вы пишете парсер (например, с помощью ANTLR или JavaCC) и интерпретатор. Пример: SQL — внешний DSL, который вы отправляете базе данных.

Плюсы: полная свобода синтаксиса, можно сделать очень красиво. Минусы: нужно писать и поддерживать парсер, сложнее дебажить.

Для большинства задач хватает внутреннего DSL. Внешние оправданы, когда язык будет использовать много людей (например, как SQL) или когда синтаксис принципиально важен.

Практика: создаём внутренний DSL на Java для системы мониторинга

Допустим, мы строим систему мониторинга серверов. Вместо того чтобы писать:

AlertRule rule = new AlertRule();
rule.setMetric("cpu_usage");
rule.setOperator(GREATER_THAN);
rule.setThreshold(90.0);
rule.setDuration(5, TimeUnit.MINUTES);
rule.setAction(new SendEmailAction("admin@example.com"));

Мы хотим писать так:

AlertRule rule = AlertRuleBuilder
    .whenMetric("cpu_usage")
    .isGreaterThan(90.0)
    .forDuration(5, TimeUnit.MINUTES)
    .thenSendEmail("admin@example.com")
    .build();

Красиво, правда? Давайте реализуем этот fluent API.

Шаг 1: Создаём класс AlertRule

public class AlertRule {
    private String metric;
    private String operator;
    private double threshold;
    private long duration;
    private TimeUnit timeUnit;
    private String actionType;
    private String actionTarget;

    // Геттеры и сеттеры опущены для краткости
}

Шаг 2: Строим билдер с fluent интерфейсом

public class AlertRuleBuilder {
    private AlertRule rule = new AlertRule();

    public static MetricStep whenMetric(String metric) {
        AlertRuleBuilder builder = new AlertRuleBuilder();
        builder.rule.setMetric(metric);
        return new MetricStep(builder);
    }

    public static class MetricStep {
        private final AlertRuleBuilder builder;
        MetricStep(AlertRuleBuilder builder) { this.builder = builder; }

        public ThresholdStep isGreaterThan(double threshold) {
            builder.rule.setOperator("GT");
            builder.rule.setThreshold(threshold);
            return new ThresholdStep(builder);
        }

        public ThresholdStep isLessThan(double threshold) {
            builder.rule.setOperator("LT");
            builder.rule.setThreshold(threshold);
            return new ThresholdStep(builder);
        }
    }

    public static class ThresholdStep {
        private final AlertRuleBuilder builder;
        ThresholdStep(AlertRuleBuilder builder) { this.builder = builder; }

        public DurationStep forDuration(long amount, TimeUnit unit) {
            builder.rule.setDuration(amount);
            builder.rule.setTimeUnit(unit);
            return new DurationStep(builder);
        }
    }

    public static class DurationStep {
        private final AlertRuleBuilder builder;
        DurationStep(AlertRuleBuilder builder) { this.builder = builder; }

        public BuildStep thenSendEmail(String email) {
            builder.rule.setActionType("EMAIL");
            builder.rule.setActionTarget(email);
            return new BuildStep(builder);
        }

        public BuildStep thenCallWebhook(String url) {
            builder.rule.setActionType("WEBHOOK");
            builder.rule.setActionTarget(url);
            return new BuildStep(builder);
        }
    }

    public static class BuildStep {
        private final AlertRuleBuilder builder;
        BuildStep(AlertRuleBuilder builder) { this.builder = builder; }

        public AlertRule build() {
            // Валидация
            if (builder.rule.getMetric() == null) throw new IllegalStateException("Metric is required");
            return builder.rule;
        }
    }
}

Теперь наш DSL на Java работает. Компилятор проверяет порядок вызовов: вы не сможете забыть указать метрику или пропустить действие. Это и есть суть предметно-ориентированного языка — безопасность и выразительность.

Продвинутые техники: лямбды и функциональный DSL

Java 8+ с лямбдами открывает новые горизонты для создания DSL. Рассмотрим пример — DSL для построения отчётов:

Report report = ReportBuilder.create("Sales Report")
    .withFilter(r -> r.field("date").isAfter(LocalDate.now().minusDays(30)))
    .withGroupBy("category")
    .withAggregation(Aggregation.SUM, "revenue")
    .build();

Реализуем фильтр через лямбду:

@FunctionalInterface
public interface FilterExpression {
    boolean evaluate(Record record);

    default FilterExpression and(FilterExpression other) {
        return record -> this.evaluate(record) && other.evaluate(record);
    }

    static FieldFilter field(String name) {
        return new FieldFilter(name);
    }
}

public class FieldFilter {
    private final String fieldName;
    FieldFilter(String name) { this.fieldName = name; }

    public FilterExpression isAfter(LocalDate date) {
        return record -> {
            LocalDate value = (LocalDate) record.getField(fieldName);
            return value != null                

Блоги

Рекомендаций по книгам