«Безумное чаепитие» — эксперимент по обучению школьников правилам хорошего кода

Мы — математический лагерь «Слон» — уже давно проводим летние и зимние школы для учеников 8–11 классов. Основной вид деятельности на школе — работа над крупной задачей, проектом. Это может быть что угодно от моделирования сложной физической системы до программы взлома шифров или написания игрушки под Android. Большая часть проектов на школе так или иначе связана с программированием, но редко программирование является самоцелью проекта. Школьники, которые еще не успели стать матерыми программистами, да еще и в условиях вечной нехватки времени пишут код «шоб работало». Так что мы не понаслышке знаем, что такое плохой код и каждый год встречаем всё новые, иногда удивляющие даже нас, способы сделать код нечитаемым — и каждый год решаем, что делать с этой проблемой.

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

Еще одной идеей было использовать git, «чтобы дурь каждого видна была». Тогда ближе к концу проекта можно было бы пересмотреть, с чего все начиналось и куда вывернуло, ужаснуться и делать по-другому. Однако эта идея не прошла проверку временем. По нашему опыту, школьников сложно научить пользоваться системой контроля версий, да еще и регулярно. Им непонятно, для чего СКВ нужны, а потому им скучно. Кроме того, отнимать пару часов только на освоение git — безумное расточительство для проекта длиной в одну неделю. Да и не для того системы контроля версий изначально задумывались.

Решение же, которое мы использовали этой зимой нам самим очень понравилось, поэтому считаем нужным поделиться своим методом. Мы назвали его «Безумное чаепитие».
Итак, задача: научить школьников писать понятный и аккуратный код. При этом надо сделать этот процесс увлекательным…

Чтобы научиться писать хороший код, мы обычно смотрим на примеры хорошего кода и плохого кода. Школьники же обычно смотрят только на свой собственный код. Курс сконструирован так, чтобы поменять эту практику: участники смотрят и на хороший код, и на плохой и пишут код сами. Обычно дети выступают в роли критикуемых, на спецкурсе же у них была возможность посмотреть на чужой код, покритиковать его самим и постараться улучшить. Как?
Мы взяли одну задачу, условия которой можно сильно варьировать (у нас это была Конвеевская игра «Жизнь») и разбили ее на несколько небольших этапов. Разбиение производится так, чтобы в конце каждой итерации получалась готовая работающая программа. Сами этапы довольно небольшие, чтобы за одно занятие (длиной в 1.5 часа) их можно было завершить, иногда в 1.5 часа умещались даже две итерации.
Будущие итерации участникам заранее неизвестны, так что где «подложить соломки» на текущей итерации, чтобы делать следующую итерацию было легко, им тоже неизвестно.
В начале школьникам выдается задание первого этапа. После того, как оно было закончено всеми участниками, приходило время «пересесть». Участники обменивались решениями и получали задание второго этапа. Теперь им надо было разобраться в чужом коде и модифицировать его так, чтобы он решал задачу второго этапа. При этом рефакторить код и исправлять чужие ошибки разрешалось, а вот переписывать код полностью было запрещено. После второго этапа школьники снова обменивались решениями и снова продолжали дописывать код, и опять на новом для них основании. Общение между участниками спецкурса ограничено, поэтому узнать, «что за ужас здесь написан», можно было только вчитавшись в код. Между итерациями проводились разборы повторяющихся стилевых ошибок, которые рекомендовалось исправить.

На нашем спецкурсе было пять школьников и, соответственно, пять проектов. Над каждым файлом в разные моменты времени трудилось три школьника. Когда через несколько итераций до них доходил свой собственный когда-то код, он уже очень сильно отличался от того, что они писали изначально. Чужой код, с которым они работали на предыдущей итерации для них оказывался более «своим», чем собственный код, который приходил к ним, пройдя через всех остальных участников.
Также важно чтобы школьники обменивались кодом не по кругу, иначе на каждой итерации участнику будет приходить код из рук одного и того же человека.

Но давайте с подробностями, так интереснее. Как я уже сказал, глобальной задачей было написать Конвеевскую игру «Жизнь», но эта задача была заранее неизвестна. Задания этапов были такими:

  1. Пишем одномерную игру «Жизнь» (aka элементарный клеточный автомат), игровое поле размером в 200 клеток. Клетка выживает или зарождается, если у нее ровно один сосед. Для крайних точек «сосед» за границей всегда «мертв». Изначально заполняем поле живыми клетками так, что номера живых клеток составляют геометрическую прогрессию с коэффициентом 2. Каждый ход происходит по нажатию клавиши Enter.
  2. Разрешаем пользователю вводить размер поля и коэффициент геометрической прогрессии. Добавляем возможность по вводу команды «W» делать не один, а сразу 10 шагов.
  3. Даем пользователю возможность задать начальную конфигурацию поля: пользователь вводит число, его двоичное представление задает, какие клетки живы (бит 1), а какие мертвы (бит 0). Также необходимо сделать проверку, что исходно заданная конфигурация влезает в указанный размер поля. Добавляем команду «R», которая перезапускает программу.
  4. Изменяем правила выживания: теперь выживает клетка с одним соседом, а зарождается из ничего, если есть сосед слева. Дополняем опцию «W» так, чтобы можно было ввести количество шагов, которые поле должно сделать.
  5. Правила жизни теперь задаются числом от 0 до 255: для каждой из восьми исходных конфигураций (центральная клетка с двумя соседними, каждая в одном из двух положений) задается один бит: какой должна быть клетка на следующем шаге.
    Кстати, по этой модификации «Жизни» можно гадать
    Выдержка из школьной газеты:

    «Безумное чаепитие» предсказывает судьбу по одномерной игре «Жизнь»: вводим возраст как правило — смотрим на развитие клеток. Получены такие весьма правдоподобные результаты: в 16 лет и дальше всё безнадёжно плохо, и все постоянно уходят в закат; в 23, наконец, наступает стабильность; иногда ещё можно жить, если быть вдвоём. Сотрудник редакции — эксперт по игре «Жизнь» добавляет, что в 25 это гадание предсказывает размножение, что характерно. Универсальное знание, как мы помним из лекции, придёт в 110.


  6. Добавляем сохранение и загрузку поля и правил.
  7. Переносим игру в двумерный мир. Теперь правила задаются стандартным образом: число соседей, чтобы выжить и число соседей, чтобы ожить; состояние поля задается построчно.

Все школьники у нас писали на Python, однако программистские навыки участников довольно сильно различались. Формат задания таков, что различие уровня людей в группе является не минусом, а существенным плюсом. Использование одного общего языка программирования — удобно, но не обязательно. Возможно, что использование нескольких языков сделает курс даже более увлекательным, ведь в реальной жизни иногда приходится читать и редактировать код на незнакомом языке.
Мы не заставляли школьников использовать систему контроля версий, но для удобства сами свели материалы в один репозиторий. Довольно увлекательное чтение для тех, кого интересует обучение программированию.

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

Обмен кодом между участниками крайне удачно сгладил качество кодовой базы. Даже если начиналась программа с множества однобуквенных переменных и отсутствия модульности, то через пару итераций кто-нибудь непременно давал переменным хорошие имена, структурировал файл, упрощал код. А испортить чужой (относительно) хороший код — это еще надо постараться; проще встроиться в чужой стиль. Кроме того, школьники подхватывают друг у друга (или у преподавателя?) понравившиеся паттерны. Иногда хорошие.

Однако вопреки нашим ожиданиям, с течением времени код становился все более и более непохожим. Единожды принятые решения продолжали жить и развиваться, хотя и попадали в руки к людям, которые у себя сделали по-другому. Впрочем, справедливости ради, так было не всегда. Некоторым участникам тяжело было смириться с тем, что кто-то решил задачу по-другому, и они садились переписывать на свой лад нормальные работающие куски кода… И не всегда делали этим лучше. А уж названия переменных были настолько же переменчивы, как их содержание. Видимо, стоит вводить какие-то формальные ограничения на масштаб изменений.

Как считает автор курса, лучше всего «Безумное чаепитие» повлияло на школьников среднего уровня, а меньше всего на самого слабого и самого сильного школьника. Самому слабому всегда приходит весьма приличный по его меркам код, а самому сильному не на кого равняться. Улучшить эту ситуацию можно, если преподаватель курса сам будет участвовать в написании кода, как дополнительные два участника. Первый такой участник будет писать рефакторить и менять код так, как если писал бы его сам, а второй будет делать искуственно плохой код, хуже самого слабого участника.

Надо сказать, некоторые из наших школьников неплохо умеют писать в ООП-стиле. И потому довольно примечательно, что они его не использовали даже там, где ООП напрашивается. Похоже, что для них объекты — это не очевидный выбор, когда дело доходит до упрощения кода. Недостаточно изучить, как писать свои классы, нужно обучать, в каких случаях использование классов позволит улучшить программу. В будущих инкарнациях курса стоит это учесть.

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

Одной из причин такого непонимания является представление о переменной как о какой-то «коробочке», в которую можно положить значение, прочитать значение и поменять значение. Само название «переменная» подсказывает, что они созданы чтобы в них значение менялось, а четко описать смысл объекта, который в разные моменты времени хранит в себе разное содержимое — не так-то просто. Иногда и просто невозможно. Переиспользование одной и той же переменной, вероятно, полезно в высокопроизводительных приложениях на языках низкого уровня (и то, только если компилятор плохо справляется с оптимизацией), но в современном мире это не самое осмысленное использование возможностей языка. Обычно это какие-то безобидные небольшие преобразования: добавили к строке пару символов, округлили число, сконвертировали строку в число. Например, к полю добавили две фиктивные клетки по краям и записали в ту же переменную. И теперь уже не сказать, что лежит в переменной: число или его строковое представление? поле или расширенное поле? Это кажется мелочью, но из них состоит качество кода. И подобные мелочи способны попортить немало крови при рефакторинге.
Я не являюсь ярым апологетом функциональных языков программирования, однако концепция неизменяемых переменных может оказаться весьма полезной для создания привычек к хорошему стилю кода.

Другая причина заключается в том, что многим просто лень сосредоточиться и сформулировать смысл переменной. Нередко в названии отражается не содержание переменной, а ее форма. Например, на третьем этапе появилась возможность задать исходную конфигурацию поля десятичным числом, которое при переводе в двоичную форму давало бы конфигурацию. В 4 программах из 5 эта переменная называлась `number` или `decimalNumber`, а вовсе не `initialFieldPosition` или `aliveCellsEncoded`. Потом эти названия еще по несколько раз менялись в угоду вкусам того или иного программиста, но так и не приобрели законченного и логичного названия. В отличие от многих других переменных, которые, обретя свое истинное имя, прекратили эти метания. Или вот `count_symb`, что это? На самом деле длина поля —, а все потому что поле представлено строкой, стало быть число символов представляет собой длину строки.

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

С названиями функций происходят аналогичные войны правок. Часть причин аналогична, но есть и специфические. Дело в том, что новый код обычно дописывается туда, куда его проще дописать, а не туда, куда логичнее. Если нужно добавить аргумент функции или возвращаемый параметр (помните, мы пишем на питоне?), он добавляется без зазрения совести. В удачном случае имя функции меняется соответствующим образом, но не всегда. Через некоторое время смысл производимых функцией действий имеет весьма условное отношение к ее имени. Что делать с этой проблемой пока непонятно. Один из вариантов — директивно запретить школьникам менять тип возвращаемого функцией значения и сделать четкое разделение на функции, производящие действия и возвращающие результат. Впрочем, наивно полагать, что подобный запрет сработает.

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

Курс «Безумное чаепитие» придумал и провёл Павел Смирнов. Я же лишь расспросил автора, прочитал получившиеся исходные коды программ и записал свои наблюдения.

P.S. Курс учит не тому, как писать продукт, который заранее тщательно продумывают на несколько шагов вперед, а тому как работать с чужим кодом, как постепенно улучшать его и не давать программе превратиться во что-то ужасное.

© Megamozg