Uma constante de string deve ser definida se for usada apenas uma vez?

24

Estamos implementando um adaptador para o Jaxen (uma biblioteca XPath para Java) que nos permite usar o XPath para acessar o modelo de dados de nosso aplicativo.

Isso é feito implementando classes que mapeiam seqüências de caracteres (passadas do Jaxen para nós) em elementos do nosso modelo de dados. Estimamos que precisaremos de cerca de 100 classes com mais de 1000 comparações de strings no total.

Eu acho que a melhor maneira de fazer isso é simples declarações if / else com as strings escritas diretamente no código - em vez de definir cada strings como uma constante. Por exemplo:

public Object getNode(String name) {
    if ("name".equals(name)) {
        return contact.getFullName();
    } else if ("title".equals(name)) {
        return contact.getTitle();
    } else if ("first_name".equals(name)) {
        return contact.getFirstName();
    } else if ("last_name".equals(name)) {
        return contact.getLastName();
    ...

No entanto, sempre fui ensinado que não devemos incorporar valores de string diretamente no código, mas criar constantes em vez disso. Isso seria algo como isto:

private static final String NAME = "name";
private static final String TITLE = "title";
private static final String FIRST_NAME = "first_name";
private static final String LAST_NAME = "last_name";

public Object getNode(String name) {
    if (NAME.equals(name)) {
        return contact.getFullName();
    } else if (TITLE.equals(name)) {
        return contact.getTitle();
    } else if (FIRST_NAME.equals(name)) {
        return contact.getFirstName();
    } else if (LAST_NAME.equals(name)) {
        return contact.getLastName();
    ...

Nesse caso, acho que é uma má ideia. A constante será usada apenas uma vez no getNode()método. Usar as strings diretamente é tão fácil de ler e entender quanto usar constantes e nos poupa a escrever pelo menos mil linhas de código.

Existe alguma razão para definir constantes de string para um único uso? Ou é aceitável usar seqüências de caracteres diretamente?


PS. Antes que alguém sugira o uso de enumerações, prototipamos isso, mas a conversão de enumerações é 15 vezes mais lenta que a simples comparação de cadeias, para que não seja considerada.


Conclusão: As respostas abaixo expandiram o escopo desta pergunta além de apenas constantes de string, então eu tenho duas conclusões:

  • Provavelmente, não há problema em usar as seqüências diretamente, em vez de constantes, neste cenário, mas
  • Existem maneiras de evitar o uso de strings, o que pode ser melhor.

Então, eu vou tentar a técnica de invólucro que evita seqüências de caracteres completamente. Infelizmente, não podemos usar a instrução switch de cadeia porque ainda não estamos no Java 7. Em última análise, porém, acho que a melhor resposta para nós é tentar cada técnica e avaliar seu desempenho. A realidade é que, se uma técnica for claramente mais rápida, provavelmente a escolheremos, independentemente de sua beleza ou adesão à convenção.

ravina
fonte
3
Você não planeja digitar manualmente 1.000 instruções if, não?
Jeffo
1
Acho que é tão triste como desagradável algo tão simples pode ser em algumas línguas ...
Jon Purdy
5
O Java 7 permite Strings como switchrótulos. Use um switch em vez de ifcascatas.
Restabeleça Monica - M. Schröder
3
A conversão de enum é 15 vezes mais lenta se você converter uma string para seu valor de enum! Passe enum diretamente e compare com outro valor de enum do mesmo tipo!
Neil
2
Cheiros como HashMap podem ser uma solução.
MarioDS

Respostas:

5

Tente isso. A reflexão inicial é certamente cara, mas se você a usar muitas vezes, o que eu acho que você usará, essa certamente será uma solução melhor para o que você está propondo. Não gosto de usar reflexão, mas me vejo usando quando não gosto da alternativa à reflexão. Eu acho que isso poupará muita dor de cabeça à sua equipe, mas você deve passar o nome do método (em minúsculas).

Em outras palavras, em vez de passar "nome", você passaria "nome completo" porque o nome do método get é "getFullName ()".

Map<String, Method> methodMapping = null;

public Object getNode(String name) {
    Map<String, Method> methods = getMethodMapping(contact.getClass());
    return methods.get(name).invoke(contact);
}

public Map<String, Method> getMethodMapping(Class<?> contact) {
    if(methodMapping == null) {
        Map<String, Method> mapping = new HashMap<String, Method>();
        Method[] methods = contact.getDeclaredMethods();
        for(Method method : methods) {
            if(method.getParameterTypes().length() == 0) {
                if(method.getName().startsWith("get")) {
                    mapping.put(method.getName().substring(3).toLower(), method);
                } else if (method.getName().startsWith("is"))) {
                    mapping.put(method.getName().substring(2).toLower(), method);
                }
            }
        }
        methodMapping = mapping;
    }
    return methodMapping;
}

Se você precisar acessar os dados contidos nos membros do contato, considere criar uma classe de wrapper para o contato que possua todos os métodos para acessar qualquer informação necessária. Isso também seria útil para garantir que os nomes dos campos de acesso sempre permaneçam os mesmos (ou seja, se a classe do wrapper tiver getFullName () e você chamar com fullname, ele sempre funcionará mesmo que o getFullName () do contato tenha sido renomeado - ele causaria um erro de compilação antes de permitir isso).

public class ContactWrapper {
    private Contact contact;

    public ContactWrapper(Contact contact) {
        this.contact = contact;
    }

    public String getFullName() {
        return contact.getFullName();
    }
    ...
}

Essa solução me salvou várias vezes, ou seja, quando eu queria ter uma única representação de dados para usar em tabelas de dados jsf e quando esses dados precisavam ser exportados para um relatório usando jasper (que não lida bem com acessadores de objetos complicados na minha experiência) .

Neil
fonte
Eu gosto da idéia de um objeto wrapper com os métodos chamados via .invoke(), porque elimina completamente as constantes de string. Não gosto muito da reflexão em tempo de execução para configurar o mapa, embora talvez esteja executandogetMethodMapping() em um staticbloco esteja OK para que aconteça na inicialização, em vez de quando o sistema estiver em execução.
Gutch
@gutch, o padrão de invólucro é um que eu uso com frequência, pois tende a gerar muitos problemas relacionados à interface / controlador. A interface sempre pode usar o invólucro e ficar feliz com ele, enquanto o controlador pode ser virado do avesso. Tudo o que você precisa saber é quais dados você deseja disponibilizar na interface. E, novamente, digo enfaticamente, normalmente não gosto de reflexão, mas se for um aplicativo da Web, é completamente aceitável se você fizer isso na inicialização, pois o cliente não verá esse tempo de espera.
314 Neil
@ Neil Por que não usar BeanUtils do Apache commons? Ele também suporta objetos incorporados. Você pode passar por toda uma estrutura de dados obj.attrA.attrB.attrN e tem muitas outras possibilidades :-)
LAIV
Em vez de mapeamentos com o Google Maps, eu usaria @Annotations. Algo como JPA faz. Definir minha própria anotação para mapear entradas do controlador (string) com um atributo ou um getter específico. Para trabalhar com anotação é muito fácil e é disponível a partir de Java 1.6 (eu acho)
LAIV
5

Se possível, use o Java 7, que permite usar Strings em switchinstruções.

Em http://docs.oracle.com/javase/tutorial/java/nutsandbolts/switch.html

public class StringSwitchDemo {

    public static int getMonthNumber(String month) {

        int monthNumber = 0;

        if (month == null) {
            return monthNumber;
        }

        switch (month.toLowerCase()) {
            case "january":
                monthNumber = 1;
                break;
            case "february":
                monthNumber = 2;
                break;
            case "march":
                monthNumber = 3;
                break;
            case "april":
                monthNumber = 4;
                break;
            case "may":
                monthNumber = 5;
                break;
            case "june":
                monthNumber = 6;
                break;
            case "july":
                monthNumber = 7;
                break;
            case "august":
                monthNumber = 8;
                break;
            case "september":
                monthNumber = 9;
                break;
            case "october":
                monthNumber = 10;
                break;
            case "november":
                monthNumber = 11;
                break;
            case "december":
                monthNumber = 12;
                break;
            default: 
                monthNumber = 0;
                break;
        }

        return monthNumber;
    }

    public static void main(String[] args) {

        String month = "August";

        int returnedMonthNumber =
            StringSwitchDemo.getMonthNumber(month);

        if (returnedMonthNumber == 0) {
            System.out.println("Invalid month");
        } else {
            System.out.println(returnedMonthNumber);
        }
    }
}

Não medi, mas acredito que as instruções switch são compiladas em uma tabela de salto em vez de uma longa lista de comparações. Isso deve ser ainda mais rápido.

Em relação à sua pergunta real: Se você a usar apenas uma vez , não será necessário transformá-la em uma constante. Considere, no entanto, que uma constante pode ser documentada e exibida em Javadoc. Isso pode ser importante para valores de sequência não triviais.


fonte
2
Em relação à mesa de salto. A opção String é substituída por switch, primeiro é baseada no código de hash (a igualdade é verificada para todas as constantes com o mesmo código de hash) e seleciona o índice da ramificação, depois alterna no índice da ramificação e seleciona o código da ramificação original. O último é claramente adequado para uma tabela de ramificação, o primeiro não é devido à distribuição da função hash. Portanto, qualquer vantagem de desempenho provavelmente se deve à realização baseada em hash.
scarfridge
Um ponto muito bom; se ele funciona bem pode valer a pena se mudar para Java 7 apenas para esta ...
Gutch
4

Se você deseja manter isso (faça qualquer tipo de alteração não trivial), eu poderia considerar usar algum tipo de geração de código orientado a anotações (talvez via CGLib ) ou até mesmo apenas um script que escreva todo o código para você. Imagine o número de erros de digitação e erros que podem surgir com a abordagem que você está considerando ...

Steven Schlansker
fonte
Consideramos a anotação de métodos existentes, mas alguns mapeamentos atravessam vários objetos (por exemplo, mapeamento de "país" para object.getAddress().getCountry())), o que é difícil de representar com anotações. As comparações de strings if / else não são bonitas, mas são rápidas, flexíveis, fáceis de entender e fáceis de testar na unidade.
Gutch
1
Você está certo sobre o potencial de erros de digitação; minha única defesa há testes de unidade. É claro que isso significa ainda mais código ...
Gutch
2

Eu ainda usaria constantes definidas no topo de suas aulas. Isso torna seu código mais sustentável, pois é mais fácil ver o que pode ser alterado posteriormente (se necessário). Por exemplo, "first_name"pode se tornar "firstName"em algum momento posterior.

Bernard
fonte
Concordo, no entanto, se esse código for gerado automaticamente e as constantes não forem usadas em outros lugares, isso não importa (o OP diz que precisa fazer isso em 100 classes).
NoChance
5
Eu simplesmente não vejo o ângulo de "manutenção" aqui, você muda "first_name" para "namedName" uma vez em um só lugar, no entanto, no caso de constantes nomeadas, você agora fica com uma variável confusa "first_name", que se refere a uma string "givenName" então você provavelmente quer mudança que tão bem como agora você tem três mudanças em dois lugares
James Anderson
1
Com o IDE certo, essas alterações são triviais. O que eu estou defendendo é que é mais evidente onde fazer essas alterações, porque você gastou um tempo para declarar constantes no topo da classe e não precisa ler o restante do código da classe para poder faça essas alterações.
Bernard
Mas quando você está lendo a instrução if, precisa voltar e verificar se a constante contém a string que você acha que contém - nada salvo aqui.
James Anderson
1
Talvez, mas é por isso que nomeio bem minhas constantes.
Bernard
1

Se sua nomeação for consistente (também conhecida como "some_whatever"sempre é mapeada getSomeWhatever()), você poderá usar a reflexão para determinar e executar o método get.

scarfridge
fonte
Melhor getSome_whatever (). Pode estar quebrando o estojo de camelo, mas é muito mais importante garantir que a reflexão funcione. Além disso, tem a vantagem adicional de fazer você dizer: "Por que diabos fizemos dessa maneira ... oh, espere .. Espere! George não muda o nome desse método!"
Neil
0

Eu acho que o processamento de anotações pode ser a solução, mesmo sem anotações. É o que pode gerar todo o código chato para você. A desvantagem é que você receberá N classes geradas para N classes de modelo. Você também não pode adicionar nada a uma classe existente, mas escrevendo algo como

public Object getNode(String name) {
    return SomeModelClassHelper.getNode(this, name);
}

uma vez por aula não deve ser um problema. Como alternativa, você pode escrever algo como

public Object getNode(String name) {
    return getHelper(getClass()).getNode(this, name);
}

em uma superclasse comum.


Você pode usar reflexão em vez de processamento de anotação para geração de código. A desvantagem é que você precisa que seu código seja compilado antes de poder refletir sobre ele. Isso significa que você não pode confiar no código gerado em suas classes de modelo, a menos que gere alguns stubs.


Eu também consideraria o uso direto da reflexão. Claro, a reflexão é lenta, mas por que é lenta? É porque ele precisa fazer todas as coisas necessárias, por exemplo, ativar o nome do campo.

maaartinus
fonte