Conheça sua linguagem: Armadilhas em Java
Publicado em
Um post mais técnico. ‘Conheça sua linguagem’ é uma nova categoria em meu blog, sobre linguagens de programação e seus problemas mais comuns em ambientes corporativos
De acordo com Tiobe Java é a linguagem de programação de backend mais usada do mundo. De acordo com StackOverflow, este título vai para Javascript, mas Java fica em terceiro lugar. Java e JVM também são quase onipresentes no celular, com o Android usando-o como plataforma.
Java é adotada por grandes empresas e há muitas vagas de emprego na área. Além disso, você sempre pode encontrar alguma ferramenta ou dependência que resolva aquele problema específico que você está enfrentando, dada sua enorme história e fama.
Também é extremamente prolixo, altamente verboso, baixo nível (em comparação com as linguagens mais recentes), desajeitado em vários aspectos e consomidor de recursos computacionais.
Eu sei que, no final, tudo se resume a “existem códigos ruins em todas as linguagens”, mas acredito que existem detalhes de design de cada linguagem que colocam os desenvolvedores em uma posição fácil de cair em algumas armadilhas, especialmente considerando que normalmente tendemos seguir as “boas práticas” dos gurus.
Nos mantendo em código e design, vamos ver quais são as armadilhas mais comuns que vi em projetos Java.
Build complexo
Bem simples. Você faz o seu clone do repositório, acessa o diretório e roda o comando de construção
./gradlew build
E então o build falha.
Normalmente, isso significa uma dependência de ambiente ou hosts adicionados manualmente para executar a compilação ou os testes. A menos que sua construção realmente seja executada em um único comando, você está fazendo errado.
O Gradle é especialmente infrator neste caso, devido à facilidade de criar tarefas personalizadas. Evite tarefas e abordagens excessivamente românticas, como geração de código.
Design Patterns jogados
A ideia por trás de um Design Pattern é um tanto simples: você tem um problema recorrente e um Design Pattern é uma maneira conhecida de resolver esse problema. Simples, o problema leva à solução. As dores surgem quando você gosta de uma solução e tende a aplicá-la em qualquer lugar que achar adequado (o antigo “quando tudo o que você tem é um martelo, tudo parece um prego”).
Exemplo simples, o padrão de estratégia. Em palavras legais, a estratégia é uma maneira que permite a seleção de algoritmos em tempo de execução. Em termos comuns, é usado para ocultar uma instrução “if”.
Uma interface com cálculo de preço:
interface BillingStrategy {
double getActPrice(final double rawPrice);
}
Primeira implementação:
class NormalStrategy implements BillingStrategy {
@Override
public double getActPrice(final double rawPrice) {
return rawPrice;
}
}
Segunda implementação:
class HappyHourStrategy implements BillingStrategy {
@Override
public double getActPrice(final double rawPrice) {
return rawPrice*0.5;
}
}
Nós poderíamos ter nossa classe cliente:
class Customer {
/* fields */
public Customer(final BillingStrategy strategy) {
this.drinks = new ArrayList<Double>();
this.strategy = strategy;
}
public void add(final double price, final int quantity) {
drinks.add(strategy.getActPrice(price*quantity));
}
public void setStrategy(final BillingStrategy strategy) {
this.strategy = strategy;
}
}
Então poderíamos usar de forma elegante:
public class StrategyPatternWiki {
public static void main(final String[] arguments) {
Customer firstCustomer = new Customer(new NormalStrategy());
// Normal billing
firstCustomer.add(1.0, 1);
// Start Happy Hour
firstCustomer.setStrategy(new HappyHourStrategy());
firstCustomer.add(1.0, 2);
}
}
Legal né? Como isso pode ser ruim? Vamos aplicar em todos os lugares!
Mas há um problema, mesmo neste exemplo simples. Como sei quando usar HappyHourStrategy ou NormalStrategy a não ser em um novo “if”? Posso esconder esse outro “if” com outra estratégia? Se sim, seria melhor do que um único “if”? Talvez, em vez disso, eu pudesse adicionar na classe do cliente:
class Customer {
...
public void add(final double price, final int quantity) {
drinks.add(floatingPriceCalculator.getCurrentPrice(price*quantity));
}
...
}
E então:
class FloatingPriceCalculator {
public double getCurrentPrice(final double rawPrice) {
if (isHappyHourTime()){
return rawPrice * 0.5;
} else {
return rawPrice;
}
}
}
Está tão ruim assim? Você está tomando a decisão em um único IF-ELSE ao invés de construir uma camada de abstração, além disso, ainda em um único ponto, caso precise refatorar ou ler. Se as condições aumentarem em número, talvez seja hora de refatorar, mas agora, acho que é bom o suficiente.
Os Design Patterns são ferramentas, não a única maneira de fazer algo.
Mal uso de Interfaces Funcionais
As interfaces funcionais são sensacionais. Podem realmente tornar seu código mais elegante e simples. Vamos dar uma olhada, usando OReilly como um recurso.
@FunctionalInterface
public interface Runnable {
public void run();
}
Nos velhos tempos nós precisaríamos escrever algo como:
Thread thread1 = new Thread(new Runnable() {
@Override
public void run(){
System.out.println("Run now!");
}
});
Com Java8 e Lambdas, o compilador sabe que você está chamando o método único da interface funcional.
Runnable task1 = () -> { System.out.println("run you damn!"); };
new Thread(task1).start();
Bem legal. A sintaxe “()” é útil porque você pode simplesmente passar os argumentos e o compilador infere os tipos.
Os problemas surgem quando você tenta construir uma DSL para uma linguagem fluente com isso, porque hoje em dia você vê coisas como:
maClass.needDoThis()
.AndThis()
.withALittleBitOfThis()
.andNowDone();
E para fazer isso compilar você cria uma interface funcional para cada um dos pontos, assim:
public interface AndThis {
LittleBitOfThis andThis();
}
public interface LittleBitOfThis {
AndThis withALittleBitOfThis();
}
public interface NowDone {
void andNowDone();
}
E em maClass.needDoThis() você lança um:
public class MaClass{
public void needDoThis() {
() -> () -> () -> {
//do a lot of things
}
}
}
Eu vi isso acontecer mais de uma vez. Não faça isso. Você criou muitas complicações apenas para fazer uma chamada bonitinha, que, no final das contas, é apenas um método único. Não seria mais fácil se você apenas chamasse o método?
Orientação a Objeto pobre (ou em exagero)
Eu sei que isso é possível em qualquer linguagem OO, mas Java sofre de forma especial. Há muita confusão sobre conceitos como composição ao invés de herança, ou partes soltas levemente acopladas, ou design de classe simples, e diversos problemas surgem dessa confusão.
(Os métodos padrão em interfaces no Java8 tornaram isso ainda mais comum)
Um grande sinal de mau design de classe é o operador instanceof
muito utilizado. Ele, é claro, tem seus usos, mas sempre pense bem ao usá-lo para verificar se seu design é compatível com OO.
Um exemplo ruim de StackOverflow
class Cage {
public static Cage createCage(Animal animal) {
if (animal instanceof Dog)
return new DogHouse();
else if (animal instanceof Lion)
return new SteelCage();
else if (animal instanceof Chicken)
return new ChickenWiredCage();
else if (animal instanceof AlienPreditor)
return new ForceFieldCage();
else
return new GenericCage();
}
}
Você poderia colocar a responsabilidade dentro da interface do Animal:
interface Animal {
Cage getCage();
}
class Dog implements Animal {
...
public Cage getCage() {
return new DogCage();
}
...
}
(Eu sei, é apenas um exemplo acadêmico, mas você entendeu)
No outro lado do espectro, há o design de classe excessivamente complexo, que também deve ser evitado.
Reflection
O Reflection é um dos recursos mais poderosos do Java. Você pode, basicamente, acessar qualquer coisa em tempo de execução, até mesmo modificar os métodos ou campos de suas classes. A maioria dos frameworks usa Reflection como base para ser capaz de chamar seu código usando as convenções da linguagem (getters e setters, por exemplo. Como você acha que o hibernate preenche seus campos?).
Novamente, o problema com isso é a camada de abstração que você coloca em seu código. Quando você usa Reflection, seus erros estarão em tempo de execução e sua IDE não será capaz de ajudá-lo com o “encontrar utilizações desse método”. Você está sozinho.
Como regra geral, use o Reflection para construir frameworks, porque eles precisam ser capazes de chamar código ainda não existente. Para construir seus aplicativos, evite sempre.
Excessos de Aspecto
Acho que este é um problema mais “pessoal”, porque vejo muitas pessoas elogiando AOP e os benefícios do AspectJ, mas me encontro gostando cada vez menos com o passar do tempo. No momento, eu não o usaria em aplicações, mas, como é com Reflection, é útil para a criação de frameworks.
Uma das principais razões pelas quais eu acho que é prejudicial, é para depuração. Você se deparou com um sistema legado com um bug e vislumbrou o log:
2017-05-25 00:22:31.496 INFO 1 --- [Thread-3] maProject.maPackage.maClass1 : Trying to do something
2017-05-25 00:22:31.512 INFO 1 --- [Thread-2] maProject.maPackage.maClass2 : I shoudn't be trying this, but I am nonetheless
2017-05-25 00:22:31.530 INFO 1 --- [Thread-3] maProject.maPackage.maClass3 : Continuing ok, nothing to see here
Legal, maClass2 parece ser o agressor. Então você abre a classe e não encontra a linha de log. E não encontra onde maClass1 está chamando maClass2. E nem a chamada para maClass1. Tudo é construído em torno de aspectos, e todo o seu projeto é uma coleção enorme de camadas indiretas, uma após a outra.
Evite isso. Não é prejudicial ver um log.info (")
dentro do seu método.
Às vezes, é melhor ser direto do que elegante.
EJB 2
Não faça. Eu falo sério.
Comentários
Sinto que os comentários em blogs têm diminuído com o passar do tempo. Se você tiver alguma dúvida ou quiser falar sobre o post, entre em contato comigo pelos links abaixo.