Как писать код так, чтобы он рос в высоту, а не в ширину

от
Совершенный код    style guide, стиль кода, java, отступы

Каждый раз, когда мы добавляем в код новый if или for, приходится увеличивать отступ тела условия или цикла. Чем больше вложенных циклов и условий, тем шире становится код и начинаются трудности при его чтении. В этой небольшой заметке я покажу несколько приёмов благодаря которым можно избежать роста отступов, чтобы код был понятнее и чтобы в нём никогда не появлялась горизонтальная прокрутка.
2018-09-11_12-29-14.png
Правило 0. Не слушай меня, слушай свой style guide
Прежде всего напомню о самом важном — стиле написания кода. Если вы работаете не один, а в команде, то вам важно писать код так, чтобы не переключаться со стиля на стиль при чтении кода (помните, над одним файлом может работать множество разработчиков). Поэтому вы должны согласовать стиль написания кода: как именовать переменные, сколько пробелов (или табов) в отступах, где ставить фигурные скобки и так далее. Если вы пишете код один, у вас всё равно может использоваться стиль кода, определённый создателями языка программирования или сообществом. Придерживайтесь его.


Правило 1. Ставьте else как можно ближе к своему if
Иногда листаешь код, глазами перескакиваешь с одного метода на другой, прокручиваешь и видишь это:
  1.     // ещё 20 строк перед этим
  2.     String[] params = text.split(" ");
  3.     members.clear()
  4.     members.add(params[1]);
  5.     members.add(params[2]);
  6.     tournamentEnabled = true;
  7.     markup = new Markup();
  8.     rows = List.of(members);
  9.     markup.setRows(rows);
  10.     markup.setAction("tournament");
  11.     status = Message.create()
  12.         .withText("Let's go!")
  13.         .withMarkup(markup)
  14.         .toChat(chatId)
  15.         .send();
  16.     validateStatus(status);
  17. } else {
  18.     Log.error("Empty command");
  19. }
И вроде всё ничего, но стоит глазам опуститься к строке "} else {", впадаешь в ступор и задаёшься вопросом: если есть else, то каким был if? И приходится прокручивать код вверх, чтобы понять, что перед тобой это:
  1. void handleMessage(Message message) {
  2.     String text = message.getText();
  3.     if (text != null) {
  4.         // 35 строк кода
  5.     } else {
  6.         Log.error("Empty command");
  7.     }
  8. }

Можно инвертировать условие и поменять местами тело блока if и else так, чтобы тело if было короче тела else. Это избавит нас от висячих else, ступоров и прокруток вверх.
  1. void handleMessage(Message message) {
  2.     String text = message.getText();
  3.     if (text == null) {
  4.         Log.error("Empty command");
  5.     } else {
  6.         // ещё 20 строк перед этим
  7.         String[] params = text.split(" ");
  8.         members.clear()
  9.         members.add(params[1]);
  10.         members.add(params[2]);
  11.         tournamentEnabled = true;
  12.         markup = new Markup();
  13.         rows = List.of(members);
  14.         markup.setRows(rows);
  15.         markup.setAction("tournament");
  16.         status = Message.create()
  17.             .withText("Let's go!")
  18.             .withMarkup(markup)
  19.             .toChat(chatId)
  20.             .send();
  21.         validateStatus(status);
  22.     }
  23. }


Правило 2. if/else можно заменить на if-return
В предыдущем коде можно уменьшить отступ блока else, если в конце блока if поставить return:
  1. void handleMessage(Message message) {
  2.     String text = message.getText();
  3.     if (text == null) {
  4.         Log.error("Empty command");
  5.         return;
  6.     }
  7.     // ещё 20 строк перед этим
  8.     String[] params = text.split(" ");
  9.     members.clear()
  10.     members.add(params[1]);
  11.     members.add(params[2]);
  12.     tournamentEnabled = true;
  13.     markup = new Markup();
  14.     rows = List.of(members);
  15.     markup.setRows(rows);
  16.     markup.setAction("tournament");
  17.     status = Message.create()
  18.         .withText("Let's go!")
  19.         .withMarkup(markup)
  20.         .toChat(chatId)
  21.         .send();
  22.     validateStatus(status);
  23. }

В данном случае return ничего не сломает, напротив, избавляет от лишней вложенности. Конечно, кому-то может быть неочевидно, что это на самом деле if/else, кто-то может быть против такого кода, считая, что у метода должен быть один вход и один выход. И будут правы, поэтому решение должен диктовать ваш style guide.

Этот же способ работает и с if/else if/else:
  1. if (command.startWith("/help")) {
  2.     Message.create()
  3.         .withText("Help message")
  4.         .toChat(chatId)
  5.         .send();
  6. } else if (command.startWith("/clear")) {
  7.     data.clear();
  8.     Message.create()
  9.         .withText("Cleared!")
  10.         .toChat(chatId)
  11.         .send();
  12. } else if (command.startWith("/add") && isAdmin) {
  13.     data.add(params[1]);
  14.     sendStatusNessage();
  15. } else if // ...

  1. if (command.startWith("/help")) {
  2.     Message.create()
  3.         .withText("Help message")
  4.         .toChat(chatId)
  5.         .send();
  6.     return;
  7. }
  8. if (command.startWith("/clear")) {
  9.     data.clear();
  10.     Message.create()
  11.         .withText("Cleared!")
  12.         .toChat(chatId)
  13.         .send();
  14.     return;
  15. }
  16. if (command.startWith("/add") && isAdmin) {
  17.     data.add(params[1]);
  18.     sendStatusNessage();
  19.     return;
  20. }
  21. // ...

Хотя в данном случае отступ остаётся прежним, но зато однотипные блоки визуально лучше выделяются.


Правило 3. Внутри циклов if/else можно заменить на if-continue
Внутри циклов для удаления else следует использовать не return, а continue. Было так:
  1. for (Members member : members) {
  2.     if (member.isBlocked()) {
  3.         Log.warning("Member {} is blocked", member);
  4.         tournamentMembers.remove(member);
  5.         if (tournamentMembers.size() > MIN_MEMBERS_COUNT) {
  6.             tournamentEnabled = false;
  7.         }
  8.     } else {
  9.         Member opponent = tournamentMembers.pop();
  10.         tournamentEnabled = true;
  11.         Message.create()
  12.             .withText("Tournament started. Your opponent is {}", opponent)
  13.             .toChat(member.getId())
  14.             .send();
  15.         Message.create()
  16.             .withText("Tournament started. Your opponent is {}", member)
  17.             .toChat(opponent.getId())
  18.             .send();
  19.         Message.create()
  20.             .withText("Tournament started. {} vs {}", member, opponent)
  21.             .toChat(chatId)
  22.             .send();
  23.     }
  24. }

В конце тела ветки if добавляем continue и получаем работающий код с меньшим отступом:
  1. for (Members member : members) {
  2.     if (member.isBlocked()) {
  3.         Log.warning("Member {} is blocked", member);
  4.         tournamentMembers.remove(member);
  5.         if (tournamentMembers.size() > MIN_MEMBERS_COUNT) {
  6.             tournamentEnabled = false;
  7.         }
  8.         continue;
  9.     }
  10.  
  11.     Member opponent = tournamentMembers.pop();
  12.     tournamentEnabled = true;
  13.     Message.create()
  14.         .withText("Tournament started. Your opponent is {}", opponent)
  15.         .toChat(member.getId())
  16.         .send();
  17.     Message.create()
  18.         .withText("Tournament started. Your opponent is {}", member)
  19.         .toChat(opponent.getId())
  20.         .send();
  21.     Message.create()
  22.         .withText("Tournament started. {} vs {}", member, opponent)
  23.         .toChat(chatId)
  24.         .send();
  25. }


Правило 4. Без надобности делать замену не стоит
Если код и так понятен, лучше ничего не трогать и оставить как есть, например:
  1. if (chatId == config.tournamentChat()) {
  2.     processTournamentChat(message);
  3. } else {
  4.     processRegularCommand(message);
  5. }
не стоит заменять на:
  1. if (chatId == config.tournamentChat()) {
  2.     processTournamentChat(message);
  3.     return;
  4. }
  5. processRegularCommand(message);

Так читабельность только ухудшается.


Правило 5. Если уровень вложенности превышает допустимую отметку, вводите новый метод
Если случается так, что внутри одного метода так много вложенных циклов и условий, что код уехал вправо из-за отступов и вот-вот появится горизонтальная прокрутка, значит пора делать рефакторинг и заменять какую-либо часть метода другим методом. Например:
  1. @Override
  2. public void processUpdates(List<Update> updates) {
  3.     for (Update update : updates) {
  4.         if (update != null) {
  5.             Message message = update.getMessage();
  6.             // don't process old messages
  7.             long current = System.currentTimeMillis() / 1000;
  8.             if (message.getDate() + 60 >= current) {
  9.                 long chatId = message.getChatId();
  10.                 if (chatId == config.tournamentChat()) {
  11.                     if ("/tournament".equals(message.getText())) {
  12.                         for (Members member : members) {
  13.                             if (member.isBlocked()) {
  14.                                 Log.warning("Member {} is blocked", member);
  15.                                 tournamentMembers.remove(member);
  16.                                 if (tournamentMembers.size() > MIN_MEMBERS_COUNT) {
  17.                                     tournamentEnabled = false;
  18.                                     // 9 уровень вложенности!!!!!
  19.                                 }
  20.                             } else {
  21.                                 Member opponent = tournamentMembers.pop();
  22.                                 tournamentEnabled = true;
  23.                                 Message.create()
  24.                                     .withText("Tournament started. Your opponent is {}", opponent)
  25.                                     .toChat(member.getId())
  26.                                     .send();
  27.                                 Message.create()
  28.                                     .withText("Tournament started. Your opponent is {}", member)
  29.                                     .toChat(opponent.getId())
  30.                                     .send();
  31.                                 Message.create()
  32.                                     .withText("Tournament started. {} vs {}", member, opponent)
  33.                                     .toChat(chatId)
  34.                                     .send();
  35.                             }
  36.                         }
  37.                     }
  38.                 } else {
  39.                     String text = message.getText();
  40.                     if (text != null) {
  41.                         // ещё 20 строк перед этим
  42.                         String[] params = text.split(" ");
  43.                         members.clear()
  44.                         members.add(params[1]);
  45.                         members.add(params[2]);
  46.                         tournamentEnabled = true;
  47.                         markup = new Markup();
  48.                         rows = List.of(members);
  49.                         markup.setRows(rows);
  50.                         markup.setAction("tournament");
  51.                         status = Message.create()
  52.                             .withText("Let's go!")
  53.                             .withMarkup(markup)
  54.                             .toChat(chatId)
  55.                             .send();
  56.                         validateStatus(status);
  57.                     } else {
  58.                         Log.error("Empty command");
  59.                     }
  60.                 }
  61.             }
  62.         }
  63.     }
  64. }
  65. // особенно я люблю вот эту стеночку из закрывающих фигурных скобок

Даже не пытайтесь понять этот код, только потратите время. его я просто придумал для примера. Рефакторинг — тема отдельной статьи, поэтому поэтапно рассматривать этот процесс не стану. Суть именно в изменениях:
  1. @Override
  2. public void processUpdates(List<Update> updates) {
  3.     for (Update update : updates) {
  4.         processUpdate(update);
  5.     }
  6. }
  7.  
  8. private void processUpdate(Update update) {
  9.     if (update == null) {
  10.         return;
  11.     }
  12.     Message message = update.getMessage();
  13.     // don't process old messages
  14.     long current = System.currentTimeMillis() / 1000;
  15.     if (message.getDate() + 60 < current) {
  16.         return;
  17.     }
  18.     processMessage(message);
  19. }
  20.  
  21. private void processMessage(Message message) {
  22.     long chatId = message.getChatId();
  23.     if (chatId == config.tournamentChat()) {
  24.         processTournamentChat(message);
  25.     } else {
  26.         processRegularCommand(message);
  27.     }
  28. }
  29.  
  30. private void processTournamentChat(Message message) {
  31.     long chatId = message.getChatId();
  32.     if ("/tournament".equals(message.getText())) {
  33.         processTournamentCommand(chatId);
  34.     }
  35. }
  36.  
  37. private void processTournamentCommand(long chatId) {
  38.     for (Members member : members) {
  39.         if (member.isBlocked()) {
  40.             Log.warning("Member {} is blocked", member);
  41.             tournamentMembers.remove(member);
  42.             if (tournamentMembers.size() > MIN_MEMBERS_COUNT) {
  43.                 tournamentEnabled = false;
  44.             }
  45.             continue;
  46.         }
  47.  
  48.         Member opponent = tournamentMembers.pop();
  49.         tournamentEnabled = true;
  50.         Message.create()
  51.             .withText("Tournament started. Your opponent is {}", opponent)
  52.             .toChat(member.getId())
  53.             .send();
  54.         Message.create()
  55.             .withText("Tournament started. Your opponent is {}", member)
  56.             .toChat(opponent.getId())
  57.             .send();
  58.         Message.create()
  59.             .withText("Tournament started. {} vs {}", member, opponent)
  60.             .toChat(chatId)
  61.             .send();
  62.     }
  63. }
  64.  
  65. private void processRegularCommand(Message message) {
  66.     long chatId = message.getChatId();
  67.     String text = message.getText();
  68.     if (text == null) {
  69.         Log.error("Empty command");
  70.         return;
  71.     }
  72.  
  73.     // ещё 20 строк перед этим
  74.     String[] params = text.split(" ");
  75.     members.clear()
  76.     members.add(params[1]);
  77.     members.add(params[2]);
  78.     tournamentEnabled = true;
  79.     markup = new Markup();
  80.     rows = List.of(members);
  81.     markup.setRows(rows);
  82.     markup.setAction("tournament");
  83.     status = Message.create()
  84.         .withText("Let's go!")
  85.         .withMarkup(markup)
  86.         .toChat(chatId)
  87.         .send();
  88.     validateStatus(status);
  89. }

Применив перечисленные правила, мы сделали код менее широким, но более длинным. Это нормально, потому что лучше прокручивать код вверх-вниз, чем вверх-вниз и вправо-влево, да и большой уровень вложенности вместе с фигурными скобками тоже мешает пониманию.
Также код стал более понятным за счёт методов с содержательными именами, но это уже заслуга рефакторинга.
  • +16
  • views 4528