Чем больше разработчик работает над приложением в команде и чем лучше знает его код, тем чаще он занимается вычиткой творчества своих товарищей. Сегодня я покажу, что может быть выловлено за одну неделю в коде, написанном весьма неплохими разработчиками. Под катом собрание ярких артефактов нашего творчества (и немного моих размышлений).
Компараторы
Есть код:
@Getter
class Dto {
private Long id;
}
// another class
List<Long> readSortedIds(List<Dto> list) {
List<Long> ids = list.stream().map(Dto::getId).collect(Collectors.toList());
ids.sort(new Comparator<Long>() {
public int compare(Long o1, Long o2) {
if (o1 < o2) return -1;
if (o1 > o2) return 1;
return 0;
}
});
return ids;
}
Кто-то отметит, что сортировать можно напрямую стрим, я же хочу обратить ваше внимание на компаратор. В документации к методу Comparator::compare
английским по белому написано:
Compares its two arguments for order. Returns a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second.
Именно это поведение и реализовано в нашем коде. Что же не так? Дело в том, что создатели явы весьма дальновидно предположили, что подобный компаратор понадобится многим и сделали его за нас. Нам же остаётся им воспользоваться упростив наш код:
List<Long> readSortedIds(List<Dto> list) {
List<Long> ids = list.stream().map(Dto::getId).collect(Collectors.toList());
ids.sort(Comparator.naturalOrder());
return ids;
}
Точно так же этот код
List<Dto> sortDtosById(List<Dto> list) {
list.sort(new Comparator<Dto>() {
public int compare(Dto o1, Dto o2) {
if (o1.getId() < o2.getId()) return -1;
if (o1.getId() > o2.getId()) return 1;
return 0;
}
});
return list;
}
лёгким движением руки превращается в такой
List<Dto> sortDtosById(List<Dto> list) {
list.sort(Comparator.comparing(Dto::getId));
return list;
}
Кстати, в новой версии "Идеи" можно будет делать вот так:
Злоупотребление Optional-ами
Наверное, каждый из нас видел что-то вроде этого:
List<UserEntity> getUsersForGroup(Long groupId) {
Optional<Long> optional = Optional.ofNullable(groupId);
if (optional.isPresent()) {
return userRepository.findUsersByGroup(optional.get());
}
return Collections.emptyList();
}
Часто Optional
используют для проверки наличия/отсутствия значения, хотя создан он был не для этого. Завязывайте со злоупотреблением и пишите проще:
List<UserEntity> getUsersForGroup(Long groupId) {
if (groupId == null) {
return Collections.emptyList();
}
return userRepository.findUsersByGroup(groupId);
}
Помните, что Optional
— это не про аргумент метода или поле, а про возвращаемое значение. Именно поэтому он спроектирован без поддержки сериализации.
void-методы, меняющие состояние аргумента
Представьте такой метод:
@Component
@RequiredArgsConstructor
public class ContractUpdater {
private final ContractRepository repository;
@Transactional
public void updateContractById(Long contractId, Dto contractDto) {
Contract contract = repository.findOne(contractId);
contract.setValue(contractDto.getValue());
repository.save(contract);
}
}
Наверняка вы много раз видели и писали что-то подобное. Здесь мне не нравится, что метод меняет состояние сущности, но не возвращает её. Как ведут себя подобный методы фреймворков? Например, org.springframework.data.jpa.repository.JpaRepository::save
и javax.persistence.EntityManager::merge
возвращают значение. Предположим, после обновления контракта нам нужно получить его за пределами метода update
. Получается что-то вроде этого:
@Transactional
public void anotherMethod(Long contractId, Dto contractDto) {
updateService.updateContractById(contractId, contractDto);
Contract contract = repositoroty.findOne(contractId);
doSmth(contract);
}
Да, мы могли бы передавать сущность напрямую в метод UpdateService::updateContract
, изменив его сигнатуру, но лучше сделать вот так:
@Component
@RequiredArgsConstructor
public class ContractUpdater {
private final ContractRepository repository;
@Transactional
public Contract updateContractById(Long contractId, Dto contractDto) {
Contract contract = repository.findOne(contractId);
contract.setValue(contractDto.getValue());
return repository.save(contract);
}
}
//использование
@Transactional
public void anotherMethod(Long contractId, Dto contractDto) {
Contract contract = updateService.updateContractById(contractId, contractDto);
doSmth(contract);
}
С одной стороны, это помогает упростить код, с другой — помогает при тестировании. Вообще тестирование void
методов на редкость муторная задача, что я покажу, используя этот же пример:
@RunWith(MockitoJUnitRunner.class)
public class ContractUpdaterTest {
@Mock
private ContractRepository repository;
@InjectMocks
private ContractUpdater updater;
@Test
public void updateContractById() {
Dto dto = new Dto();
dto.setValue("что-то там");
Long contractId = 1L;
when(repository.findOne(contractId)).thenReturn(new Contract());
updater.updateContractById(contractId, contractDto); //void
//как проверить, что в контракт вписали значение из dto? - Примерно так:
ArgumentCaptor<Contract> captor = ArgumentCaptor.forClass(Contract.class);
verify(repository).save(captor.capture());
Contract updated = captor.getValue();
assertEquals(dto.getValue(), updated.getValue());
}
}
А ведь всё можно сделать проще, если метод возвращает значение:
@RunWith(MockitoJUnitRunner.class)
public class ContractUpdaterTest {
@Mock
private ContractRepository repository;
@InjectMocks
private ContractUpdater updater;
@Test
public void updateContractById() {
Dto dto = new Dto();
dto.setValue("что-то там");
Long contractId = 1L;
when(repository.findOne(contractId)).thenReturn(new Contract());
Contract updated = updater.updateContractById(contractId, contractDto);
assertEquals(dto.getValue(), updated.getValue());
}
}
Одним махом проверяется не только вызов ContractRepository::save
, но и правильность сохранённого значения.
Велосипедостроение
Прикола ради, откройте свой проект и поищите вот это:
lastIndexOf('.')
С большой вероятностью всё выражение выглядит примерно так:
String fileExtension = fileName.subString(fileName.lastIndexOf('.'));
Вот о чём ни один статический анализатор не может предупредить, так это о вновь изобретённом велосипеде. Господа, если вы решаете некую задачу, связанную с именем/расширением файла или путём к нему, ровно как и с чтением/записью/копирование, то в 9 случаях из 10 задача уже решена до вас. Поэтому завязывайте с велосипедостроением и берите готовые (и проверенные) решения:
import org.apache.commons.io.FilenameUtils;
//...
String fileExtension = FilenameUtils.getExtension(fileName);
В этом случае вы сберегает время, которое было бы потрачено на проверку годности велосипеда, а также получаете более продвинутый функционал (см. документацию FilenameUtils::getExtension
).
Или вот такой код, копирующий содержимое одного файла в другой:
try {
FileChannel sc = new FileInputStream(src).getChannel();
FileChannel dc = new FileOutputStream(new File(targetName)).getChannel();
sc.transferTo(0, sc.size(), dc);
dc.close();
sc.close();
} catch (IOException ex) {
log.error("ой", ex);
}
Какие обстоятельства могут нам помешать? Тысячи их:
- точка назначения может оказаться папкой, а вовсе не файлом
- источник может оказаться папкой
- источник и точка назначения могут оказаться одним и тем же файлом
- точку назначения невозможно создать
- и т. д. и т. п.
Печаль в том, что пользуясь самописью об этом всём мы узнаем уже в ходе копирования.
Если же сделаем по уму
import org.apache.commons.io.FileUtils;
try {
FileUtils.copyFile(src, new File(targetName));
} catch (IOException ex) {
log.error("ой", ex);
}
то часть проверок будет выполнена ещё до начала копирования, а возможное исключение будет более информативным (см. исходники FileUtils::copyFile
).
Пренебрежение @Nullable/@NotNull
Предположим, у нас есть сущность:
@Entity
@Getter
public class UserEntity {
@Id
private Long id;
@Column
private String email;
@Column
private String petName;
}
В нашем случае колонка email
в таблице описана как not null
, в отличие от petName. Т. е. мы можем пометить поля соответствующими аннотациями:
import javax.annotation.Nullable;
import javax.annotation.NotNull;
//...
@Column
@NotNull
private String email;
@Column
@Nullable
private String petName;
На первый взгляд, это выглядит как подсказка разработчику, и это действительно так. Вместе с тем эти аннотации — куда более могущественное средство, чем обычная метка.
Например, их понимают среды разработки, и если после добавления аннотаций мы попробуем сделать вот так:
boolean checkIfPetBelongsToUser(UserEnity user, String lostPetName) {
return user.getPetName().equals(lostPetName);
}
то "Идея" предупредит нас об опасности таким сообщением:
Method invocation 'equals' may produce 'NullPointerException'
В коде
boolean hasEmail(UserEnity user) {
boolean hasEmail = user.getEmail() == null;
return hasEmail;
}
будет другое предупреждение:
Condition 'user.getEmail() == null' is always 'false'
Это помогает встроенному цензору находить возможные ошибки и помогает нам лучше понимать исполнение кода. С той же целью аннотации полезно расставлять над методами, возвращающими значение, и их аргументами.
Если мои доводы кажутся неубедительными, то смотрите исходники любого серьёзного проекта, того же "Спринга" — они увешаны аннотациями как новогодняя ёлка. И это не блажь, а суровая необходимость.
Единственный недостаток, как мне кажется, — необходимость постоянно поддерживать аннотации в современном состоянии. Хотя, если разобраться, — это скорее благо, ведь возвращаясь раз за разом к коду мы понимаем его всё лучше.
Невнимательность
В этом коде нет ошибок, но есть излишество:
Collection<Dto> dtos = getDtos();
Stream.of(1,2,3,4,5)
.filter(id -> {
List<Integer> ids = dtos.stream().map(Dto::getId).collect(toList());
return ids.contains(id);
})
.collect(toList());
Непонятно, зачем создавать новый список ключей, по которому выполняется поиск, если он не меняется при проходе по стриму. Хорошо, что элементов всего 5, а если их будет 100500? А если метод getDtos
вернёт 100500 объектов (в списке!), то какая производительность будет у этого кода? Никакая, поэтому лучше вот так:
Collection<Dto> dtos = getDtos();
Set<Integer> ids = dtos.stream().map(Dto::getId).collect(toSet());
Stream.of(1,2,3,4,5)
.filter(ids::contains)
.collect(toList());
Также и здесь:
static <T, Q extends Query> void setParams(
Map<String, Collection<T>> paramMap,
Set<String> notReplacedParams,
Q query) {
notReplacedParams.stream()
.filter(param -> paramMap.keySet().contains(param))
.forEach(param -> query.setParameter(param, paramMap.get(param)));
}
Очевидно, что значение, возвращаемое выражение inParameterMap.keySet()
неизменно, поэтому его можно вынести в переменную:
static <T, Q extends Query> void setParams(
Map<String, Collection<T>> paramMap,
Set<String> notReplacedParams,
Q query) {
Set<String> params = paramMap.keySet();
notReplacedParams.stream()
.filter(params::contains)
.forEach(param -> query.setParameter(param, paramMap.get(param)));
}
Кстати, такие участки можно вычислить используя проверку 'Object allocation in a loop'.
Когда статический анализ бессилен
Восьмая ява давно отгремела, но все мы любим стримы. Некоторые из нас любят их так сильно, что используют повсюду:
public Optional<EmailAdresses> getUserEmails() {
Stream<UserEntity> users = getUsers().stream();
if (users.count() == 0) {
return Optional.empty();
}
return users.findAny();
}
Стрим, как известно, свеж до вызова на нём завершения, поэтому повторное обращение к переменной users
в нашем коде приведёт к IllegalStateException
.
Статические анализаторы пока не умеют сообщать о таких ошибках, поэтому ответственность за их своевременное вылавливание ложится на ревьюера.
Мне кажется, что использование переменных типа Stream
, равно как их передача в качестве аргументов и возврат из методов, похоже на хождение по минному полю. Может повезти, а может и нет. Отсюда простое правило: любое появление Stream<T>
в коде должно проверяться (а по-хорошему сразу же выпиливаться).
Простые типы
Многие уверены, что boolean
, int
и т. п. — это только про производительность. Отчасти это так, но кроме того простой тип not null
по умолчанию. Если целочисленное поле сущности ссылается на колонку, которая объявлена в таблице как not null
, то имеет смысл использовать int
, а не Integer
. Это своего рода комбо — и потребление памяти ниже, и код упрощается ввиду ненужности проверок на null
.
На этом всё. Помните, что всё вышеперечисленное не является истиной в последней инстанции, думайте своей головой и осмысленно подходите к применению любых советов.