Естетична краса: Switch vs If

Вступна
Як розробники, ми кожен день стикаємося з кодом і ніж більше того, який припадає нам до душі, ми бачимо, пишемо, тим більшим ентузіазмом переймаємося, тим більш продуктивними та ефективними стаємо. Та що там говорити, ми просто пишаємося нашим кодом. Але одна дилема не дає мені спокою: коли 2 розробника дивляться на один і той же код вони можуть відчувати абсолютно протилежні почуття. І що робити, якщо ці почуття, емоції, навіяні його естетичною красою, не збігається з емоціями більшості оточуючих Вас професіоналів? Загалом, історія про те, чому може не подобатися мовна конструкція switch на стільки, що волієш if. Кому цікава ця холиварная позиція ласкаво просимо під кат.

Трохи нюансів
Мова нижче піде про порівняння оператора switch і його конкретної реалізації на мові Java і операторами if у вигляді опонентів цієї конструкції. Про те, що в оператора switch є туз у рукаві — продуктивність (за рідкісним винятком), мабуть, знає кожен і цей момент розглядатися не буде — так як крити його нічим. Але все ж, що не так?

1. Багатослівність і громіздкість
Серед причин, які відштовхують мене від використання оператора switch — це багатослівність і громіздкість самої конструкції, тільки погляньте — switch, case, break і default, причому опускаю за дужки, що між ними, напевно, ще зустрінеться return і throw. Не подумайте, що закликаю не використовувати службові слова мови java або уникати їх великої кількості в коді, немає, лише вважаю, що прості речі не повинні бути багатослівними. Ось невеликий приклад того, про що кажу:

public List < String> getTypes1(Channel channel) {
switch (channel) {
case INTERNET:
return Arrays.asList("SMS", "MAC");
case HOME_PHONE:
case DEVICE_PHONE:
return Arrays.asList("CHECK_LIST");
default:
throw new IllegalArgumentException("Непідтримуваний канал зв'язку " + channel);
}
}

варіант з if

public List < String> getTypes2(Channel channel) {
if (INTERNET == channel) {
return Arrays.asList("SMS", "MAC");
}
if (HOME_PHONE == channel || DEVICE_PHONE == channel) {
return Arrays.asList("CHECK_LIST");
}
throw new IllegalArgumentException("Непідтримуваний канал зв'язку " + channel);
}

Разом для мене: switch/case/default VS if.

Якщо запитати, кому який подобається більше-код, то більшість віддасть перевагу 1ому варіанту, в той час як по мені, він — багатослівний. Мова про рефакторинге коду тут не йде, всі ми знаємо, що можна було б використовувати константу типу EnumMap або IdentityHashMap для пошуку списку по ключу channel або взагалі прибрати потрібні дані в сам Channel, хоча це рішення дискусійне. Але повернемося.

2. Відступи
Можливо в академічних прикладах використання оператора swicth — це єдина частина коду, але той код, з яким доводиться стикатися, підтримувати — це більш складний, оброслий перевірками, хаками, де-то просто предметної складністю код. І особисто мене, кожен відступ в такому місці напружує. Звернемося до 'живого' наприклад (прибрав зайве, але суть залишилася).

public static List<String> getReference1(Request request, DataSource ds) {
switch (request.getType()) {
case "enum_ref":
return EnumRefLoader.getReference(ds);
case "client_ref":
case "any_client_ref":
switch (request.getName()) {
case "client_types":
return ClientRefLoader.getClientTypes(ds);
case "mailboxes":
return MailboxesRefLoader.getMailboxes(ds);
default:
return ClientRefLoader.getClientReference(request.getName(), ds);
}
case "simple_ref":
return ReferenceLoader.getReference(request, ds);
}
throw new IllegalStateException("Непідтримуваний тип довідника: " + request.getType());
}

варіант з if

public static List<String> getReference2(Request request, DataSource ds) {
if ("enum_ref".equals(request.getType())) {
return EnumRefLoader.getReference(ds);
}
if ("simple_ref".equals(request.getType())) {
return ReferenceLoader.getReference(request, ds);
}
boolean selectByName = "client_ref".equals(request.getType()) || "any_client_ref".equals(request.getType());
if (!selectByName) {
throw new IllegalStateException("Непідтримуваний тип довідника: " + request.getType());
}
if ("client_types".equals(request.getName())) {
return ClientRefLoader.getClientTypes(ds);
}
if ("mailboxes".equals(request.getName())) {
return MailboxesRefLoader.getMailboxes(ds);
}
return ClientRefLoader.getClientReference(request.getName(), ds);
}

Разом для мене: 5 відступів VS 2.

Але знову, кому який подобається варіант? Більшість віддасть перевагу getReference1.
Окремо варто відзначити, що кількість відступів ще залежить від обраного стилю форматування коду.

3. Перевірка null
Якщо використовується оператор switch з рядками або енумами параметр вибору необхідно перевіряти на значення null. Повернемося до getTypes прикладів.

// switch: перевірка на null потрібна
public List < String> getTypes1(Channel channel) {
// Перевірка на null
if (channel == null) {
throw new IllegalArgumentException("Канал зв'язку повинен бути заданий");
}
switch (channel) {
case INTERNET:
return Arrays.asList("SMS", "MAC");
case HOME_PHONE:
case DEVICE_PHONE:
return Arrays.asList("CHECK_LIST");
default:
throw new IllegalArgumentException("Непідтримуваний канал зв'язку " + channel);
}
}
// if: можна обійтися без перевірки на значення null
public List < String> getTypes2(Channel channel) {
if (INTERNET == channel) {
return Arrays.asList("SMS", "MAC");
}
if (HOME_PHONE == channel || DEVICE_PHONE == channel) {
return Arrays.asList("CHECK_LIST");
}
throw new IllegalArgumentException("Непідтримуваний канал зв'язку " + channel);
}

Разом для мене: зайвий код.

Навіть якщо Ви абсолютно впевнені у тому, що null 'не прийде', це абсолютно не означає, що так буде завжди. Я проаналізував корпоративний багтрэк і знайшов цим твердженням підтвердження. Справедливості заради варто відзначити, що структура коду виражена через if не позбавлена цієї проблеми, часто константи для порівняння використовуються праворуч, а не ліворуч, наприклад, name.equals(«John»), замість «John».equals(name). Але в рамках даної статті, щодо цього пункту, хотів сказати, що при інших рівних, підхід з switch роздувається перевіркою на null if ж перевірка не потрібна. Додам ще, що статичні аналізатори коду легко справляються з можливими null-багами.

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

public static void doSomeWork(Channel channel, String cond) {
Logger log = getLogger();
//...
switch (channel) {
//...
case INTERNET:
// З часом з'являється така перевірка
if ("fix price".equals(cond)) {
// ...
log.info("Your tariff");
return;
}
// Ще під вибір?
// ...
break;
//...
}
//...
}

Разом для мене: різний стиль.

Раніше був 'чистенький' switch, а тепер switch + if. Відбувається, як я називаю, змішання стилів, частина коду 'вибрати' виражена через switch, частина через if. Зрозуміло, ніхто не забороняє використовувати if і switch разом, якщо це не стосується операції вибору/під вибору, як у наведеному прикладі.

5. Чий break?
При використанні оператора switch, case блоках може з'явитися цикл або на оборот, в циклі — switch, зі своїми перериваннями процесу обробки. Питання, чий break, панове?

public static List<String> whoseBreak(List<String> states) {
List<String> results = new ArrayList<>();
for (String state : states) {
Result result = process(state);
switch (result.getCode()) {
case "OK":
if (result.hasId()) {
results.add(result.getId());
// Надуманий випадок, але break чий-то?
break;
}
if (result.getInnerMessage() != null) {
results.add(result.getInnerMessage());
// Ось це поворот
continue;
}
// ...
break;
case "NOTHING":
results.add("SKIP");
break;
case "ERROR":
results.add(result.getErroMessage());
break;
default:
throw new IllegalArgumentException("Невірний код: " + result.getCode());
}
}
return results;
}

Разом для мене: читаність коду знижується.

Якщо чесно, зустрічалися і куди більш заплутані приклади коду.

6. Недоречність
В java 7 з'явилася можливість використовувати оператор switch з рядками. Коли наша компанія перейшла на java 7 — це був справжній Switch-Бум. Може з цього, а може і з іншої причини, але в багатьох проектах зустрічаються схожі заготовки:

public String resolveType(String type) {
switch (type) {
case "Java 7?":
return "Ага";
default:
throw new IllegalArgumentException("Люблю switch, шкода, що немає такого варіанту з case " + type);
}
}

Разом для мене: з'являються недоречні конструкції.

7. Гламурний switch під акомпанемент хардкода
Трохи гумору не завадить.

public class Hit {

public static enum Variant {
ZERO ONE, TWO, THREE
}

public static void switchBanter(Variant variant) {
int shift = 0;
ZERO: ONE: while (variant != null) {
shift++;
switch (variant) {
default: {
THREE: {
System.out.println("default");
break THREE;
}
break;
}
case ONE: {
TWO: {
THREE: for (int index = shift; index <= 4; index++) {
System.out.println("one");
switch (index) {
case 1: continue ONE;
case 2: break TWO;
case 3: continue THREE;
case 4: break ZERO;
}
}
}
continue ONE;
}
case TWO: {
TWO: {
System.out.println("two");
if (variant == THREE) {
continue;
}
break TWO;
}
break ZERO;
}
}
variant = null;
}
}

public static void main(String[] args) {
switchBanter(ONE);
}
}

Без коментарів.

Висновок
Я не закликаю Вас відмовлятися від оператора switch, місцями він дійсно гарний собою і локшина із if/if-else/else та ще каша. Але черезмерное його використання, де заманеться, може викликати невдоволення в інших розробників. І я один з них.

Окремо хотілося б відзначити, що з точки зору розуміння коду, у мене немає ніяких проблем з switch/case — сенс написаного ясний, але от з точки зору сприйняття естетичної краси — є.

І наостанок. Використовуйте те, що Вам доводиться по душі, опускаючи за дужки нав'язані думки, головне, щоб Ваш код був простим робітником, красивим і надійним. Всього доброго.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.