ZP ZapolyarnyDev
Статья

Мой взгляд на обучение программированию

Набор моих рекомендаций о том, как учиться через код, задачи и разбор собственных ошибок

Вернуться в блог

Главная мысль

Первая простая мысль, которая уже укоренилась в сообществе: программированию нельзя нормально научиться только через просмотр курсов, чтение документации или повторение туториалов. Всё это полезно, но само по себе не делает человека программистом.

Скажу даже больше: обучение в формате “сейчас поучу базу языка, а потом, может быть, напишу TODO на фреймворке” будет мешать будущему специалисту.

Лучший выигрыш для программиста, на мой взгляд, — взять непонятную, а желательно и очень сложную задачу, и превратить её в мелкие понятные шаги. По своему опыту скажу, что дальше начинается самый душный цикл: код -> много ошибок -> разбираемся -> улучшаем решение.

Цель такого обучения — не просто получить готовый продукт или знание синтаксиса языка, который мы учим, а запомнить путь к решению поставленной задачи или, скажем, к изучению нового фреймворка.

Если совсем коротко:

  • не учить язык отдельно от задач;
  • не копировать код без объяснения каждой строки;
  • не пытаться сразу писать идеально;
  • регулярно читать чужой код;
  • переписывать свои решения после понимания всех этапов;
  • обязательно делать выводы о том, как мы пришли к решению задачи и что мешало раньше;
  • фиксировать ошибки как материал для роста.

Обучение должно идти через маленькие законченные программы

Крайне плохая практика — учиться в течение n-ого количества времени, а потом сразу пытаться написать большую программу. Рекомендую учиться через маленькие программы, которые можно завершить за один вечер:

  • калькулятор опыта игрока;
  • простой чат на сокетах;
  • фильтр списка пользователей (здесь я смог освоить StreamAPI);
  • таймер матча;
  • генератор расписания;
  • простая система инвентаря;
  • асинхронная генерация набора миров / игровых предметов.

Важно, чтобы программа была маленькой, но выполняла достаточно действий для получения нужного результата. Лучше всего мне помогала разработка мини-приложений / модов на игры с типичным потоком данных:

  1. Ввод.
  2. Обработка информации.
  3. Отказоустойчивость и ответ на ошибки.
  4. Вывод.

Например, не просто:

int level = 10;
System.out.println(level);

А так:

public final class ExperienceCalculator {
  public int experienceForNextLevel(int level) {
    if (level < 1)
      throw new IllegalArgumentException("Level must be positive");

    return 100 + (level - 1) * 35;
  }
}

Здесь уже есть поведение, понятное самому разработчику:

  • что именно считается некорректным вводом;
  • где мы рассчитываем значение;
  • как назвать метод так, чтобы он объяснял смысл;
  • что произойдёт, если вызвать код не так, как задумывалось.

Часто я вижу, как новички при запуске размышляют: “а как будет работать мой код?” Так происходит потому, что поведение программы заранее не было продумано.

Учимся не по темам, а по вопросам

Раньше я просто ставил себе цель. Например: “изучить коллекции”.

Вопрос программиста звучал бы иначе: “как хранить список игроков так, чтобы быстро найти игрока по имени?”

Это разные парадигмы мышления. В первом случае человек запоминает названия классов. Во втором он выбирает инструмент под задачу и понимает первопричины такого выбора.

Например:

Map<String, PlayerProfile> profilesByName = new HashMap<>();

profilesByName.put("ZapolyarnyDev", new PlayerProfile("ZapolyarnyDev", 67));

PlayerProfile profile = profilesByName.get("ZapolyarnyDev");

После такой реализации стоит задать себе вопросы:

  • почему здесь Map, а не простой список;
  • что будет, если имени нет;
  • можно ли хранить ключ в другом регистре;
  • что сломается, если два игрока имеют одинаковое имя;
  • где лучше проверять пустые и некорректные значения.

Так мы сможем понять, какие недостатки есть в коде, и объяснить его поведение вместо пересказа документации.

Ошибки — это не плохо

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

Плохой сценарий:

  1. Получил ошибку.
  2. Скопировал текст в поиск.
  3. Вставил первое решение.
  4. Код заработал.
  5. Пошёл дальше.

А первые два года программирования именно так я и делал :D

Более приемлемый сценарий:

  1. Получил ошибку.
  2. Прочитал сообщение или лог полностью.
  3. Нашёл строку, где она возникла.
  4. Сформулировал причину своими словами.
  5. Исправил.
  6. Записал короткий вывод.

Например:

String rawName = null;
String normalized = rawName.trim().toLowerCase();

Ошибка здесь возникает не по вине языка. Причина в том, что код предполагает наличие строки, но это предположение нигде не защищено. О таких местах и должен заботиться программист.

Исправление может быть таким:

public String normalizeName(String rawName) {
  if (rawName == null || rawName.isBlank())
    throw new IllegalArgumentException("Name must not be empty");

  return rawName.trim().toLowerCase();
}

После этого важно запомнить не то, что мы “защитили вызов от значения null”, а более общий принцип:

Если метод не может работать без значения, он должен явно проверять входные данные или принимать тип, который запрещает пустое состояние.

Вот это уже обучение.

Не надо бояться переписывать код

Первое решение почти всегда кривое. Это нормально. Более того, попытка сразу написать “правильно” часто может быть вредной.

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

Пример первого решения:

if (command.equals("start"))
  startGame();
else if (command.equals("stop"))
  stopGame();
else if (command.equals("reload"))
  reloadGame();
else
  sendUnknownCommand();

Для трёх команд это нормально. Для тридцати это уже выльется в трудную поддержку и расширение.

Более удобная версия может быть такой:

Map<String, Runnable> commands = Map.of(
  "start", this::startGame,
  "stop", this::stopGame,
  "reload", this::reloadGame
);

commands.getOrDefault(command, this::sendUnknownCommand).run();

Но и здесь надо подумать, а не просто писать красивые решения. Второй вариант короче, но не всегда понятнее новичку. Хороший программист учится задавать вопрос: стало ли решение проще для будущего изменения?

Код всегда стоит писать так, чтобы большинство данных и операций при выполнении были предсказуемыми.

Читать чужой код нужно активно

Чтение чужого кода помогает шире смотреть на реализацию нужной задачи.

Например, мы увидели следующий метод:

public MatchResult finishMatch(Player winner, Player loser) {
  ratingService.addWin(winner);
  ratingService.addLoss(loser);
  rewardService.giveReward(winner);

  return new MatchResult(winner.id(), loser.id());
}

Перед выводами лучше лишний раз задать себе вопросы:

  • можно ли вызвать метод два раза;
  • где проверяется, что матч действительно активен;
  • должен ли проигравший получать утешительную награду;
  • должны ли мы учитывать контекст вызова данного метода;
  • кто сохраняет результат.

Так можно натренировать способность лучше понимать работу и тонкости большинства технологий.

Мини-проекты должны расти

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

Например, самая типичная учебная задача: “сделать TODO”.

v0.1:

  • добавить задачу;
  • вывести список задач;
  • отметить задачу выполненной.

v0.2:

  • сохранить задачи в файл;
  • добавить дедлайн;
  • отфильтровать выполненные;
  • обработать пустой ввод.

v0.3:

  • покрыть основные операции тестами;
  • отделить хранение от логики;
  • добавить импорт и экспорт;
  • сделать простой интерфейс.

Такой рост гораздо лучше учит продумывать архитектуру программы / приложения, чем попытка написать всё с первого раза.

Как заниматься каждый день

Обычно сессии обучения у меня занимали 60-90 минут и выглядели примерно так:

  1. Выбрать один желаемый результат.
  2. Написать код в формате “как есть”.
  3. Запустить и протестировать его множеством некорректных входных данных.
  4. Исправить найденные ошибки.
  5. Постепенно и более корректно переписать программу частями.
  6. Записать три вывода.