Каждый раз, когда мы добавляем новый if или for в код, мы должны увеличивать отступ тела условия или цикла. Чем больше вложенных циклов и условий, тем шире становится ваш код и тем труднее его читать. В этой короткой статье я покажу несколько приемов, с помощью которых можно избежать увеличения отступов. Ваш код будет более понятным, и вы никогда не увидите горизонтальной прокрутки.

Правило 0. Не слушай меня, слушай свое руководство по стилю.

Прежде всего, напомню о самом главном - стиле кода. Если вы работаете в команде, важно писать код так, чтобы вы не переключались от стиля к стилю при его чтении (помните, что многие разработчики могут работать с одним и тем же файлом). Поэтому следует согласовать стиль кодирования: как называть переменные, каков размер отступа в пробелах или табуляциях, где ставить фигурные скобки и так далее. Если вы пишете код в одиночку, вы все равно можете использовать стиль кодирования, определенный создателями языка программирования или сообществом. Придерживаться его.

Правило 1. Поместите else как можно ближе к if блоку

Иногда вы пролистываете код, переходите от одного метода к другому и видите следующее:

    // 20 more lines before this
    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");
}

И вроде бы все нормально, но как только взгляд опускается до линии } else {, ты впадаешь в ступор и спрашиваешь себя: если есть else, то что это было за if? И вам нужно прокрутить вверх, чтобы понять весь блок:

void handleMessage(Message message) {
    String text = message.getText();
    if (text != null) {
        // 35 lines of code
    } 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 more lines before this
        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, если вы поместите return в конец блока if:

void handleMessage(Message message) {
    String text = message.getText();
    if (text == null) {
        Log.error("Empty command");
        return;
    }
    // 20 more lines before this
    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 ничего не ломает, а, наоборот, исключает ненужное вложение. Конечно, может быть неочевидно, что это оператор _17 _ / _ 18_ или кто-то может быть против такого кода, учитывая, что метод должен иметь один вход и один выход. И они будут правы, поэтому решение должно быть продиктовано вашим руководством по стилю.

Тот же трюк работает с _19 _ / _ 20 _ / _ 21_:

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 // ...

С _22 _-_ 23_ это будет:

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 внутренние петли, вы должны использовать continue вместо return. До:

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

Добавив continue в конец тела if, мы получим рабочий код с меньшим отступом:

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. Если вложенность превышает допустимый уровень, введите новый метод.

Если внутри одного метода так много вложенных циклов и условий, что код переместился в правую сторону из-за отступов и вот-вот появится горизонтальная прокрутка, то пора провести рефакторинг и заменить некоторые части метода другим методом. Например:

Даже не пытайтесь понять этот код. Я просто придумал это для примера. Рефакторинг - тема отдельной статьи, поэтому я не буду рассматривать этот процесс поэтапно. Дело в изменениях:

Применяя эти правила, мы сделали код менее широким, но более длинным. Это нормально, потому что лучше прокручивать вверх / вниз, чем вверх / вниз и влево / вправо, а высокий уровень вложенности вместе с фигурными скобками также затрудняет понимание кода. Кроме того, код стал более понятным за счет методов с осмысленными именами, но в этом заслуга рефакторинга.