Groovy имеет аннотацию Immutable которая позволяет создавать неизменяемые классы, что является необходимым условием для создания объектов-значений . К сожалению, когда класс был аннотирован с помощью Immutable , больше невозможно добавить свой собственный конструктор, чтобы проверить, не являются ли предоставленные параметры null , что сделало бы наши объекты значений действительно пуленепробиваемыми.
|
1
2
3
4
5
6
7
|
@groovy.transform.Immutableclass 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.Immutableclass 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
|
@Immutableclass 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) // okdef 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
|
@Immutableclass 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.NonNullclass Person { String name}new Person() // throws IllegalArgumentException: "Name can not be null" |
Как это работает?
В текущей версии @NonNull можно применять только на уровне класса.
Соответствующее преобразование AST изменяет класс и
- добавляет метод «проверки» для каждого свойства, проверяющего значение на ненулевое .
- добавляет метод «uber checker», вызывающий каждую из указанных выше отдельных проверок
- изменяет каждый конструктор и добавляет вызов вышеупомянутого метода Uber Checker в качестве последнего оператора
Итак, что-то простое, как
|
1
2
3
4
|
@NonNullclass Person { String name} |
заканчивается в скомпилированной версии (по поведению) примерно как
|
01
02
03
04
05
06
07
08
09
10
|
@NonNullclass 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.ValueObjectclass Money { BigDecimal amount}new Money(amount: null)// throws IllegalArgumentException because of NonNulldef 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 и дайте мне знать, что можно улучшить или предложения в противном случае.
- Если у вас есть проблема, используйте систему отслеживания проблем.
| Ссылка: | Сделайте ваши Groovy объекты более пуленепробиваемыми от нашего партнера JCG Теда Винке в блоге |