Перейти к основному содержимому

5.12. Рекомендации по разработке на Groovy

Разработчику Архитектору

Рекомендации по разработке на Groovy

Введение в культуру кода Groovy

Groovy сочетает динамическую природу скриптовых языков с мощью платформы JVM. Культура кода в экосистеме Groovy строится на принципах читаемости, лаконичности и практической применимости. Язык предоставляет множество синтаксических упрощений по сравнению с Java, но это не означает отказ от дисциплины в проектировании. Качественный код на Groovy сохраняет структурированность, предсказуемость и удобство сопровождения при максимальном использовании выразительных возможностей языка.

Основные ценности разработки на Groovy:

  • Читаемость превыше краткости — лаконичность не должна снижать понимание логики
  • Использование идиом языка — применение замыканий, операторов расширения, безопасной навигации там, где это уместно
  • Согласованность в рамках проекта — единые правила оформления для всех участников команды
  • Прагматизм — выбор между динамической и статической типизацией на основе требований к надёжности и производительности
  • Интеграция с экосистемой JVM — естественное взаимодействие с библиотеками и фреймворками на Java

Соглашения об именовании

Общие правила именования

Имена в коде на Groovy следуют стандартным соглашениям экосистемы JVM с учётом специфики языка:

  • Классы и интерфейсы используют стиль PascalCase: UserService, PaymentProcessor
  • Методы и переменные используют стиль camelCase: calculateTotal, userList
  • Константы объявляются в стиле SCREAMING_SNAKE_CASE: MAX_RETRY_COUNT, DEFAULT_TIMEOUT
  • Пакеты именуются строчными буквами без подчёркиваний: com.example.payment
  • Файлы скриптов сохраняют имя класса с расширением .groovy

Именование методов

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

  • Используйте глагольные префиксы для действий: fetchUsers(), validateInput(), processOrder()
  • Для проверок применяйте префиксы is, has, can: isActive(), hasPermission(), canExecute()
  • Методы, возвращающие коллекции, именуйте во множественном числе: getActiveUsers(), findPendingOrders()
  • Для конвертаций используйте префиксы to и as: toString(), asList(), toDate()
  • Методы-предикаты возвращают логическое значение и имеют осмысленное имя: isEligibleForDiscount()

Groovy поддерживает методы с пробелами в имени через обратные кавычки, но такая практика применяется только в тестовых фреймворках (Spock):

def "should calculate total correctly when items present"() {
expect:
calculator.total([item1, item2]) == 42
}

Именование переменных

Переменные должны точно отражать своё назначение:

// Предпочтительно
def activeUsers = userRepository.findAllByStatus('ACTIVE')
def retryCount = 3
def maxConnectionPoolSize = 50

// Избегайте
def au = userRepository.findAllByStatus('ACTIVE')
def rc = 3
def m = 50

Для временных переменных в коротких замыканиях допустимы краткие имена:

def total = items.sum { it.price * it.quantity }
def names = users.collect { it.fullName }

Оформление кода

Отступы и пробелы

  • Используйте 4 пробела для отступов, не применяйте символ табуляции
  • После ключевых слов ставьте пробел: if (condition), while (active), for (item in list)
  • Окружайте бинарные операторы пробелами: a + b, count > 0, name == 'Groovy'
  • Не ставьте пробел после открывающей скобки и перед закрывающей: method(arg1, arg2)
  • Ставьте пробел после запятых в списках и вызовах методов: [1, 2, 3], process(item1, item2)

Фигурные скобки

Применяйте стиль K&R (Kernighan & Ritchie) для блоков кода:

if (user.active) {
process(user)
} else {
log.warn("Inactive user: ${user.name}")
}

class UserService {
def findAll() {
userRepository.list()
}
}

Для однострочных блоков в замыканиях допустимо опускать фигурные скобки:

users.each { println it.name }
def squares = (1..10).collect { it * it }

Переносы строк

При длинных выражениях применяйте вертикальное выравнивание:

def result = userRepository
.findByStatusAndRegion('ACTIVE', 'EUROPE')
.findAll { it.balance > MIN_BALANCE }
.sort { it.registrationDate }
.take(MAX_RESULTS)

Для цепочек методов каждый вызов размещайте на новой строке с отступом:

def users = User.createCriteria().list {
eq('status', 'ACTIVE')
ge('balance', 100)
order('lastName', 'asc')
order('firstName', 'asc')
maxResults(50)
}

Строковые литералы

Используйте подходящий тип строк в зависимости от задачи:

  • Одинарные кавычки для простых строк: 'простая строка'
  • Двойные кавычки для интерполяции: "Пользователь: ${user.name}"
  • Тройные кавычки для многострочных текстов:
    def template = """
    Здравствуйте, ${user.name}!

    Ваш заказ №${order.id} обработан.
    Сумма: ${order.total} руб.
    """
  • Обратные кавычки для многострочных строк без интерполяции или с минимальной обработкой

Структура проекта

Типичная структура проекта на Groovy соответствует стандартам Gradle/Maven:

project-root/
├── src/
│ ├── main/
│ │ ├── groovy/ # Основной код на Groovy
│ │ │ └── com/
│ │ │ └── example/
│ │ │ ├── domain/ # Сущности предметной области
│ │ │ ├── service/ # Сервисный слой
│ │ │ ├── repository/ # Работа с данными
│ │ │ └── util/ # Вспомогательные утилиты
│ │ ├── resources/ # Конфигурации, шаблоны, статические ресурсы
│ │ └── java/ # Интеграционный код на Java (при необходимости)
│ └── test/
│ ├── groovy/ # Тесты на Groovy
│ │ └── com/
│ │ └── example/
│ │ ├── service/
│ │ └── specification/ # Спецификации Spock
│ └── resources/ # Тестовые конфигурации и данные
├── build.gradle # Сборка Gradle
├── settings.gradle
└── gradle.properties # Свойства проекта

Для скриптовых проектов допустима упрощённая структура:

scripts/
├── utils/
│ ├── FileHelper.groovy
│ └── StringUtils.groovy
├── tasks/
│ ├── backup.groovy
│ └── migrate.groovy
└── config/
└── application.groovy

Проектирование классов и объектов

Принципы проектирования

Классы в Groovy должны соответствовать принципам SOLID с учётом динамической природы языка:

  • Принцип единственной ответственности: класс решает одну задачу
  • Открытость для расширения через метапрограммирование и категории
  • Предпочитайте композицию наследованию
  • Инкапсулируйте внутреннее состояние, предоставляя контролируемые точки доступа

Объявление классов

Используйте ключевое слово class для объявления обычных классов:

class User {
String firstName
String lastName
String email
Date registrationDate = new Date()

String getFullName() {
"$firstName $lastName"
}

boolean isActive() {
registrationDate > Date.parse('yyyy-MM-dd', '2020-01-01')
}
}

Для неизменяемых объектов применяйте @Immutable:

@Immutable
class Address {
String street
String city
String postalCode
String country
}

Для упрощённого объявления данных используйте @Canonical:

@Canonical
class Product {
String name
BigDecimal price
String category
}

Свойства и поля

Groovy автоматически генерирует геттеры и сеттеры для полей. Явное объявление геттеров/сеттеров требуется только для специальной логики:

class Account {
BigDecimal balance = 0

void deposit(BigDecimal amount) {
if (amount <= 0) throw new IllegalArgumentException("Сумма должна быть положительной")
balance += amount
}

boolean withdraw(BigDecimal amount) {
if (amount > balance) return false
balance -= amount
true
}
}

Для приватных полей используйте модификатор private:

class SecureService {
private String apiKey

SecureService(String key) {
this.apiKey = key?.trim() ?: throw new IllegalArgumentException("Ключ не может быть пустым")
}

String getMaskedKey() {
apiKey ? "${apiKey[0..3]}****${apiKey[-4..-1]}" : null
}
}

Интерфейсы и абстрактные классы

Интерфейсы объявляются стандартным образом:

interface PaymentGateway {
PaymentResult process(PaymentRequest request)
boolean supportsCurrency(String currency)
}

Абстрактные классы позволяют реализовать общую логику:

abstract class AbstractRepository<T> {
abstract List<T> findAll()
abstract T findById(Long id)

List<T> findAllById(Collection<Long> ids) {
ids.collect { findById(it) }.findAll()
}
}

Работа с коллекциями и функциональными конструкциями

Идиоматичная работа с коллекциями

Groovy предоставляет богатый набор методов для работы с коллекциями. Предпочитайте функциональные операции императивным циклам:

// Фильтрация
def activeUsers = users.findAll { it.status == 'ACTIVE' }

// Преобразование
def userEmails = users.collect { it.email }

// Агрегация
def totalBalance = accounts.sum { it.balance ?: 0 }

// Поиск
def admin = users.find { it.role == 'ADMIN' }
def hasPremium = users.any { it.plan == 'PREMIUM' }
def allVerified = users.every { it.emailVerified }

// Группировка
def usersByCountry = users.groupBy { it.country }

Замыкания

Замыкания в Groovy — основной инструмент для выразительного кода. Используйте краткий синтаксис:

// Полная форма
def square = { number -> number * number }

// Краткая форма с неявным параметром it
def isEven = { it % 2 == 0 }

// Многострочное замыкание
def process = { item ->
log.debug("Обработка: ${item.id}")
validator.validate(item)
repository.save(item)
notifier.send(item)
}

Для замыканий с несколькими параметрами указывайте их явно:

def comparator = { a, b -> a.priority <=> b.priority ?: a.name <=> b.name }

Операторы расширения

Операторы расширения позволяют добавлять методы к существующим классам без наследования:

// В файле StringExtensions.groovy
String.metaClass.toSlug = {
delegate.toLowerCase()
.replaceAll('[^a-z0-9]+', '-')
.replaceAll('(^-|-$)', '')
}

// Использование
assert 'Hello World!'.toSlug() == 'hello-world'

Для продакшн-кода предпочтительнее использовать категории или миксины для лучшего контроля области видимости.

Обработка ошибок и исключений

Стратегии обработки исключений

Обрабатывайте исключения осмысленно:

try {
paymentService.process(paymentRequest)
notificationService.sendConfirmation(user)
} catch (PaymentDeclinedException e) {
log.warn("Платёж отклонён для пользователя ${user.id}: ${e.message}")
userService.markPaymentFailed(user, e.reason)
} catch (ServiceUnavailableException e) {
log.error("Сбой платёжного шлюза", e)
retryService.scheduleRetry(paymentRequest, e)
throw new PaymentProcessingException("Временная недоступность сервиса", e)
} finally {
auditService.logAttempt(user, paymentRequest, System.currentTimeMillis() - startTime)
}

Безопасная навигация

Используйте оператор безопасной навигации ?. для работы с возможными null-значениями:

def city = user?.address?.city ?: 'Не указан'
def firstOrderDate = user?.orders?.min { it.date }?.date

Для цепочек вызовов с потенциальными null-значениями применяйте Elvis-оператор ?::

def displayName = user?.fullName ?: user?.email ?: 'Анонимный пользователь'

Валидация входных данных

Проверяйте входные параметры в публичных методах:

class OrderService {
Order createOrder(OrderRequest request) {
assert request != null, 'Запрос не может быть null'
assert request.items && !request.items.isEmpty(), 'Список товаров не может быть пустым'
assert request.customerId, 'Идентификатор клиента обязателен'

// Основная логика создания заказа
// ...
}
}

Для сложных сценариев валидации используйте специализированные библиотеки (например, Hibernate Validator).

Комментирование кода

Типы комментариев

Применяйте три типа комментариев в зависимости от назначения:

  • Однострочные комментарии // для кратких пояснений в коде
  • Многострочные комментарии /* */ для временного отключения блоков кода
  • Groovydoc-комментарии /** */ для документирования публичного API

Документирование классов и методов

Документируйте все публичные классы, методы и поля с помощью Groovydoc:

/**
* Сервис обработки платежей через внешние платёжные шлюзы.
* Поддерживает несколько провайдеров с автоматическим выбором
* на основе валюты и суммы платежа.
*/
class PaymentService {

/**
* Обрабатывает платёж через подходящий платёжный шлюз.
*
* @param request объект запроса с данными платежа
* @return результат обработки с идентификатором транзакции
* @throws PaymentValidationException при некорректных данных запроса
* @throws PaymentProcessingException при ошибках обработки платежа
*/
PaymentResult process(PaymentRequest request) {
// Реализация
}
}

Комментарии в коде

Размещайте комментарии на отдельной строке перед объясняемым кодом:

// Рассчитываем комиссию с учётом прогрессивной шкалы
def commission = calculateProgressiveCommission(amount)

// Применяем сезонную скидку только для премиум-пользователей
if (user.plan == 'PREMIUM' && isHolidaySeason()) {
applySeasonalDiscount(order)
}

Избегайте комментариев, дублирующих код:

// Плохо: комментарий повторяет очевидное
// Увеличиваем счётчик на единицу
counter++

// Хорошо: комментарий объясняет причину
// Счётчик увеличивается здесь, а не в основном цикле,
// чтобы избежать двойного учёта при повторной обработке
counter++

Тестирование кода на Groovy

Структура тестов

Используйте фреймворк Spock для написания выразительных спецификаций:

class UserServiceSpec extends Specification {

UserService userService
UserRepository userRepository = Mock()

def setup() {
userService = new UserService(userRepository: userRepository)
}

def "should return active users only"() {
given:
userRepository.findAll() >> [
new User(status: 'ACTIVE', name: 'Alice'),
new User(status: 'INACTIVE', name: 'Bob'),
new User(status: 'ACTIVE', name: 'Charlie')
]

when:
def result = userService.getActiveUsers()

then:
result.size() == 2
result*.name == ['Alice', 'Charlie']
}

def "should throw exception when user not found"() {
given:
userRepository.findById(123) >> null

when:
userService.getUser(123)

then:
thrown(UserNotFoundException)
}
}

Практики тестирования

  • Тестируйте одно поведение в одном методе спецификации
  • Используйте осмысленные имена методов тестов на естественном языке
  • Применяйте разделы given, when, then для структурирования тестов
  • Используйте моки и заглушки для изоляции тестируемого компонента
  • Покрывайте граничные случаи и обработку ошибок

Интеграция с экосистемой JVM

Взаимодействие с кодом на Java

Groovy полностью совместим с Java. Вызывайте Java-код из Groovy без дополнительных преобразований:

// Использование Java-классов
import java.time.LocalDate
import java.util.stream.Collectors

def today = LocalDate.now()
def names = users.stream()
.filter { it.active }
.map { it.name }
.collect(Collectors.toList())

Для обратного вызова Groovy-кода из Java применяйте интерфейсы:

// Java-код
public interface DataProcessor {
void process(Map<String, Object> data);
}

// Groovy-реализация
class GroovyProcessor implements DataProcessor {
void process(Map<String, Object> data) {
// Обработка данных
}
}

Использование библиотек

Подключайте зависимости через Gradle:

dependencies {
implementation 'org.codehaus.groovy:groovy-all:3.0.19'
implementation 'io.micronaut:micronaut-inject-groovy:3.9.4'
implementation 'org.slf4j:slf4j-api:2.0.9'

testImplementation 'org.spockframework:spock-core:2.3-groovy-3.0'
testImplementation 'org.spockframework:spock-junit4:2.3-groovy-3.0'
}

Статическая компиляция

Для критичных к производительности участков кода применяйте статическую компиляцию:

import groovy.transform.CompileStatic

@CompileStatic
class PerformanceCriticalService {
int calculate(int a, int b) {
a * b + (a - b)
}
}

Статическая компиляция обеспечивает производительность, близкую к Java, с сохранением синтаксических преимуществ Groovy.

Практические примеры архитектурных решений

Слой доступа к данным

@CompileStatic
class UserRepository {
private final Sql sql

UserRepository(DataSource dataSource) {
this.sql = new Sql(dataSource)
}

List<User> findAllActive() {
sql.rows('SELECT * FROM users WHERE status = ?', ['ACTIVE'])
.collect { rowToUser(it) }
}

User findById(Long id) {
def row = sql.firstRow('SELECT * FROM users WHERE id = ?', [id])
row ? rowToUser(row) : null
}

private User rowToUser(Map row) {
new User(
id: row.id as Long,
email: row.email as String,
name: row.name as String,
status: row.status as String,
createdAt: row.created_at as Timestamp
)
}
}

Сервисный слой с транзакционной поддержкой

@CompileStatic
class OrderService {

@Autowired
OrderRepository orderRepository

@Autowired
PaymentService paymentService

@Transactional
Order createOrder(OrderRequest request) {
validateRequest(request)

def order = new Order(
userId: request.userId,
items: request.items,
status: 'PENDING',
createdAt: new Date()
)

order = orderRepository.save(order)
processPayment(order, request.paymentDetails)

order.status = 'CONFIRMED'
orderRepository.save(order)

notificationService.sendOrderConfirmation(order)
order
}

private void validateRequest(OrderRequest request) {
assert request.userId, 'Идентификатор пользователя обязателен'
assert request.items && !request.items.isEmpty(), 'Список товаров не может быть пустым'
assert request.paymentDetails, 'Данные платежа обязательны'
}

private void processPayment(Order order, PaymentDetails details) {
try {
paymentService.charge(details, order.totalAmount)
} catch (PaymentException e) {
order.status = 'PAYMENT_FAILED'
orderRepository.save(order)
throw new OrderProcessingException('Ошибка обработки платежа', e)
}
}
}

Конфигурация приложения

// config/application.groovy
dataSource {
pooled = true
driverClassName = 'org.postgresql.Driver'
username = System.getenv('DB_USER') ?: 'app_user'
password = System.getenv('DB_PASSWORD') ?: 'secret'
url = System.getenv('DB_URL') ?: 'jdbc:postgresql://localhost:5432/app_db'
}

environments {
development {
dataSource {
url = 'jdbc:postgresql://localhost:5432/app_dev'
}
}
production {
dataSource {
properties {
maxActive = 50
maxIdle = 25
minIdle = 5
initialSize = 5
maxWait = 10000
}
}
}
}