Будьте внимательны с var
В lombok уже достаточно давно, а так же в самом языке Java начиная с 10 версии существует возможность объявления переменных через var
. Это позволяет не указывать тип явно, а позволить компилятору определить его автоматически. Так же, в lombok, Kotlin и Scala (но не в самом Java) есть его “финализированный” собрат val
.
Это удобно, и это может сделать код лаконичнее и чище, но тем не менее я считаю что использование var
и val
требует дополнительной осторожности, и использую их далеко не всегда. В этой заметке я хочу показать на конкретном примере почему.
История
У пользователей системы, над которой я тогда работал, был список ролей по которым мы авторизовывали их действия. Роли выдавались в контексте мероприятий, таким образом была реализована возможность проверять есть ли у пользователя нужная роль в конкретном мероприятии.
Для удобства все роли со всех мероприятий конвертировались в массив строк в формате EVENT:${ID мероприятия}:${код роли}
, и уже по этому списку выполнялись различные проверки.
Ошибка с которой я столкнулся заключалась в том что список ролей пользователя составлялся неправильно, ID в этих строках (это были UUID) почему-то не совпадали с ID мероприятий. Взглянув на код, я нашел там следующую часть:
1
2
3
4
5
6
7
8
9
10
val events = eventRepository.findAllByAccount(account);
for (val event : events) {
roles.add("EVENT:" + event.getId());
val eventRoles = event.getRoles();
if (eventRoles != null) {
for (val role : eventRoles) {
roles.add("EVENT:" + event.getId() + ":" + role.getName());
}
}
}
На первый взгляд все выглядит корректно. Что же здесь может быть не так?
Чтобы далеко не ходить, я сразу приведу исправленный вариант этой же части кода:
1
2
3
4
5
6
7
8
9
10
val eventMembers = eventRepository.findAllByAccount(account);
for (val eventMember : eventMembers) {
roles.add("EVENT:" + eventMember.getEvent().getId());
val eventRoles = eventMember.getRoles();
if (eventRoles != null) {
for (val role : eventRoles) {
roles.add("EVENT:" + eventMember.getEvent().getId() + ":" + role.getName());
}
}
}
Ошибка заключалась в том что метод findAllByAccount
возвращал не список мероприятий (Event
), а список участников мероприятия (EventMember
). Но это не было замечено из-за использования val
. Далее, т.к. у EventMember
был ID такого же формата, то код получился совершенно валидным, собрался, запустился и даже был выпущен.
Если бы разработчик в данном случае указал бы тип явно
1
EventMember event = eventRepository.findAllByAccount(account);
то скорее всего он бы сразу заметил что метод возвращает EventMember
а не Event
и исправился, но использование val
сделало эту ошибку незаметной.
Таким образом, ошибка здесь произошла из-за того что совпало несколько факторов:
- Плохой нейминг метода сервиса (такой метод следовало назвать иначе иначе, или поместить в отдельный
eventMemberRepository
) - Использование
val
, которое замаскировало ошибку Отсутствие тестов
Заключение
Безусловно это не повод полностью отказываться от var
и val
в своем коде, но это хороший пример того, почему при их использовании нужно быть внимательнее, чем при явном указании типов, и как их использование может косвенно привести к настоящей ошибке.
Такая ошибка может оказаться очень незаметной, и есть довольно большой шанс того что она будет пропущена как при написании кода, так и на ревью.
Существующие рекомендации говорят о том что при использовании var
в названии переменной желательно отражать информацию о ее типе. Но даже это не помогло в описываемом случае, т.к. название содержало тип (но он был неправильным).
Поэтому, в качестве заключения хочется добавить к рекомендациям следующее:
Рекомендация: При использовании
var
илиval
внимательно следите за тем, что на самом деле возвращают методы, результат которых вы присваиваете переменным.