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 изменяет класс и
- добавляет метод «проверки» для каждого свойства, проверяющего значение на ненулевое .
- добавляет метод «uber checker», вызывающий каждую из указанных выше отдельных проверок
- изменяет каждый конструктор и добавляет вызов вышеупомянутого метода 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 и дайте мне знать, что можно улучшить или предложения в противном случае.
- Если у вас есть проблема, используйте систему отслеживания проблем.
Ссылка: | Сделайте ваши Groovy объекты более пуленепробиваемыми от нашего партнера JCG Теда Винке в блоге |