Как писать код так, чтобы он рос в высоту, а не в ширину
от aNNiMON
Каждый раз, когда мы добавляем в код новый if или for, приходится увеличивать отступ тела условия или цикла. Чем больше вложенных циклов и условий, тем шире становится код и начинаются трудности при его чтении. В этой небольшой заметке я покажу несколько приёмов благодаря которым можно избежать роста отступов, чтобы код был понятнее и чтобы в нём никогда не появлялась горизонтальная прокрутка.
Правило 0. Не слушай меня, слушай свой style guide
Прежде всего напомню о самом важном — стиле написания кода. Если вы работаете не один, а в команде, то вам важно писать код так, чтобы не переключаться со стиля на стиль при чтении кода (помните, над одним файлом может работать множество разработчиков). Поэтому вы должны согласовать стиль написания кода: как именовать переменные, сколько пробелов (или табов) в отступах, где ставить фигурные скобки и так далее. Если вы пишете код один, у вас всё равно может использоваться стиль кода, определённый создателями языка программирования или сообществом. Придерживайтесь его.
Правило 1. Ставьте else как можно ближе к своему if
Иногда листаешь код, глазами перескакиваешь с одного метода на другой, прокручиваешь и видишь это:
И вроде всё ничего, но стоит глазам опуститься к строке "} else {", впадаешь в ступор и задаёшься вопросом: если есть else, то каким был if? И приходится прокручивать код вверх, чтобы понять, что перед тобой это:
Можно инвертировать условие и поменять местами тело блока if и else так, чтобы тело if было короче тела else. Это избавит нас от висячих else, ступоров и прокруток вверх.
Правило 2. if/else можно заменить на if-return
В предыдущем коде можно уменьшить отступ блока else, если в конце блока if поставить return:
В данном случае return ничего не сломает, напротив, избавляет от лишней вложенности. Конечно, кому-то может быть неочевидно, что это на самом деле if/else, кто-то может быть против такого кода, считая, что у метода должен быть один вход и один выход. И будут правы, поэтому решение должен диктовать ваш style guide.
Этот же способ работает и с if/else if/else:
Хотя в данном случае отступ остаётся прежним, но зато однотипные блоки визуально лучше выделяются.
Правило 3. Внутри циклов if/else можно заменить на if-continue
Внутри циклов для удаления else следует использовать не return, а continue. Было так:
В конце тела ветки if добавляем continue и получаем работающий код с меньшим отступом:
Правило 4. Без надобности делать замену не стоит
Если код и так понятен, лучше ничего не трогать и оставить как есть, например:
не стоит заменять на:
Так читабельность только ухудшается.
Правило 5. Если уровень вложенности превышает допустимую отметку, вводите новый метод
Если случается так, что внутри одного метода так много вложенных циклов и условий, что код уехал вправо из-за отступов и вот-вот появится горизонтальная прокрутка, значит пора делать рефакторинг и заменять какую-либо часть метода другим методом. Например:
Даже не пытайтесь понять этот код, только потратите время. его я просто придумал для примера. Рефакторинг — тема отдельной статьи, поэтому поэтапно рассматривать этот процесс не стану. Суть именно в изменениях:
Применив перечисленные правила, мы сделали код менее широким, но более длинным. Это нормально, потому что лучше прокручивать код вверх-вниз, чем вверх-вниз и вправо-влево, да и большой уровень вложенности вместе с фигурными скобками тоже мешает пониманию.
Также код стал более понятным за счёт методов с содержательными именами, но это уже заслуга рефакторинга.
Правило 0. Не слушай меня, слушай свой style guide
Прежде всего напомню о самом важном — стиле написания кода. Если вы работаете не один, а в команде, то вам важно писать код так, чтобы не переключаться со стиля на стиль при чтении кода (помните, над одним файлом может работать множество разработчиков). Поэтому вы должны согласовать стиль написания кода: как именовать переменные, сколько пробелов (или табов) в отступах, где ставить фигурные скобки и так далее. Если вы пишете код один, у вас всё равно может использоваться стиль кода, определённый создателями языка программирования или сообществом. Придерживайтесь его.
Правило 1. Ставьте else как можно ближе к своему if
Иногда листаешь код, глазами перескакиваешь с одного метода на другой, прокручиваешь и видишь это:
- // ещё 20 строк перед этим
- String[] params = text.split(" ");
- members.clear()
- members.add(params[1]);
- members.add(params[2]);
- tournamentEnabled = true;
- markup = new Markup();
- rows = List.of(members);
- markup.setRows(rows);
- markup.setAction("tournament");
- status = Message.create()
- .withText("Let's go!")
- .withMarkup(markup)
- .toChat(chatId)
- .send();
- validateStatus(status);
- } else {
- Log.error("Empty command");
- }
- void handleMessage(Message message) {
- String text = message.getText();
- if (text != null) {
- // 35 строк кода
- } else {
- Log.error("Empty command");
- }
- }
Можно инвертировать условие и поменять местами тело блока if и else так, чтобы тело if было короче тела else. Это избавит нас от висячих else, ступоров и прокруток вверх.
- void handleMessage(Message message) {
- String text = message.getText();
- if (text == null) {
- Log.error("Empty command");
- } else {
- // ещё 20 строк перед этим
- String[] params = text.split(" ");
- members.clear()
- members.add(params[1]);
- members.add(params[2]);
- tournamentEnabled = true;
- markup = new Markup();
- rows = List.of(members);
- markup.setRows(rows);
- markup.setAction("tournament");
- status = Message.create()
- .withText("Let's go!")
- .withMarkup(markup)
- .toChat(chatId)
- .send();
- validateStatus(status);
- }
- }
Правило 2. if/else можно заменить на if-return
В предыдущем коде можно уменьшить отступ блока else, если в конце блока if поставить return:
- void handleMessage(Message message) {
- String text = message.getText();
- if (text == null) {
- Log.error("Empty command");
- return;
- }
- // ещё 20 строк перед этим
- String[] params = text.split(" ");
- members.clear()
- members.add(params[1]);
- members.add(params[2]);
- tournamentEnabled = true;
- markup = new Markup();
- rows = List.of(members);
- markup.setRows(rows);
- markup.setAction("tournament");
- status = Message.create()
- .withText("Let's go!")
- .withMarkup(markup)
- .toChat(chatId)
- .send();
- validateStatus(status);
- }
В данном случае return ничего не сломает, напротив, избавляет от лишней вложенности. Конечно, кому-то может быть неочевидно, что это на самом деле if/else, кто-то может быть против такого кода, считая, что у метода должен быть один вход и один выход. И будут правы, поэтому решение должен диктовать ваш style guide.
Этот же способ работает и с if/else if/else:
- if (command.startWith("/help")) {
- Message.create()
- .withText("Help message")
- .toChat(chatId)
- .send();
- } else if (command.startWith("/clear")) {
- data.clear();
- Message.create()
- .withText("Cleared!")
- .toChat(chatId)
- .send();
- } else if (command.startWith("/add") && isAdmin) {
- data.add(params[1]);
- sendStatusNessage();
- } else if // ...
- if (command.startWith("/help")) {
- Message.create()
- .withText("Help message")
- .toChat(chatId)
- .send();
- return;
- }
- if (command.startWith("/clear")) {
- data.clear();
- Message.create()
- .withText("Cleared!")
- .toChat(chatId)
- .send();
- return;
- }
- if (command.startWith("/add") && isAdmin) {
- data.add(params[1]);
- sendStatusNessage();
- return;
- }
- // ...
Хотя в данном случае отступ остаётся прежним, но зато однотипные блоки визуально лучше выделяются.
Правило 3. Внутри циклов if/else можно заменить на if-continue
Внутри циклов для удаления else следует использовать не return, а continue. Было так:
- for (Members member : members) {
- if (member.isBlocked()) {
- Log.warning("Member {} is blocked", member);
- tournamentMembers.remove(member);
- if (tournamentMembers.size() > MIN_MEMBERS_COUNT) {
- tournamentEnabled = false;
- }
- } else {
- Member opponent = tournamentMembers.pop();
- tournamentEnabled = true;
- Message.create()
- .withText("Tournament started. Your opponent is {}", opponent)
- .toChat(member.getId())
- .send();
- Message.create()
- .withText("Tournament started. Your opponent is {}", member)
- .toChat(opponent.getId())
- .send();
- Message.create()
- .withText("Tournament started. {} vs {}", member, opponent)
- .toChat(chatId)
- .send();
- }
- }
В конце тела ветки if добавляем continue и получаем работающий код с меньшим отступом:
- for (Members member : members) {
- if (member.isBlocked()) {
- Log.warning("Member {} is blocked", member);
- tournamentMembers.remove(member);
- if (tournamentMembers.size() > MIN_MEMBERS_COUNT) {
- tournamentEnabled = false;
- }
- continue;
- }
- Member opponent = tournamentMembers.pop();
- tournamentEnabled = true;
- Message.create()
- .withText("Tournament started. Your opponent is {}", opponent)
- .toChat(member.getId())
- .send();
- Message.create()
- .withText("Tournament started. Your opponent is {}", member)
- .toChat(opponent.getId())
- .send();
- Message.create()
- .withText("Tournament started. {} vs {}", member, opponent)
- .toChat(chatId)
- .send();
- }
Правило 4. Без надобности делать замену не стоит
Если код и так понятен, лучше ничего не трогать и оставить как есть, например:
- if (chatId == config.tournamentChat()) {
- processTournamentChat(message);
- } else {
- processRegularCommand(message);
- }
- if (chatId == config.tournamentChat()) {
- processTournamentChat(message);
- return;
- }
- processRegularCommand(message);
Так читабельность только ухудшается.
Правило 5. Если уровень вложенности превышает допустимую отметку, вводите новый метод
Если случается так, что внутри одного метода так много вложенных циклов и условий, что код уехал вправо из-за отступов и вот-вот появится горизонтальная прокрутка, значит пора делать рефакторинг и заменять какую-либо часть метода другим методом. Например:
- @Override
- public void processUpdates(List<Update> updates) {
- for (Update update : updates) {
- if (update != null) {
- Message message = update.getMessage();
- // don't process old messages
- long current = System.currentTimeMillis() / 1000;
- if (message.getDate() + 60 >= current) {
- long chatId = message.getChatId();
- if (chatId == config.tournamentChat()) {
- if ("/tournament".equals(message.getText())) {
- for (Members member : members) {
- if (member.isBlocked()) {
- Log.warning("Member {} is blocked", member);
- tournamentMembers.remove(member);
- if (tournamentMembers.size() > MIN_MEMBERS_COUNT) {
- tournamentEnabled = false;
- // 9 уровень вложенности!!!!!
- }
- } else {
- Member opponent = tournamentMembers.pop();
- tournamentEnabled = true;
- Message.create()
- .withText("Tournament started. Your opponent is {}", opponent)
- .toChat(member.getId())
- .send();
- Message.create()
- .withText("Tournament started. Your opponent is {}", member)
- .toChat(opponent.getId())
- .send();
- Message.create()
- .withText("Tournament started. {} vs {}", member, opponent)
- .toChat(chatId)
- .send();
- }
- }
- }
- } else {
- String text = message.getText();
- if (text != null) {
- // ещё 20 строк перед этим
- String[] params = text.split(" ");
- members.clear()
- members.add(params[1]);
- members.add(params[2]);
- tournamentEnabled = true;
- markup = new Markup();
- rows = List.of(members);
- markup.setRows(rows);
- markup.setAction("tournament");
- status = Message.create()
- .withText("Let's go!")
- .withMarkup(markup)
- .toChat(chatId)
- .send();
- validateStatus(status);
- } else {
- Log.error("Empty command");
- }
- }
- }
- }
- }
- }
- // особенно я люблю вот эту стеночку из закрывающих фигурных скобок
Даже не пытайтесь понять этот код, только потратите время. его я просто придумал для примера. Рефакторинг — тема отдельной статьи, поэтому поэтапно рассматривать этот процесс не стану. Суть именно в изменениях:
- @Override
- public void processUpdates(List<Update> updates) {
- for (Update update : updates) {
- processUpdate(update);
- }
- }
- private void processUpdate(Update update) {
- if (update == null) {
- return;
- }
- Message message = update.getMessage();
- // don't process old messages
- long current = System.currentTimeMillis() / 1000;
- if (message.getDate() + 60 < current) {
- return;
- }
- processMessage(message);
- }
- private void processMessage(Message message) {
- long chatId = message.getChatId();
- if (chatId == config.tournamentChat()) {
- processTournamentChat(message);
- } else {
- processRegularCommand(message);
- }
- }
- private void processTournamentChat(Message message) {
- long chatId = message.getChatId();
- if ("/tournament".equals(message.getText())) {
- processTournamentCommand(chatId);
- }
- }
- private void processTournamentCommand(long chatId) {
- for (Members member : members) {
- if (member.isBlocked()) {
- Log.warning("Member {} is blocked", member);
- tournamentMembers.remove(member);
- if (tournamentMembers.size() > MIN_MEMBERS_COUNT) {
- tournamentEnabled = false;
- }
- continue;
- }
- Member opponent = tournamentMembers.pop();
- tournamentEnabled = true;
- Message.create()
- .withText("Tournament started. Your opponent is {}", opponent)
- .toChat(member.getId())
- .send();
- Message.create()
- .withText("Tournament started. Your opponent is {}", member)
- .toChat(opponent.getId())
- .send();
- Message.create()
- .withText("Tournament started. {} vs {}", member, opponent)
- .toChat(chatId)
- .send();
- }
- }
- private void processRegularCommand(Message message) {
- long chatId = message.getChatId();
- String text = message.getText();
- if (text == null) {
- Log.error("Empty command");
- return;
- }
- // ещё 20 строк перед этим
- String[] params = text.split(" ");
- members.clear()
- members.add(params[1]);
- members.add(params[2]);
- tournamentEnabled = true;
- markup = new Markup();
- rows = List.of(members);
- markup.setRows(rows);
- markup.setAction("tournament");
- status = Message.create()
- .withText("Let's go!")
- .withMarkup(markup)
- .toChat(chatId)
- .send();
- validateStatus(status);
- }
Применив перечисленные правила, мы сделали код менее широким, но более длинным. Это нормально, потому что лучше прокручивать код вверх-вниз, чем вверх-вниз и вправо-влево, да и большой уровень вложенности вместе с фигурными скобками тоже мешает пониманию.
Также код стал более понятным за счёт методов с содержательными именами, но это уже заслуга рефакторинга.