É um mau hábito (excesso) usar a reflexão?

16

É uma boa prática usar a reflexão se reduzir bastante a quantidade de código padrão?

Basicamente, há uma troca entre desempenho e talvez legibilidade de um lado e abstração / automação / redução do código padrão no outro lado.

Edit: Aqui está um exemplo de um uso recomendado de reflexão .

Para dar um exemplo, suponha que exista uma classe abstrata Baseque possua 10 campos e 3 subclasses SubclassA, SubclassBe SubclassCcada uma com 10 campos diferentes; todos são feijões simples. O problema é que você obtém duas Basereferências de tipo e deseja ver se os objetos correspondentes são do mesmo (sub) tipo e são iguais.

Como soluções, existe a solução bruta na qual você primeiro verifica se os tipos são iguais e depois verifica todos os campos ou pode usar a reflexão e ver dinamicamente se eles são do mesmo tipo e iteram em todos os métodos que começam com "get" (convenção sobre a configuração), chame-os nos objetos e chame iguais nos resultados.

boolean compare(Base base1, Base, base2) {
    if (base1 instanceof SubclassA && base2 instanceof SubclassA) { 
         SubclassA subclassA1 = (SubclassA) base1;
         SubclassA subclassA2 = (SubclassA) base2;
         compare(subclassA1, subclassA2);
    } else if (base1 instanceof SubclassB && base2 instanceof SubclassB) {
         //the same
    }
    //boilerplate
}

boolean compare(SubclassA subA1, SubclassA subA2) {
    if (!subA1.getField1().equals(subA2.getField1)) {
         return false;
    }
    if (!subA1.getField2().equals(subA2.getField2)) {
         return false;
    }
    //boilerplate
}

boolean compare(SubclassB subB1, SubclassB subB2) {
    //boilerplate
}

//boilerplate

//alternative with reflection 
boolean compare(Base base1, Base base2) {
        if (!base1.getClass().isAssignableFrom(base2.getClass())) {
            System.out.println("not same");
            System.exit(1);
        }
        Method[] methods = base1.getClass().getMethods();
        boolean isOk = true;
        for (Method method : methods) {
            final String methodName = method.getName();
            if (methodName.startsWith("get")) {
                Object object1 = method.invoke(base1);
                Object object2 = method.invoke(base2);
                if(object1 == null || object2 == null)  {
                    continue;
                }
                if (!object1.equals(object2)) {
                    System.out.println("not equals because " + object1 + " not equal with " + object2);
                    isOk = false;
                }
            }
        }

        if (isOk) {
            System.out.println("is OK");
        }
}
m3th0dman
fonte
20
O uso excessivo de qualquer coisa é um mau hábito.
Tulains Córdova
1
@ user61852 Certo, muita liberdade leva à ditadura. Alguns gregos antigos já sabiam disso.
ott--
6
“Muita água seria ruim para você. Obviamente, muito é precisamente a quantidade que é excessivo, isto é o que significa!”- Stephen Fry
Jon Purdy
4
"Sempre que você escrever código do formulário" se o objeto for do tipo T1, faça alguma coisa, mas se for do tipo T2, faça outra coisa ", dê um
rm5248

Respostas:

25

Reflexão foi criado para uma finalidade específica, para descobrir a funcionalidade de uma classe que era desconhecido em tempo de compilação, semelhante ao que o dlopene dlsymfunções fazer em C. Qualquer utilização fora do que deve ser fortemente examinado.

Já lhe ocorreu que os próprios designers de Java encontraram esse problema? É por isso que praticamente toda classe tem um equalsmétodo. Classes diferentes têm definições diferentes de igualdade. Em algumas circunstâncias, um objeto derivado pode ser igual a um objeto base. Em algumas circunstâncias, a igualdade pode ser determinada com base em campos privados sem getters. Você não sabe.

É por isso que todo objeto que deseja igualdade personalizada deve implementar um equalsmétodo. Eventualmente, você desejará colocar os objetos em um conjunto ou usá-los como um índice de hash, e precisará implementá-lo de equalsqualquer maneira. Outras linguagens fazem isso de maneira diferente, mas o Java usa equals. Você deve seguir as convenções do seu idioma.

Além disso, o código "clichê", se colocado na classe correta, é muito difícil de estragar. A reflexão adiciona complexidade adicional, o que significa chances adicionais de erros. No seu método, por exemplo, dois objetos são considerados iguais se um retornar nullpara um determinado campo e o outro não. E se um de seus getters retornar um de seus objetos, sem um apropriado equals? Seu if (!object1.equals(object2))irá falhar. Também tornando-o propenso a erros é o fato de que a reflexão raramente é usada, para que os programadores não estejam tão familiarizados com suas dicas.

Karl Bielefeldt
fonte
12

O uso excessivo da reflexão provavelmente depende do idioma usado. Aqui você está usando Java. Nesse caso, a reflexão deve ser usada com cuidado, porque geralmente é apenas uma solução alternativa para um design inadequado.

Portanto, você está comparando classes diferentes, este é um problema perfeito para a substituição de métodos . Observe que as instâncias de duas classes diferentes nunca devem ser consideradas iguais. Você pode comparar a igualdade apenas se tiver instâncias da mesma classe. Consulte /programming/27581/overriding-equals-and-hashcode-in-java para obter um exemplo de como implementar a comparação de igualdade corretamente.

Sulthan
fonte
16
+1 - Alguns de nós acreditam que QUALQUER uso de reflexão é uma bandeira vermelha indicando mau design.
Ross Patterson
1
Provavelmente, nesse caso, é desejável uma solução baseada em iguais, mas como idéia geral, o que há de errado com a solução de reflexão? Na verdade, é muito genérico e os métodos equals não precisam ser escritos explicitamente em todas as classes e subclasses (embora possam ser facilmente gerados por um bom IDE).
M3th0dman
2
@RossPatterson Por quê?
M3th0dman
2
@ m3th0dman Porque leva a coisas como o seu compare()método, assumindo que qualquer método que comece com "get" é um getter e é seguro e apropriado chamar como parte de uma operação de comparação. É uma violação da definição de interface pretendida do objeto e, embora possa ser conveniente, quase sempre está errado.
Ross Patterson
4
@ m3th0dman Viola o encapsulamento - a classe base precisa acessar atributos em suas subclasses. E se eles forem privados (ou privados)? E o polimorfismo? Se eu decidir adicionar outra subclasse e desejar fazer uma comparação diferente nela? Bem, a classe base já está fazendo isso por mim e não posso mudar. E se os getters carregarem algo preguiçosamente? Eu quero o método de comparação para fazer isso? Como eu sei que um método começando com geté um getter e não um método personalizado que retorna algo?
Sulthan 01/04
1

Eu acho que você tem dois problemas aqui.

  1. Quanto código dinâmico vs estático devo ter?
  2. Como expresso uma versão personalizada da igualdade?

Código Estático vs Dinâmico

Esta é uma pergunta perene e a resposta é muito opinativa.

Por um lado, seu compilador é muito bom em capturar todo tipo de código incorreto. Isso é feito através de várias formas de análise, sendo a análise de tipo comum. Ele sabe que você não pode usar um Bananaobjeto no código que está esperando a Cog. Ele diz isso através de um erro de compilação.

Agora, isso só pode ser feito quando é possível inferir o tipo aceito e o tipo fornecido do contexto. Quanto pode ser inferido, e quão geral é essa inferência, depende em grande parte do idioma que está sendo usado. Java pode inferir informações de tipo por meio de mecanismos como Herança, Interfaces e Genéricos. A milhagem varia, alguns outros idiomas fornecem menos mecanismos e outros fornecem mais. Ainda se resume ao que o compilador pode saber como verdadeiro.

Por outro lado, seu compilador não pode prever a forma do código externo e, às vezes, um algoritmo geral pode ser expresso em muitos tipos que não podem ser expressos facilmente usando o sistema de tipos da linguagem. Nesses casos, o compilador nem sempre pode saber o resultado com antecedência e pode nem ser capaz de saber qual pergunta fazer. Reflexão, interfaces e a classe Object são a maneira do Java lidar com esses problemas. Você precisará fornecer as verificações e o manuseio corretos, mas não é prejudicial ter esse tipo de código.

A possibilidade de tornar seu código muito específico ou muito geral se resume ao problema que você está tentando solucionar. Se você puder expressá-lo facilmente usando o sistema de tipos, faça-o. Deixe o compilador tocar seus pontos fortes e ajudá-lo. Se o sistema de tipos não puder conhecer antecipadamente (código estrangeiro), ou se o sistema de tipos não for adequado para uma implementação geral do seu algoritmo, a reflexão (e outros meios dinâmicos) é a ferramenta certa a ser usada.

Lembre-se de que sair do sistema de tipos do seu idioma é surpreendente. Imagine ir até o seu amigo e começar uma conversa em inglês. De repente, solte algumas palavras do espanhol, francês e cantonês que expressam seus pensamentos com precisão. O contexto dirá muito a seu amigo, mas ele também pode não saber lidar com essas palavras, levando a todo tipo de mal-entendido. Lidar com esses mal-entendidos é melhor do que explicar essas idéias em inglês usando mais palavras?

Igualdade personalizada

Embora eu entenda que o Java depende muito do equalsmétodo para comparar geralmente dois objetos, nem sempre é adequado em um determinado contexto.

Existe outra maneira, e também é um padrão Java. É chamado de Comparador .

A maneira como você implementa o seu comparador dependerá do que você está comparando e como.

  • Pode ser aplicado a dois objetos, independentemente de sua equalsimplementação específica .
  • Ele poderia implementar um método de comparação geral (baseado em reflexão) para manipular dois objetos.
  • As funções de comparação padrão foram adicionadas para tipos de objetos comumente comparados, garantindo segurança e otimização do tipo.
Kain0_0
fonte
1

Eu prefiro evitar a programação reflexiva, tanto quanto possível, porque

  • torna o código mais difícil de verificar estaticamente pelo compilador
  • torna o código mais difícil de raciocinar sobre
  • torna o código mais difícil de refatorar

Também tem muito menos desempenho do que chamadas simples de método; costumava ser mais lento por uma ordem de magnitude ou mais.

Código de verificação estaticamente

Qualquer código reflexivo procura classes e métodos usando strings; no exemplo original, ele está procurando por qualquer método que comece com "get"; ele retornará getters, mas também outros métodos, como "gettysburgAddress ()". As regras podem ser reforçadas no código, mas a questão é que é uma verificação de tempo de execução ; um IDE e o compilador não podem ajudar. Em geral, não gosto de códigos "tipificados" ou "obsessivos primitivos".

Mais difícil de raciocinar

O código reflexivo é mais detalhado do que as chamadas simples de método. Mais código = mais bugs, ou pelo menos mais potencial para bugs, mais código para ler, testar etc. Menos é mais.

Mais difícil de refatorar

Porque o código é baseado em strings / dinamicamente; assim que a reflexão estiver em cena, você não poderá refatorar o código com 100% de confiança usando as ferramentas de refatoração de um IDE, pois o IDE não poderá captar os usos reflexivos.

Basicamente, evite a reflexão no código geral, se possível; procure um design aprimorado.

David Kerr
fonte
0

O uso excessivo de qualquer coisa por definição é ruim, certo? Então, vamos nos livrar do (over) por enquanto.

Você chamaria o uso interno incrivelmente pesado da reflexão da primavera de um mau hábito?

A primavera domestica a reflexão usando anotações - o mesmo ocorre com o Hibernate (e provavelmente dezenas / centenas de outras ferramentas).

Siga esses padrões se você o usar em seu próprio código. Use anotações para garantir que o IDE do usuário ainda possa ajudá-lo (mesmo se você for o único "Usuário" do seu código, o uso descuidado da reflexão provavelmente voltará a morder sua bunda eventualmente).

No entanto, sem considerar como seu código será usado pelos desenvolvedores, mesmo o uso mais simples da reflexão provavelmente será usado em excesso.

Bill K
fonte
0

Eu acho que a maioria dessas respostas não entendem o ponto.

  1. Sim, você provavelmente deve escrever equals()e hashcode(), como observado por @KarlBielefeldt.

  2. Mas, para uma classe com muitos campos, isso pode ser um tédio.

  3. Então depende .

    • Se você raramente precisar de iguais e hashcode raramente , é pragmático e provavelmente ok usar um cálculo de reflexão de uso geral. Pelo menos como um primeiro passe rápido e sujo.
    • mas se você precisar de muito, por exemplo , esses objetos são colocados no HashTables, para que o desempenho seja um problema, você definitivamente deve escrever o código. será muito mais rápido.
  4. Outra possibilidade: se você realmente tem tantos campos que escrever um igual é entediante, considere

    • colocando os campos em um mapa.
    • você ainda pode escrever getters e setters personalizados
    • use Map.equals()para sua comparação. (ou Map.hashcode())

por exemplo ( nota : ignorar as verificações nulas, provavelmente deve usar Enums em vez de chaves String, muito não mostrado ...)

class TooManyFields {
  private HashMap<String, Object> map = new HashMap<String, Object>();

  public setFoo(int i) { map.put("Foo", Integer.valueOf(i)); }
  public int getFoo()  { return map.get("Foo").intValue(); }

  public setBar(Sttring s) { map.put("Bar", s); }
  public String getBar()  { return map.get("Bar").toString(); }

  ... more getters and setters ...

  public boolean equals(Object o) {
    return (o instanceof TooManyFields) &&
           this.map.equals( ((TooManyFields)o).map);
}
user949300
fonte