Minha captura em uma semana

Quanto mais um desenvolvedor trabalha em um aplicativo em uma equipe e melhor conhece seu código, mais frequentemente ele se dedica a revisar o trabalho de seus companheiros. Hoje vou mostrar o que pode ser capturado em uma semana no código escrito por desenvolvedores muito bons. Sob o corte, há uma coleção de artefatos vívidos de nossa criatividade (e um pouco de meus pensamentos).


Comparadores


Existe um código:


@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; } 

Alguém observa que você pode classificar o fluxo diretamente, mas quero chamar sua atenção para o comparador. A documentação do método Comparator::compare diz em inglês em branco:


Compara seus dois argumentos para ordem. Retorna um número inteiro negativo, zero ou um número inteiro positivo, pois o primeiro argumento é menor que, igual a ou maior que o segundo.

É esse comportamento que é implementado em nosso código. O que está errado? O fato é que os criadores de Java sugeriram muito claramente que muitos precisariam de um comparador e o fizeram para nós. Podemos usá-lo simplificando nosso código:


 List<Long> readSortedIds(List<Dto> list) { List<Long> ids = list.stream().map(Dto::getId).collect(Collectors.toList()); ids.sort(Comparator.naturalOrder()); return ids; } 

Da mesma forma, esse código


 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; } 

com um movimento do pulso se transforma em


 List<Dto> sortDtosById(List<Dto> list) { list.sort(Comparator.comparing(Dto::getId)); return list; } 

A propósito, na nova versão de “Ideas” será possível fazer o seguinte:


Feitiçaria


Abuso opcional


Provavelmente, cada um de nós viu algo parecido com isto:


 List<UserEntity> getUsersForGroup(Long groupId) { Optional<Long> optional = Optional.ofNullable(groupId); if (optional.isPresent()) { return userRepository.findUsersByGroup(optional.get()); } return Collections.emptyList(); } 

Freqüentemente, Optional usado para verificar a presença / ausência de um valor, embora não tenha sido criado para isso. Amarre o abuso e escreva mais facilmente:


 List<UserEntity> getUsersForGroup(Long groupId) { if (groupId == null) { return Collections.emptyList(); } return userRepository.findUsersByGroup(groupId); } 

Lembre-se de que Optional não é sobre o argumento ou campo do método, mas sobre o valor de retorno. É por isso que foi projetado sem suporte à serialização.


métodos nulos que alteram o estado de um argumento


Imagine um método como este:


 @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); } } 

Certamente você já viu e escreveu algo semelhante muitas vezes. Aqui não gosto que o método altere o estado da entidade, mas não o retorne. Como se comportam métodos-quadro semelhantes? Por exemplo, org.springframework.data.jpa.repository.JpaRepository::save e javax.persistence.EntityManager::merge retornam um valor. Suponha que, após atualizar um contrato, precisamos tirá-lo do método de update . Acontece algo como isto:


 @Transactional public void anotherMethod(Long contractId, Dto contractDto) { updateService.updateContractById(contractId, contractDto); Contract contract = repositoroty.findOne(contractId); doSmth(contract); } 

Sim, podemos passar a entidade diretamente para o método UpdateService::updateContract alterando sua assinatura, mas é melhor fazer isso da seguinte maneira:


 @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); } 

Por um lado, isso ajuda a simplificar o código, por outro lado, ajuda nos testes. Em geral, testar métodos void é uma tarefa extremamente sombria, que mostrarei usando o mesmo exemplo:


 @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()); } } 

Mas tudo pode ser simplificado se o método retornar um valor:


Certifique-se de
 @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()); } } 

De uma só vez, não apenas a chamada para ContractRepository::save verificada, mas também a correção do valor salvo.


Construção de bicicletas


Por diversão, abra seu projeto e procure pelo seguinte:


 lastIndexOf('.') 

Com uma alta probabilidade, toda a expressão se parece com isso:


 String fileExtension = fileName.subString(fileName.lastIndexOf('.')); 

É sobre isso que nenhum analisador estático pode alertar, é sobre a bicicleta recém-inventada. Senhores, se você está resolvendo um determinado problema relacionado ao nome / extensão do arquivo ou ao caminho para ele, assim como a leitura / gravação / cópia, em 9 dos 10 casos a tarefa já foi resolvida antes de você. Portanto, amarre a construção da bicicleta e adote soluções prontas (e comprovadas):


 import org.apache.commons.io.FilenameUtils; //... String fileExtension = FilenameUtils.getExtension(fileName); 

Nesse caso, você economiza tempo gasto na verificação da adequação da bicicleta e também obtém funcionalidades mais avançadas (consulte a documentação FilenameUtils::getExtension ).


Ou aqui está um código que copia o conteúdo de um arquivo para outro:


 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); } 

Que circunstâncias podem nos impedir? Milhares deles:


  • o destino pode ser uma pasta, não um arquivo
  • a fonte pode ser uma pasta
  • origem e destino podem ser o mesmo arquivo
  • destino não pode ser criado
  • etc. e assim por diante.

O triste é que usando a auto-gravação sobre tudo o que aprendemos já durante a cópia.
Se fizermos isso sabiamente


 import org.apache.commons.io.FileUtils; try { FileUtils.copyFile(src, new File(targetName)); } catch (IOException ex) { log.error("", ex); } 

parte das verificações será realizada antes do início da cópia e uma possível exceção será mais informativa (consulte as fontes de FileUtils::copyFile ).


Negligenciando @ Nullable / @ NotNull


Suponha que tenhamos uma entidade:


 @Entity @Getter public class UserEntity { @Id private Long id; @Column private String email; @Column private String petName; } 

No nosso caso, a coluna de email na tabela é descrita como not null , diferente de petName. Ou seja, podemos marcar os campos com as anotações correspondentes:


 import javax.annotation.Nullable; import javax.annotation.NotNull; //... @Column @NotNull private String email; @Column @Nullable private String petName; 

À primeira vista, isso parece uma dica para o desenvolvedor, e realmente é. Ao mesmo tempo, essas anotações são uma ferramenta muito mais poderosa do que um rótulo comum.


Por exemplo, os ambientes de desenvolvimento os entendem e, se depois de adicionar anotações, tentamos fazer o seguinte:


 boolean checkIfPetBelongsToUser(UserEnity user, String lostPetName) { return user.getPetName().equals(lostPetName); } 

então a "Idéia" nos avisará do perigo com esta mensagem:


A chamada de método 'equals' pode produzir 'NullPointerException'

No código


 boolean hasEmail(UserEnity user) { boolean hasEmail = user.getEmail() == null; return hasEmail; } 

haverá outro aviso:


A condição 'user.getEmail () == null' é sempre 'false'

Isso ajuda o censor interno a encontrar possíveis erros e nos ajuda a entender melhor a execução do código. Para o mesmo objetivo, as anotações são úteis para colocar em métodos que retornam um valor e seus argumentos.


Se meus argumentos parecem inconclusivos, observe o código-fonte de qualquer projeto sério, o mesmo "Spring" - eles estão marcados com anotações como uma árvore de Natal. E isso não é um capricho, mas uma necessidade grave.


A única desvantagem, me parece, é a necessidade de manter constantemente anotações em um estado moderno. Embora, se você olhar para ele, é uma benção, porque, voltando ao código repetidamente, entendemos cada vez melhor.


Descuido


Não há erros neste código, mas há um excesso:


 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()); 

Não está claro por que criar uma nova lista de chaves pelas quais a pesquisa é realizada, se ela não muda ao passar pelo fluxo. É bom que existam apenas 5 elementos e, se houver 100500 deles? E se o método getDtos retornar 100500 objetos (na lista!), Que tipo de desempenho esse código terá? Nenhum, então é melhor assim:


 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()); 

Também aqui:


 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))); } 

Obviamente, o valor retornado pela expressão inParameterMap.keySet() permanece inalterado, portanto pode ser movido para uma variável:


 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))); } 

A propósito, essas seções podem ser calculadas usando a verificação 'Alocação de objetos em um loop'.


Quando a análise estática é impotente


Oitava Java há muito tempo desapareceu, mas todos nós amamos fluxos. Alguns de nós os amam tanto que os usamos em qualquer lugar:


 public Optional<EmailAdresses> getUserEmails() { Stream<UserEntity> users = getUsers().stream(); if (users.count() == 0) { return Optional.empty(); } return users.findAny(); } 

O fluxo, como você sabe, é novo antes da rescisão ser chamada, portanto, acessar novamente a variável de users em nosso código resultará em uma IllegalStateException .


Os analisadores estáticos ainda não sabem como reportar esses erros; portanto, a responsabilidade pela captura oportuna recai sobre o revisor.


Parece-me que usar variáveis ​​do tipo Stream , além de transmiti-las como argumentos e retornar de métodos, é como caminhar em um campo minado. Talvez com sorte, talvez não. Portanto, uma regra simples: qualquer aparência do Stream<T> no código deve ser verificada (mas de uma maneira boa imediatamente cortada).


Tipos simples


Muitos têm certeza de que boolean , int etc. são apenas sobre desempenho. Isso é parcialmente verdade, mas, além disso, o tipo simples not null é not null por padrão. Se um campo inteiro de uma entidade se referir a uma coluna declarada na tabela como not null , faz sentido usar int vez de Integer . Esse é um tipo de combinação - e o consumo de memória é menor e o código é simplificado devido a verificações desnecessárias de null .


Isso é tudo. Lembre-se de que tudo o que foi exposto acima não é a verdade suprema, pense com sua própria cabeça e aborde significativamente a aplicação de qualquer conselho.

Source: https://habr.com/ru/post/pt426105/


All Articles