È necessario definire una costante di stringa se verrà utilizzata una sola volta?


24

Stiamo implementando un adattatore per Jaxen (una libreria XPath per Java) che ci consente di utilizzare XPath per accedere al modello di dati della nostra applicazione.

Questo viene fatto implementando classi che mappano le stringhe (passate da Jaxen) in elementi del nostro modello di dati. Stimiamo che avremo bisogno di circa 100 classi con oltre 1000 confronti di stringhe in totale.

Penso che il modo migliore per farlo sia semplice se / else istruzioni con le stringhe scritte direttamente nel codice - piuttosto che definire ogni stringa come costante. Per esempio:

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

Tuttavia mi è stato sempre insegnato che non dovremmo incorporare i valori di stringa direttamente nel codice, ma creare invece costanti di stringa. Sarebbe simile a questo:

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

In questo caso penso che sia una cattiva idea. La costante verrà utilizzata una sola volta, nel filegetNode() metodo. L'uso diretto delle stringhe è altrettanto facile da leggere e comprendere come l'utilizzo delle costanti e ci consente di risparmiare scrivendo almeno mille righe di codice.

Quindi c'è qualche motivo per definire le costanti di stringa per un singolo uso? O è accettabile usare direttamente le stringhe?


PS. Prima che qualcuno suggerisse di usare enum invece, lo abbiamo prototipato ma la conversione di enum è 15 volte più lenta del semplice confronto di stringhe, quindi non viene presa in considerazione.


Conclusione: le risposte che seguono hanno ampliato l'ambito di questa domanda oltre le sole costanti di stringa, quindi ho due conclusioni:

  • Probabilmente è OK usare le stringhe direttamente anziché le costanti di stringa in questo scenario, ma
  • Ci sono modi per evitare di usare le stringhe, il che potrebbe essere migliore.

Quindi proverò la tecnica wrapper che evita completamente le stringhe. Sfortunatamente non possiamo usare l'istruzione switch di stringa perché non siamo ancora su Java 7. Alla fine, però, penso che la migliore risposta per noi sia provare ogni tecnica e valutarne le prestazioni. La realtà è che se una tecnica è chiaramente più veloce, probabilmente la sceglieremo indipendentemente dalla sua bellezza o aderenza alla convenzione.


3
Non hai intenzione di tastiera manualmente un 1000 se affermazioni lo fai?
JeffO,

1
Trovo così triste quanto sia sgradevole qualcosa di così semplice in alcune lingue ...
Jon Purdy,

5
Java 7 consente alle stringhe come switchetichette. Utilizzare un interruttore anziché le ifcascate.
Ripristina Monica - M. Schröder il

3
la conversione di enum è 15 volte più lenta se converti una stringa nel suo valore di enum! Passa enum direttamente e confronta con un altro valore enum dello stesso tipo!
Neil,

2
Odori come HashMap possono essere una soluzione.
MarioDS,

Risposte:


5

Prova questo. La riflessione iniziale è certamente costosa, ma se la userai molte volte, cosa che penso che farai, questa è sicuramente una soluzione migliore di ciò che stai proponendo. Non mi piace usare il riflesso, ma mi ritrovo ad usarlo quando non mi piace l'alternativa al riflesso. Penso che questo salverà un sacco di mal di testa alla tua squadra, ma devi passare il nome del metodo (in minuscolo).

In altre parole, anziché passare "name", passeresti "fullname" perché il nome del metodo 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 è necessario accedere ai dati contenuti nei membri del contatto, è possibile prendere in considerazione la creazione di una classe wrapper per il contatto che disponga di tutti i metodi per accedere alle informazioni richieste. Ciò sarebbe utile anche per garantire che i nomi dei campi di accesso rimarranno sempre gli stessi (vale a dire se la classe wrapper ha getFullName () e si chiama con il nome completo, funzionerà sempre anche se getFullName () del contatto è stato rinominato - esso causerebbe un errore di compilazione prima che ti permettesse di farlo).

public class ContactWrapper {
    private Contact contact;

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

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

Questa soluzione mi ha salvato diverse volte, vale a dire quando volevo avere un'unica rappresentazione dei dati da utilizzare nei database jsf e quando quei dati dovevano essere esportati in un report usando jasper (che non gestisce bene gli accessi agli oggetti complicati nella mia esperienza) .


Mi piace l'idea di un oggetto wrapper con i metodi chiamati via .invoke(), perché elimina completamente le costanti di stringa. Non sono così appassionato di riflessione del runtime per impostare la mappa, anche se forse l'esecuzione getMethodMapping()in un staticblocco sarebbe OK in modo che accada all'avvio invece che quando il sistema è in esecuzione.
gutch

@gutch, il pattern wrapper è uno che uso spesso, poiché tende a risolvere molti problemi relativi all'interfaccia / controller. L'interfaccia può sempre usare il wrapper ed esserne felice e nel frattempo il controller può essere capovolto. Tutto quello che devi sapere è quali dati desideri disponibili nell'interfaccia. E ancora, dico per enfasi, di solito non mi piace la riflessione, ma se si tratta di un'applicazione Web, è completamente accettabile se lo fai all'avvio poiché il client non vedrà nessuno di quei tempi di attesa.
Neil,

@Neil Perché non usare BeanUtils dai beni comuni di Apache? Supporta anche oggetti incorporati. Puoi passare attraverso un'intera struttura di dati obj.attrA.attrB.attrN e ha molte altre possibilità :-)
Laiv

Invece di mappare con Maps, preferirei @Annotations. Qualcosa come JPA. Definire la mia annotazione per mappare le voci del controller (stringa) con un attr o un getter specifico. Lavorare con Annotation è abbastanza semplice ed è disponibile da Java 1.6 (credo)
Laiv

5

Se possibile, utilizzare Java 7 che consente di utilizzare le stringhe nelle switchistruzioni.

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

Non ho misurato, ma credo che le istruzioni switch vengano compilate in una tabella di salto anziché in un lungo elenco di confronti. Questo dovrebbe essere ancora più veloce.

Per quanto riguarda la tua vera domanda: se la usi solo una volta , non è necessario trasformarla in una costante. Considera comunque che una costante può essere documentata e mostrata in Javadoc. Questo può essere importante per valori di stringa non banali.


2
Per quanto riguarda la tabella di salto. L'interruttore String viene sostituito da switch, il primo si basa sull'hashcode (l'uguaglianza viene verificata per tutte le costanti con lo stesso hashcode) e seleziona l'indice del ramo, i secondi passa sull'indice del ramo e seleziona il codice del ramo originale. Il secondo è chiaramente adatto per una tabella di rami, il primo non è dovuto alla distribuzione della funzione hash. Pertanto, qualsiasi vantaggio in termini di prestazioni è probabilmente dovuto alla realizzazione basata sull'hash.
scarfridge,

Un ottimo punto; se funziona bene potrebbe valere la pena passare a Java 7 proprio per questo ...
gutch

4

Se hai intenzione di mantenerlo (apporta qualsiasi tipo di modifica non banale in assoluto), potrei effettivamente prendere in considerazione l'uso di una sorta di generazione di codice basata sulle annotazioni (forse tramite CGLib ) o anche solo uno script che scrive tutto il codice per te. Immagina il numero di errori di battitura ed errori che potrebbero insinuarsi nell'approccio che stai considerando ...


Abbiamo preso in considerazione l'annotazione di metodi esistenti, ma alcune mappature attraversano più oggetti (ad esempio la mappatura "paese" object.getAddress().getCountry()) che è difficile da rappresentare con le annotazioni. I confronti di stringhe if / else non sono belli, ma sono veloci, flessibili, facili da capire e facili da testare.
gutch

1
Hai ragione sul potenziale per errori di battitura ed errori; la mia unica difesa ci sono i test unitari. Ovviamente questo significa ancora più codice ...
gutch

2

Userei ancora le costanti definite nella parte superiore delle tue classi. Rende il tuo codice più gestibile poiché è più facile vedere cosa può essere modificato in un secondo momento (se necessario). Ad esempio, "first_name"potrebbe diventare "firstName"in un secondo momento.


Sono d'accordo, tuttavia, se questo codice verrà generato automaticamente e le costanti non verranno utilizzate altrove, non importa (l'OP afferma che devono farlo in 100 classi).
NoChance,

5
Semplicemente non vedo l'angolo di "manutenibilità" qui cambiate "first_name" in "givenName" una volta in una posizione in entrambi i casi. Tuttavia, nel caso delle costanti nominate, ora vi rimane una variabile disordinata "first_name" che si riferisce a una stringa "givenName" quindi probabilmente anche tu vuoi cambiarla, quindi ora hai tre modifiche in due punti
James Anderson,

1
Con l'IDE giusto, queste modifiche sono banali. Quello che sto sostenendo è che è più evidente dove apportare queste modifiche perché hai impiegato del tempo per dichiarare le costanti in cima alla classe e non devi leggere il resto del codice nella classe per apportare queste modifiche.
Bernard,

Ma quando stai leggendo l'istruzione if, devi tornare indietro e verificare che la costante contenga la stringa che ritieni contenga - niente salvato qui.
James Anderson,

1
Forse, ma è per questo che nomina bene le mie costanti.
Bernard,

1

Se la tua denominazione è coerente (aka "some_whatever"è sempre mappata su getSomeWhatever()) puoi usare la riflessione per determinare ed eseguire il metodo get.


Meglio getSome_whatever (). Potrebbe rompere il caso del cammello, ma è molto più importante assicurarsi che il riflesso funzioni. Inoltre ha l'ulteriore vantaggio di farti dire "Perché diamine l'abbiamo fatto così .. oh aspetta .. Aspetta! George non cambiare il nome di quel metodo!"
Neil,

0

Immagino che l'elaborazione delle annotazioni potrebbe essere la soluzione, anche senza annotazioni. È la cosa che può generare tutto il codice noioso per te. Il rovescio della medaglia è che otterrai N classi generate per N classi di modello. Inoltre non puoi aggiungere nulla a una classe esistente, ma scrivendo qualcosa del genere

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

una volta per classe non dovrebbe essere un problema. In alternativa, potresti scrivere qualcosa del genere

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

in una superclasse comune.


È possibile utilizzare la riflessione anziché l'elaborazione delle annotazioni per la generazione del codice. Il rovescio della medaglia è che è necessario compilare il codice prima di poter utilizzare la riflessione su di esso. Ciò significa che non è possibile fare affidamento sul codice generato nelle classi del modello, a meno che non si generino alcuni stub.


Considererei anche l'uso diretto della riflessione. Certo, la riflessione è lenta, ma perché è lenta? È perché deve fare tutte le cose che devi fare, ad esempio, attivare il nome del campo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.