Чи слід визначати константну строку, якщо вона буде використана лише один раз?


24

Ми реалізуємо адаптер для Jaxen (бібліотека XPath для Java), що дозволяє нам використовувати XPath для доступу до моделі даних нашого додатку.

Це робиться шляхом впровадження класів, які відображають рядки (передані нам від Jaxen) в елементи нашої моделі даних. Ми вважаємо, що нам знадобиться близько 100 класів із понад 1000 порівняннями струн.

Я думаю, що найкращий спосіб зробити це простий, якщо / else висловлювання з рядками, записаними безпосередньо в код, - а не визначення кожного рядка як константи. Наприклад:

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

Однак мене завжди вчили, що ми не повинні вбудовувати рядкові значення безпосередньо в код, а створювати замість них рядкові константи. Це виглядатиме приблизно так:

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

У цьому випадку я думаю, що це погана ідея. Константа буде використана лише один раз у getNode()методі. Використання рядків прямо так само легко читати і розуміти, як використовувати константи, і економить нас, як записати принаймні тисячу рядків коду.

То чи є причина для визначення рядкових констант для одного використання? Або прийнятно використовувати рядки безпосередньо?


PS. Перш ніж хтось запропонує використовувати замість цього enums, ми прототипували це, але перетворення enum у 15 разів повільніше, ніж просте порівняння рядків, тому воно не розглядається.


Висновок: Відповіді, наведені нижче, розширили обсяг цього питання за межі просто рядкових констант, тому у мене є два висновки:

  • Можливо, добре використовувати рядки в цьому сценарії, а не рядкові константи, але
  • Є способи взагалі уникнути використання рядків, що може бути краще.

Тому я спробую обернути техніку, яка повністю уникає струн. На жаль, ми не можемо використовувати оператор перемикання рядків, оскільки ми ще не на Java 7. Зрештою, я думаю, найкраща відповідь для нас - спробувати кожну техніку та оцінити її ефективність. Реальність полягає в тому, що якщо одна техніка явно швидша, ми, ймовірно, виберемо її незалежно від її краси чи дотримання умовності.


3
Ви не плануєте вручну клавіатури 1000, якщо у вас є заяви?
JeffO

1
Мені здається так сумно, як неприємне може бути щось таке просте в деяких мовах…
Джон Перді,

5
Java 7 дозволяє рядки в якості switchміток. Використовуйте вимикач замість ifкаскадів.
Відновити Моніку - М. Шредер

3
Перетворення enum у 15 разів повільніше, якщо ви перетворюєте рядок у значення enum! Передайте enum безпосередньо та порівняйте з іншим значенням enum того ж типу!
Ніл

2
Пахне HashMap може бути рішенням.
MarioDS

Відповіді:


5

Спробуйте це. Початкові роздуми, безумовно, дорогі, але якщо ви збираєтесь використовувати його багато разів, що, на мою думку, ви хочете, це, безумовно, краще рішення того, що ви пропонуєте. Мені не подобається використовувати рефлексію, але я вважаю, що використовую це, коли мені не подобається альтернатива рефлексії. Я думаю, що це допоможе врятувати вашу команду від великого головного болю, але ви повинні ввести назву методу (з малих літер).

Іншими словами, замість того, щоб передати "ім'я", ви б передали "повне ім'я", тому що ім'я методу 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;
}

Якщо вам потрібно отримати доступ до даних, що містяться в членах контакту, ви можете розглянути можливість створення класу обгортки для контакту, який має всі способи доступу до будь-якої необхідної інформації. Це також було б корисно для того, щоб гарантувати, що імена полів доступу завжди залишатимуться однаковими (тобто, якщо клас обгортки має getFullName (), а ви зателефонуєте з повним ім'ям, воно завжди буде працювати, навіть якщо перейменований getFullName () контакту - це призведе до помилки компіляції, перш ніж це дозволить вам це зробити).

public class ContactWrapper {
    private Contact contact;

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

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

Це рішення кілька разів врятувало мене, а саме тоді, коли я хотів мати єдине представлення даних для використання у jsf-таблицях даних і коли ці дані потрібно було експортувати у звіт за допомогою яшми (що, на моєму досвіді, не справляється зі складними об'єктами) .


Мені подобається ідея обгорткового об'єкта методами, що називаються через .invoke(), тому що він повністю усуває рядкові константи. Я не так захоплююсь відображенням часу виконання, щоб створити карту, хоча, можливо, виконання getMethodMapping()в staticблоці було б добре, щоб це сталося під час запуску замість того, як тільки система працює.
гат

@gutch, візерунок обгортки - це те, що я часто використовую, оскільки він, як правило, вирішує багато проблем, пов'язаних з інтерфейсом / контролером. Інтерфейс завжди може використовувати обгортку і радувати її, а контролер тим часом можна вивернути назовні. Все, що вам потрібно знати, - це те, які дані ви бажаєте отримати в інтерфейсі. І знову ж таки, я кажу наголос, я зазвичай не люблю роздуми, але якщо це веб-додаток, це цілком прийнятно, якщо ви робите це під час запуску, оскільки клієнт не бачить жодного з цього часу очікування.
Ніл

@Neil Чому б не використовувати BeanUtils від спільноти Apache? Він також підтримує вбудовані об'єкти. Ви можете пройти всю структуру даних obj.attrA.attrB.attrN, і у неї є багато інших можливостей :-)
Laiv

Замість відображень із Maps, я б пішов на @Annotations. Щось схоже на JPA. Визначити власну Анотацію для відображення записів контролера (рядка) з конкретним attr або getter. Працювати з Анотацією досить просто, і це доступно на Java 1.6 (я думаю)
Laiv

5

Якщо взагалі можливо, використовуйте Java 7, що дозволяє використовувати Strings у switchоператорах.

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

Я не вимірював, але вважаю, що заяви переключення складаються в таблицю стрибків замість довгого списку порівнянь. Це має бути ще швидше.

Що стосується вашого актуального питання: Якщо ви використовуєте його лише один раз, вам не потрібно перетворювати його на постійне значення. Однак врахуйте, що константа може бути задокументована і відображена в Javadoc. Це може бути важливо для нетривіальних значень рядків.


2
Щодо таблиці стрибків. Перемикач String замінюється на перемикачі, спочатку базується на хеш-коді (рівність перевіряється на всі константи з однаковим хеш-кодом) і вибирає індекс гілки, другий перемикає на індекс гілки та вибирає вихідний код гілки. Остання явно підходить для таблиці гілок, перша не пов'язана з розподілом хеш-функції. Тож будь-яка перевага у виконанні, ймовірно, пов'язана з реалізацією на основі хешу.
хустка

Дуже хороший момент; якщо це добре, можливо, варто перейти на Java 7 саме для цього ...
gutch

4

Якщо ви збираєтеся підтримувати це (будь-які нетривіальні зміни будь-коли), я можу насправді використати або якесь генерування коду, орієнтованого на анотацію (можливо, через CGLib ), або навіть просто сценарій, який записує весь код для вас. Уявіть кількість помилок і помилок, які можуть повстати з підходом, який ви розглядаєте ...


Ми розглядали анотування існуючих методів, але деякі карти відображають декілька об'єктів (наприклад, "країна" object.getAddress().getCountry()), які важко представити за допомогою анотацій. Порівняння рядків if / else не дуже, але вони швидкі, гнучкі, прості для розуміння і прості в одиниці тестування.
ґут

1
Ви маєте рацію щодо можливості помилок та помилок; моя єдина захист - це тестові одиниці. Звичайно, це означає, що ще більше коду ...
gutch

2

Я б все-таки використовував константи, визначені у верхній частині ваших класів. Це робить ваш код більш ретельним, оскільки легше зрозуміти, що можна змінити пізніше (якщо потрібно). Наприклад, "first_name"може стати "firstName"в більш пізній час.


Я погоджуюся, однак, якщо цей код буде генеруватися автоматично, а константи не використовуються в іншому місці, то це не має значення (ОП говорить, що це потрібно робити в 100 класах).
NoChance

5
Я просто не бачу кута "ремонтопридатності", тут ви змінюєте "first_name" на "givenName" один раз в одному місці в будь-якому випадку. Однак у випадку з названими константами вам залишається безладна змінна "first_name", яка посилається на рядок "GivenName", тому ви, мабуть, хочете змінити це так, щоб у вас було три зміни в двох місцях
Джеймс Андерсон

1
При правильному IDE ці зміни тривіальні. Що я закликаю, це те, що більш очевидно, де зробити ці зміни, тому що ви витратили час для оголошення констант у верхній частині класу і не потрібно читати решту коду в класі, щоб внести ці зміни.
Бернар

Але коли ви читаєте оператор if, вам доведеться повернутися назад і перевірити, чи константа містить рядок, який, на вашу думку, містить - тут нічого не збережено.
Джеймс Андерсон

1
Можливо, але саме тому я добре називаю свої константи.
Бернард

1

Якщо ваше іменування є послідовним (ака "some_whatever"завжди відображається getSomeWhatever()), ви можете використовувати відображення для визначення та виконання методу get.


Краще getSome_wever (). Можливо, це буде ламання справи верблюда, але набагато важливіше переконатися, що відображення працює. Плюс у цьому є додаткова перевага, що змушує вас сказати: "Чому, чорт, ми це зробили саме так ... о, зачекайте! Зачекайте! Джордж не змінює ім'я цього методу!"
Ніл

0

Гадаю, обробка анотацій може бути вирішенням навіть без анотацій. Це річ, яка може створити для вас весь нудний код. Мінус полягає в тому, що ви отримаєте N генерованих класів для N модельних класів. Ви також не можете нічого додати до наявного класу, але написати щось подібне

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

один раз на заняття не повинно бути проблемою. Як варіант, ви можете написати щось подібне

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

у загальному суперкласі.


Ви можете використовувати відображення замість обробки анотацій для створення коду. Мінус полягає в тому, що вам потрібно зібрати свій код, перш ніж ви зможете використовувати його. Це означає, що ви не можете покладатися на згенерований код у своїх модельних класах, якщо ви не генеруєте деякі заглушки.


Я також розглядаю можливість прямого використання рефлексії. Звичайно, рефлексія повільна, але чому вона повільна? Це тому, що він повинен робити все, що вам потрібно зробити, наприклад, увімкнути ім'я поля.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.