JavaRush /Blogue Java /Random-PT /Pausa para café #85. Três lições de Java que aprendi da m...

Pausa para café #85. Três lições de Java que aprendi da maneira mais difícil. Como usar princípios SOLID no código

Publicado no grupo Random-PT

Três lições de Java que aprendi da maneira mais difícil

Fonte: Medium Learning Java é difícil. Aprendi com meus erros. Agora você também pode aprender com meus erros e experiências amargas, que você não precisa necessariamente ter. Pausa para café #85.  Três lições de Java que aprendi da maneira mais difícil.  Como usar os princípios SOLID no código - 1

1. Lambdas podem causar problemas.

Lambdas geralmente excedem 4 linhas de código e são maiores que o esperado. Isso sobrecarrega a memória de trabalho. Você precisa alterar uma variável do lambda? Você não pode fazer isso. Por que? Se o lambda puder acessar variáveis ​​do site de chamada, poderão surgir problemas de threading. Portanto você não pode alterar variáveis ​​do lambda. Mas Happy path em lambda funciona bem. Após uma falha no tempo de execução, você receberá esta resposta:
at [CLASS].lambda$null$2([CLASS].java:85)
at [CLASS]$$Lambda$64/730559617.accept(Unknown Source)
É difícil seguir o rastreamento da pilha lambda. Os nomes são confusos e difíceis de rastrear e depurar. Mais lambdas = mais rastreamentos de pilha. Qual é a melhor maneira de depurar lambdas? Use resultados intermediários.
map(elem -> {
 int result = elem.getResult();
 return result;
});
Outra boa maneira é usar técnicas avançadas de depuração do IntelliJ. Use TAB para selecionar o código que deseja depurar e combiná-lo com os resultados intermediários. “Quando paramos em uma linha que contém um lambda, se pressionarmos F7 (entrar), o IntelliJ destacará o fragmento que precisa ser depurado. Podemos mudar o bloco para depuração usando Tab e, assim que decidirmos isso, pressionar F7 novamente.” Como acessar variáveis ​​do site de chamada do lambda? Você só pode acessar variáveis ​​finais ou realmente finais. Você precisa criar um wrapper em torno das variáveis ​​de localização da chamada. Usando AtomicType ou usando seu próprio tipo. Você pode alterar o wrapper de variável criado com lambda. Como resolver problemas de rastreamento de pilha? Use funções nomeadas. Dessa forma, você pode encontrar rapidamente o código responsável, verificar a lógica e resolver problemas. Use funções nomeadas para remover o rastreamento de pilha criptografado. O mesmo lambda é repetido? Coloque-o em uma função nomeada. Você terá um único ponto de referência. Cada lambda recebe uma função gerada, o que dificulta o controle.
lambda$yourNamedFunction
lambda$0
Funções nomeadas resolvem um problema diferente. Grandes lambdas. Funções nomeadas dividem lambdas grandes, criam pedaços menores de código e criam funções conectáveis.
.map(this::namedFunc1).filter(this::namedFilter1).map(this::namedFunc2)

2. Problemas com listas

Você precisa trabalhar com listas ( Lists ). Você precisa de um HashMap para os dados. Para funções você precisará de um TreeMap . A lista continua. E não há como evitar trabalhar com coleções. Como fazer uma lista? Que tipo de lista você precisa? Deve ser imutável ou mutável? Todas essas respostas afetam o futuro do seu código. Escolha a lista certa com antecedência para não se arrepender depois. Arrays::asList cria uma lista “ponta a ponta”. O que você não pode fazer com esta lista? Você não pode redimensioná-lo. Ele é imutável. O que você pode fazer aqui? Especifique elementos, classificação ou outras operações que não afetem o tamanho. Use Arrays::asList com cuidado porque seu tamanho é imutável, mas seu conteúdo não. new ArrayList() cria uma nova lista “mutável”. Quais operações a lista criada suporta? É isso, e este é um motivo para ter cuidado. Crie listas mutáveis ​​a partir de listas imutáveis ​​usando new ArrayList() . List::of cria uma coleção “imutável”. Seu tamanho e conteúdo permanecem inalterados sob certas condições. Se o conteúdo for dados primitivos, como int , a lista será imutável. Dê uma olhada no exemplo a seguir.
@Test
public void testListOfBuilders() {
  System.out.println("### TESTING listOF with mutable content ###");

  StringBuilder one = new StringBuilder();
  one.append("a");

  StringBuilder two = new StringBuilder();
  two.append("a");

  List<StringBuilder> asList = List.of(one, two);

  asList.get(0).append("123");

  System.out.println(asList.get(0).toString());
}
### TESTING listOF com conteúdo mutável ### a123
Você precisa criar objetos imutáveis ​​​​e inseri-los em List::of . No entanto, List::of não fornece nenhuma garantia de imutabilidade. List::of fornece imutabilidade, confiabilidade e legibilidade. Saiba quando usar estruturas mutáveis ​​e quando usar estruturas imutáveis. A lista de argumentos que não devem mudar deve estar na lista imutável. Uma lista mutável pode ser uma lista mutável. Entenda qual coleção você precisa para criar um código confiável.

3. As anotações atrasam você

Você usa anotações? Você os entende? Você sabe o que eles fazem? Se você acha que a anotação registrada é adequada para todos os métodos, você está errado. Usei Logged para registrar argumentos do método. Para minha surpresa, não funcionou.
@Transaction
@Method("GET")
@PathElement("time")
@PathElement("date")
@Autowired
@Secure("ROLE_ADMIN")
public void manage(@Qualifier('time')int time) {
...
}
O que há de errado com esse código? Há muito resumo de configuração aqui. Você encontrará isso muitas vezes. A configuração é misturada com código normal. Não é ruim por si só, mas chama a atenção. Anotações são necessárias para reduzir o código padrão. Você não precisa escrever lógica de log para cada endpoint. Não há necessidade de configurar transações, use @Transactional . As anotações reduzem o padrão extraindo código. Não há um vencedor claro aqui, já que ambos estão no jogo. Ainda uso XML e anotações. Ao encontrar um padrão repetido, é melhor mover a lógica para a anotação. Por exemplo, o registro em log é uma boa opção de anotação. Moral: não abuse das anotações e não se esqueça do XML.

Bônus: você pode ter problemas com Opcional

Você usará orElse de Opcional . Comportamento indesejável ocorre quando você não passa a constante orElse . Você deve estar ciente disso para evitar problemas no futuro. Vejamos alguns exemplos. Quando getValue(x) retorna um valor, getValue(y) é executado . O método em orElse é executado se getValue(x) retornar um valor Opcional não vazio .
getValue(x).orElse(getValue(y)
                  .orElseThrow(() -> new NotFoundException("value not present")));

public Optional<Value> getValue(Source s)
{
  System.out.println("Source: " + s.getName());

  // returns value from s source
}

// when getValue(x) is present system will output
Source: x
Source: y
Use orElseGet . Ele não executará código para Opcionais não vazios .
getValue(x).orElseGet(() -> getValue(y)
                  .orElseThrow(() -> new NotFoundException("value not present")));

public Optional<Value> getValue(Source s)
{
  System.out.println("Source: " + s.getName());

  // returns value from s source
}

// when getValue(x) is present system will output
Source: x

Conclusão

Aprender Java é difícil. Você não pode aprender Java em 24 horas. Aprimore suas habilidades. Reserve um tempo, aprenda e se destaque em seu trabalho.

Como usar princípios SOLID no código

Fonte: Cleanthecode Escrever código confiável requer princípios SOLID. Em algum momento todos tivemos que aprender a programar. E sejamos honestos. Nós éramos ESTÚPIDOS. E nosso código era o mesmo. Graças a Deus temos SÓLIDO. Pausa para café #85.  Três lições de Java que aprendi da maneira mais difícil.  Como usar os princípios SOLID no código - 2

Princípios SÓLIDOS

Então, como você escreve código SOLID? Na verdade é simples. Você só precisa seguir estas cinco regras:
  • Princípio de Responsabilidade Única
  • Princípio aberto-fechado
  • Princípio de substituição de Liskov
  • Princípio de separação de interface
  • Princípio de Inversão de Dependência
Não se preocupe! Esses princípios são muito mais simples do que parecem!

Princípio de Responsabilidade Única

Em seu livro, Robert C. Martin descreve esse princípio da seguinte maneira: “Uma classe deve ter apenas um motivo para mudar”. Vejamos dois exemplos juntos.

1. O que não fazer

Temos uma classe chamada User que permite ao usuário fazer o seguinte:
  • Registre uma conta
  • Conecte-se
  • Receba uma notificação na primeira vez que fizer login
Esta classe agora tem diversas responsabilidades. Se o processo de registro for alterado, a classe User será alterada. O mesmo acontecerá se o processo de login ou notificação for alterado. Isso significa que a classe está sobrecarregada. Ele tem muitas responsabilidades. A maneira mais fácil de corrigir isso é transferir a responsabilidade para suas classes, de modo que a classe User seja responsável apenas por combinar classes. Se o processo mudar, você terá uma classe clara e separada que precisa mudar.

2. O que fazer

Imagine uma classe que deveria mostrar uma notificação para um novo usuário, FirstUseNotification . Será composto por três funções:
  • Verifique se uma notificação já foi exibida
  • Mostrar notificação
  • Marcar notificação como já mostrada
Esta classe tem vários motivos para mudar? Não. Esta classe tem uma função clara: exibir uma notificação para um novo usuário. Isso significa que a classe tem um motivo para mudar. Ou seja, se esse objetivo mudar. Portanto, esta classe não viola o princípio da responsabilidade única. Claro, existem algumas coisas que podem mudar: a forma como as notificações são marcadas como lidas pode mudar ou a forma como a notificação aparece. No entanto, como o objetivo da aula é claro e básico, tudo bem.

Princípio aberto-fechado

O princípio aberto-fechado foi cunhado por Bertrand Meyer: “Os objetos de software (classes, módulos, funções, etc.) devem ser abertos para extensão, mas fechados para modificação”. Este princípio é realmente muito simples. Você deve escrever seu código para que novos recursos possam ser adicionados a ele sem alterar o código-fonte. Isso ajuda a evitar uma situação em que você precise alterar as classes que dependem da sua classe modificada. No entanto, este princípio é muito mais difícil de implementar. Meyer sugeriu usar herança. Mas isso leva a uma conexão forte. Discutiremos isso nos Princípios de Separação de Interface e nos Princípios de Inversão de Dependência. Então Martin criou uma abordagem melhor: usar polimorfismo. Em vez da herança convencional, esta abordagem utiliza classes base abstratas. Desta forma, as especificações de herança podem ser reutilizadas enquanto a implementação não é necessária. A interface pode ser escrita uma vez e depois fechada para fazer alterações. Novas funções devem então implementar esta interface e estendê-la.

Princípio de substituição de Liskov

Este princípio foi inventado por Barbara Liskov, vencedora do Prêmio Turing por suas contribuições às linguagens de programação e metodologia de software. Em seu artigo, ela definiu seu princípio da seguinte forma: “Os objetos em um programa devem ser substituíveis por instâncias de seus subtipos sem afetar a execução correta do programa”. Vejamos esse princípio como programador. Imagine que temos um quadrado. Poderia ser um retângulo, o que parece lógico, já que um quadrado é uma forma especial de retângulo. É aqui que o princípio da substituição de Liskov vem em socorro. Onde quer que você espere ver um retângulo em seu código, também é possível que um quadrado apareça. Agora imagine que seu retângulo possui os métodos SetWidth e SetHeight . Isso significa que o quadrado também precisa desses métodos. Infelizmente, isso não faz sentido. Isto significa que o princípio da substituição de Liskov é violado aqui.

Princípio de separação de interface

Como todos os princípios, o princípio da separação de interfaces é muito mais simples do que parece: “Muitas interfaces específicas do cliente são melhores do que uma interface de uso geral”. Tal como acontece com o princípio da responsabilidade única, o objetivo é reduzir os efeitos secundários e o número de alterações necessárias. Claro, ninguém escreve esse código de propósito. Mas é fácil de encontrar. Lembra do quadrado do princípio anterior? Agora imagine que decidimos implementar o nosso plano: produzimos um quadrado a partir de um retângulo. Agora estamos forçando o square a implementar setWidth e setHeight , o que provavelmente não faz nada. Se eles fizessem isso, provavelmente quebraríamos alguma coisa porque a largura e a altura não seriam as que esperávamos. Felizmente para nós, isto significa que já não estamos a violar o princípio da substituição de Liskov, uma vez que agora permitimos a utilização de um quadrado sempre que utilizarmos um rectângulo. No entanto, isto cria um novo problema: estamos agora a violar o princípio da separação de interfaces. Forçamos a classe derivada a implementar funcionalidades que ela optou por não usar.

Princípio de Inversão de Dependência

O último princípio é simples: os módulos de alto nível devem ser reutilizáveis ​​e não devem ser afetados por alterações nos módulos de baixo nível.
  • A. Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações (como interfaces).
  • B. As abstrações não devem depender de detalhes. Os detalhes (implementações concretas) devem depender das abstrações.
Isto pode ser conseguido implementando uma abstração que separa módulos de alto e baixo nível. O nome do princípio sugere que a direção da dependência muda, mas não é o caso. Ele apenas separa a dependência introduzindo uma abstração entre elas. Como resultado, você obterá duas dependências:
  • Módulo de alto nível, dependendo da abstração
  • Módulo de baixo nível dependendo da mesma abstração
Isto pode parecer difícil, mas na verdade acontece automaticamente se você aplicar corretamente o princípio aberto/fechado e o princípio de substituição de Liskov. Isso é tudo! Agora você aprendeu os cinco princípios básicos que sustentam o SOLID. Com esses cinco princípios, você pode tornar seu código incrível!
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION