«1. Обзор

Иногда мы можем задаться вопросом, можно ли добавить несколько дополнительных удобных методов в скомпилированные классы Java или Groovy, если у нас нет возможности изменять исходный код. Как оказалось, категория Groovy позволяет нам сделать именно это.

Groovy — это динамичный и мощный язык JVM с многочисленными функциями метапрограммирования.

В этом уроке мы рассмотрим концепцию категорий в Groovy.

2. Что такое категория?

Категории — это функция метапрограммирования, вдохновленная Objective-C, которая позволяет нам добавлять дополнительные функции в новый или существующий класс Java или Groovy.

В отличие от расширений, дополнительные функции, предоставляемые категорией, не включены по умолчанию. Таким образом, ключом к включению категории является использование блока кода.

Дополнительные функции, реализуемые категорией, доступны только внутри блока кода использования.

3. Категории в Groovy

Давайте обсудим несколько известных категорий, которые уже доступны в Groovy Development Kit.

3.1. TimeCategory

Класс TimeCategory доступен в пакете groovy.time, который добавляет несколько удобных способов работы с объектами даты и времени.

Эта категория добавляет возможность конвертировать целое число во временную запись, такую ​​как секунды, минуты, дни и месяцы.

Кроме того, класс TimeCategory предоставляет такие методы, как plus и minus, для простого добавления Duration к объектам Date и вычитания Duration из объектов Date соответственно.

Давайте рассмотрим несколько удобных функций, предоставляемых классом TimeCategory. Для этих примеров мы сначала создадим объект Date, а затем выполним несколько операций, используя TimeCategory:

def jan_1_2019 = new Date("01/01/2019")
use (TimeCategory) {
    assert jan_1_2019 + 10.seconds == new Date("01/01/2019 00:00:10")
    assert jan_1_2019 + 20.minutes == new Date("01/01/2019 00:20:00")
    assert jan_1_2019 - 1.day == new Date("12/31/2018")
    assert jan_1_2019 - 2.months == new Date("11/01/2018")
}

Давайте подробно обсудим код.

Здесь 10.seconds создает объект TimeDuration со значением 10 секунд. И оператор плюс (+) добавляет объект TimeDuration к объекту Date.

Аналогично, 1.day создает объект Duration со значением 1 день. И оператор минус (-) вычитает объект Duration из объекта Date.

Кроме того, несколько методов, таких как now, ago и from, доступны через класс TimeCategory, который позволяет создавать относительные даты.

Например, 5.days.from.now создаст объект Date со значением на 5 дней раньше текущей даты. Точно так же 2.hours.ago устанавливает значение на 2 часа раньше текущего времени.

Давайте посмотрим на них в действии. Также мы будем использовать SimpleDateFormat, чтобы игнорировать границы времени при сравнении двух похожих объектов Date:

SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy")
use (TimeCategory) {
    assert sdf.format(5.days.from.now) == sdf.format(new Date() + 5.days)

    sdf = new SimpleDateFormat("dd/MM/yyyy hh:mm:ss")
    assert sdf.format(10.minutes.from.now) == sdf.format(new Date() + 10.minutes)
    assert sdf.format(2.hours.ago) == sdf.format(new Date() - 2.hours)
}

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

3.2. DOMCategory

Класс DOMCategory доступен в пакете groovy.xml.dom. Он предлагает несколько удобных способов работы с DOM-объектом Java.

В частности, DOMCategory позволяет выполнять операции GPath с элементами DOM, упрощая обход и обработку XML-файлов.

Сначала напишем простой XML-текст и проанализируем его с помощью класса DOMBuilder:

def baeldungArticlesText = """
<articles>
    <article core-java="true">
        <title>An Intro to the Java Debug Interface (JDI)</title>
        <desc>A quick and practical overview of Java Debug Interface.</desc>
    </article>
    <article core-java="false">
        <title>A Quick Guide to Working with Web Services in Groovy</title>
        <desc>Learn how to work with Web Services in Groovy.</desc>
    </article>
</articles>
"""

def baeldungArticlesDom = DOMBuilder.newInstance().parseText(baeldungArticlesText)
def root = baeldungArticlesDom.documentElement

Здесь корневой объект содержит все дочерние узлы DOM. Давайте пройдемся по этим узлам, используя класс DOMCategory:

use (DOMCategory) {
    assert root.article.size() == 2

    def articles = root.article
    assert articles[0].title.text() == "An Intro to the Java Debug Interface (JDI)"
    assert articles[1].desc.text() == "Learn how to work with Web Services in Groovy."
}

Здесь класс DOMCategory обеспечивает легкий доступ к узлам и элементам с помощью точечных операций, предоставляемых GPath. Кроме того, он предоставляет такие методы, как размер и текст, для доступа к информации о любом узле или элементе.

Теперь давайте добавим новый узел к корневому объекту DOM, используя DOMCategory:

use (DOMCategory) {
    def articleNode3 = root.appendNode(new QName("article"), ["core-java": "false"])
    articleNode3.appendNode("title", "Metaprogramming in Groovy")
    articleNode3.appendNode("desc", "Explore the concept of metaprogramming in Groovy")

    assert root.article.size() == 3
    assert root.article[2].title.text() == "Metaprogramming in Groovy"
}

Точно так же класс DOMCategory также содержит несколько методов, таких как appendNode и setValue, для изменения DOM.

4. Создайте категорию

Теперь, когда мы увидели несколько категорий Groovy в действии, давайте рассмотрим, как создать пользовательскую категорию.

4.1. Использование Self Object

Класс категории должен следовать определенным правилам для реализации дополнительных функций.

Во-первых, метод добавления дополнительной функции должен быть статическим. Во-вторых, первым аргументом метода должен быть объект, к которому применима эта новая функция.

Давайте добавим функцию использования заглавных букв в класс String. Это просто изменит первую букву строки на прописную.

«Сначала мы напишем класс BaeldungCategory со статическим методом capitalize и типом String в качестве первого аргумента:

class BaeldungCategory {
    public static String capitalize(String self) {
        String capitalizedStr = self;
        if (self.size() > 0) {
            capitalizedStr = self.substring(0, 1).toUpperCase() + self.substring(1);
        }
        return capitalizedStr
    }
}

Затем давайте напишем быстрый тест, чтобы включить BaeldungCategory и проверить возможность использования заглавных букв в объекте String:

use (BaeldungCategory) {
    assert "norman".capitalize() == "Norman"
}

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

public static double toThePower(Number self, Number exponent) {
    return Math.pow(self, exponent);
}

Наконец, давайте проверим нашу пользовательскую категорию:

use (BaeldungCategory) {
    assert 50.toThePower(2) == 2500
    assert 2.4.toThePower(4) == 33.1776
}

4.2. Аннотация @Category

Мы также можем использовать аннотацию @groovy.lang.Category, чтобы объявить категорию как класс в стиле экземпляра. При использовании аннотации мы должны указать имя класса, к которому применима наша категория.

Экземпляр объекта доступен по этому ключевому слову в методе. Следовательно, объект self не обязательно должен быть первым аргументом.

Напишем класс NumberCategory и объявим его как категорию с аннотацией @Category. Кроме того, мы добавим в нашу новую категорию несколько дополнительных функций, таких как куб и DivideWithRoundUp:

@Category(Number)
class NumberCategory {
    public Number cube() {
        return this*this*this
    }
    
    public int divideWithRoundUp(BigDecimal divisor, boolean isRoundUp) {
        def mathRound = isRoundUp ? BigDecimal.ROUND_UP : BigDecimal.ROUND_DOWN
        return (int)new BigDecimal(this).divide(divisor, 0, mathRound)
    }
}

Здесь функцияdivideWithRoundUp делит число на делитель и округляет результат вверх/вниз до следующего или предыдущего целого числа на основе в параметре isRoundUp.

Давайте протестируем нашу новую категорию:

use (NumberCategory) {
    assert 3.cube() == 27
    assert 25.divideWithRoundUp(6, true) == 5
    assert 120.23.divideWithRoundUp(6.1, true) == 20
    assert 150.9.divideWithRoundUp(12.1, false) == 12
}

5. Заключение

В этой статье мы рассмотрели концепцию категорий в Groovy — функцию метапрограммирования, которая может включать дополнительные функции в Java и Groovy. классы.

Мы рассмотрели несколько категорий, таких как TimeCategory и DOMCategory, которые уже доступны в Groovy. В то же время мы изучили несколько дополнительных удобных способов работы с Date и DOM Java с использованием этих категорий.

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

Как обычно, все реализации кода доступны на GitHub.