Во всех наших проектах мы используем классы данных, которые по определению содержат данные (поля), но не содержат (бизнес) логику.
В соответствии с лучшими практиками кодирования, класс данных предпочтительно должен быть неизменным, потому что неизменность означает безопасность потока. Основная ссылка здесь — книга Джошуа Блоха «Эффективная Java» ; этот пост Егора Бугаенко тоже очень интересно читать.
Неизменяемый класс имеет несколько интересных свойств:
- он не должен быть подклассифицированным (то есть он должен быть конечным или иметь статический метод фабрики и закрытый конструктор)
- все поля должны быть закрытыми (для предотвращения прямого доступа)
- все поля должны быть записаны один раз (во время создания экземпляра) (т.е. они должны быть окончательными и без установщиков)
- все изменяемые поля типа (например, java.util.Date) должны быть защищены, чтобы предотвратить доступ клиента к записи по ссылке
Примером неизменяемого класса является следующий:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
public final class ImmutableBean { private final String aStr; private final int anInt; public ImmutableBean(String aStr, int anInt) { this .aStr = aStr; this .anInt = anInt; } public String getAStr() { return aStr; } public int getAnInt() { return anInt; } } |
Примечание: как это часто встречается в Java, существует много стандартного кода, который скрывает определения неизменяемости.
Такие библиотеки, как Project Lombok, облегчают нашу жизнь, потому что мы можем использовать аннотацию @Value, чтобы легко определить неизменяемый класс следующим образом:
1
2
3
4
5
|
@Value public class LombokImmutableBean { String aStr; int anInt; } |
что намного более читабельно.
Должны ли мы (юнит) тестировать класс, чтобы проверить его неизменность?
В идеальном мире ответ — нет.
С помощью наших предпочтительных функций автоматической генерации кода в среде IDE или с такими библиотеками, как Lombok, нетрудно добавить неизменность в класс.
Но в реальном мире могут возникнуть человеческие ошибки, когда мы создаем класс или когда мы (или можем быть младшим членом команды) изменяем класс позже. Что произойдет, если новое поле будет добавлено без финала и с помощью генератора кода IDE будет создан сеттер? Класс больше не является неизменным.
Важно гарантировать, что класс остается неизменным на протяжении всего срока службы проекта.
А с помощью детектора изменчивости мы можем легко создать тест для проверки статуса неизменяемости класса.
Как обычно, зависимости Maven / Gradle можно найти в Maven Central .
Чтобы протестировать наш ImmutableBean, мы можем создать следующий тестовый класс jUnit:
1
2
3
4
5
6
7
8
9
|
import static org.mutabilitydetector.unittesting.MutabilityAssert.assertImmutable; public class ImmutableBeanTest { @Test public void testClassIsImmutable() { assertImmutable(ImmutableBean. class ); } } |
тест не пройден, если класс не является неизменным.
Например, если поле не является окончательным и имеет метод установки, тест не пройден, и сообщение об ошибке очень описательно:
1
2
3
4
5
6
7
8
|
org.mutabilitydetector.unittesting.MutabilityAssertionError: Expected: it.gualtierotesta.testsolutions.general.beans.ImmutableBean to be IMMUTABLE but: it.gualtierotesta.testsolutions.general.beans.ImmutableBean is actually NOT_IMMUTABLE Reasons: Field is not final , if shared across threads the Java Memory Model will not guarantee it is initialised before it is read. [Field: aStr, Class: it.gualtierotesta.testsolutions.general.beans.ImmutableBean] Field [aStr] can be reassigned within method [setaStr] [Field: aStr, Class: it.gualtierotesta.testsolutions.general.beans.ImmutableBean] |
Полный проект можно найти в моем проекте галереи Test Solutions на GitHub. Смотрите общий модуль.
Подход, который я предлагаю, состоит в том, чтобы использовать Lombok без какого-либо теста на неизменность. Если Lombok не может использоваться (например, в устаревшем проекте), используйте детектор Mutable, чтобы утверждать, что класс действительно неизменяем.