Будьте внимательны с var

Небольшая история о том как использование var или val может скрыть ошибку

В 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 сделало эту ошибку незаметной.

Таким образом, ошибка здесь произошла из-за того что совпало несколько факторов:

  1. Плохой нейминг метода сервиса (такой метод следовало назвать иначе иначе, или поместить в отдельный eventMemberRepository)
  2. Использование val, которое замаскировало ошибку
  3. Отсутствие тестов

Заключение

Безусловно это не повод полностью отказываться от var и val в своем коде, но это хороший пример того, почему при их использовании нужно быть внимательнее, чем при явном указании типов, и как их использование может косвенно привести к настоящей ошибке.

Такая ошибка может оказаться очень незаметной, и есть довольно большой шанс того что она будет пропущена как при написании кода, так и на ревью.

Существующие рекомендации говорят о том что при использовании var в названии переменной желательно отражать информацию о ее типе. Но даже это не помогло в описываемом случае, т.к. название содержало тип (но он был неправильным).

Поэтому, в качестве заключения хочется добавить к рекомендациям следующее:

Рекомендация: При использовании var или val внимательно следите за тем, что на самом деле возвращают методы, результат которых вы присваиваете переменным.

Ссылки