Статьи

Сделайте ваши Groovy объекты более пуленепробиваемыми

Groovy имеет аннотацию Immutable которая позволяет создавать неизменяемые классы, что является необходимым условием для создания объектов-значений . К сожалению, когда класс был аннотирован с помощью Immutable , больше невозможно добавить свой собственный конструктор, чтобы проверить, не являются ли предоставленные параметры null , что сделало бы наши объекты значений действительно пуленепробиваемыми.

1
2
3
4
5
6
7
@groovy.transform.Immutable
class Money {
    BigDecimal amount
}
 
def money = new Money() // we can just instantiate without an amount!
assert money.amount == null

Конечно, Money здесь представляют собой типичный пример объекта стоимости — неизменный после создания, сопоставимый по стоимости, например, по количеству. В некоторых моих базах кода я часто использую объекты-значения, и это раздражало меня тем, что не было никакого способа предотвратить, в данном случае, значение null — присвоив ему значение null или оставив его по умолчанию.

Явные конструкторы

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@groovy.transform.Immutable
class Money {
    BigDecimal amount
 
    Money(BigDecimal amount) {
        if (amount == null) {
            throw new IllegalArgumentException("Amount can not be null")
        }
        this.amount = amount
    }
}
 
def money = new Money()
// normally would throw IllegalArgumentException: "Amount can not be null"

Обычно иметь собственный конструктор было бы хорошо, и в приведенном выше примере генерировалось бы IllegalArgumentException , но, к сожалению, аннотация Immutable предотвращает это.

1
2
3
4
5
6
7
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
Script1.groovy: 6: Explicit constructors not allowed for @Immutable class: Money
 @ line 6, column 17.
                   Money(BigDecimal amount) {
                   ^
 
1 error

Заводской метод

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
@Immutable
class Money {
    BigDecimal amount
 
    static Money of(BigDecimal amount) {
        if (amount == null) {
            throw new IllegalArgumentException("Amount can not be null")
        }
        new Money(amount)
    }
}
def money1 = Money.of(3) // ok
def money2 = Money.of() // good, fails with IllegalArgumentException

Но все равно можно обойти мой метод static of factory и создать недопустимый Money напрямую с помощью конструктора по умолчанию.

1
def money3 = new Money() // still works, argh!

Так что вы можете сделать?

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

1
2
3
4
5
6
7
8
9
@Immutable
class Money {
    BigDecimal amount
 
    private Money() {
        throw new IllegalArgumentException("Use of() method instead")
    }
 
    static Money of(...

но … @Immutable позволяет вам @Immutable добавить свой собственный конструктор — или переопределить конструктор Map по умолчанию — помните?

Единственный вариант, о котором я мог подумать, помимо создания собственной пользовательской версии существующего преобразования @Immutable , — это сделать что-то после того, как он уже изменил класс.

И это можно сделать с …

АСТ преобразования

Bulletproof помогает заполнить этот пробел, добавив несколько преобразований AST.

  • Аннотация NonNull которая модифицирует каждый конструктор для выполнения нулевых проверок. Добавьте это к Immutable классу, и Immutable не пропустит ваш конструктор.
  • Мета-аннотация ValueObject которая помещает как NonNull и NonNull в ваш класс для удобства выполнения вышеупомянутого шага с одной аннотацией.

Более подробную информацию можно найти на GitHub , но в основном вы просто добавляете зависимость и используете одну из аннотаций.

ненулевая

Аннотация NonNull на уровне класса запускает преобразование AST, которое модифицирует каждый конструктор для выполнения нулевой проверки.

1
2
3
4
5
6
@Immutable
@tvinke.bulletproof.transform.NonNull
class Person {
    String name
}
new Person() // throws IllegalArgumentException: "Name can not be null"

Как это работает?

В текущей версии @NonNull можно применять только на уровне класса.

Соответствующее преобразование AST изменяет класс и

  1. добавляет метод «проверки» для каждого свойства, проверяющего значение на ненулевое .
  2. добавляет метод «uber checker», вызывающий каждую из указанных выше отдельных проверок
  3. изменяет каждый конструктор и добавляет вызов вышеупомянутого метода Uber Checker в качестве последнего оператора

Итак, что-то простое, как

1
2
3
4
@NonNull
class Person {
    String name
}

заканчивается в скомпилированной версии (по поведению) примерно как

01
02
03
04
05
06
07
08
09
10
@NonNull
class Person {
    String name
    Person(String name) {
        this.name = name
        if (this.name == null) {
            throw new IllegalArgumentException("Name can not be null")
        }
    }
}

Модификация любого существующего конструктора (каким бы он ни был!) И выполнение проверок в качестве последних утверждений, сейчас кажется наиболее разумной вещью: итерировать любой конструктор, сначала сделать так, чтобы он делал свою собственную логику, и, наконец, в качестве постусловия. убедитесь, что значения не заканчиваются как null .

Объект значения

ValueObject объединяет аннотации NonNull и NonNull вместе.

01
02
03
04
05
06
07
08
09
10
11
@tvinke.bulletproof.transform.ValueObject
class Money {
    BigDecimal amount
}
 
new Money(amount: null)
// throws IllegalArgumentException because of NonNull
 
def money = new Money(2.95)
money.amount = 3.0
// throws regular ReadOnlyPropertyException because of Immutable

К тому времени, включив эту библиотеку в свой проект, я заменил все свои аннотации @ValueObject на @ValueObject и покончил с этим!

Объекты значения полностью!

У меня были некоторые проблемы с выяснением, на какой стадии компиляции должны были выполняться мои преобразования AST, чтобы убедиться, что я буду видеть конструкторов, уже добавленных ранее с помощью Immutable аннотации. Некоторые вопросы и ответы на канале Groovy Slack мне очень помогли, в том числе получить первоначальный отзыв о коде.

Если вы хотите проверить это:

  • Зайдите на https://github.com/tvinke/bulletproof и дайте мне знать, что можно улучшить или предложения в противном случае.
  • Если у вас есть проблема, используйте систему отслеживания проблем.