Пятничные факты #366 — «The only way to go fast, is to go well.»

опубликовал kovarex

Привет,
давно не виделись 🙂

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

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

Uncle bob

Теперь, когда здесь только разработчики, я могу поделиться своим новым открытием Uncle Bob и его действительно хорошее объяснение некоторых фундаментальных принципов, связанных с управлением проектами программирования, и многое другое. Если у вас на руках 8,5 часов свободного времени, я предлагаю вам посмотреть его, так как в дальнейшем будут упоминания о том, о чем он упоминает.

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

И ответ объясняется метафорой воскового основания.

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

Одна из работ, которые вы выполняете с пчелами, заключается в том, что когда пчелы отбирают мед, вы кладете восковую основу в улей, которая выглядит следующим образом:

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

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

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

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

Это временная шкала строк кода в Factorio

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

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

Позвольте мне теперь объяснить несколько типичных примеров проблем, которые у нас были, и то, как мы приступили к их исправлению:

Пункт 1 — взаимодействие с графическим интерфейсом

Мы много писали о графическом интерфейсе (FFF-216) и как мы многократно поднимали планку того, что считаем приемлемым как с точки зрения пользователя, так и с точки зрения программиста. Общие выводы из FFF и кодирования заключались в том, что мы всегда недооценивали, насколько сложной может стать логика / стили / макет графического интерфейса и т. Д. Это означает, что улучшение способа написания графического интерфейса пользователя имеет большие потенциальные выгоды.

Мы довольны тем, как объекты GUI структурированы и расположены после обновления 0.17. Но в коде он все еще кажется намного более раздутым, чем должен быть. Основная проблема заключалась в количестве мест, к которым нужно было прикоснуться, чтобы добавить интерактивный элемент. Позвольте мне показать вам пример, простую кнопку, используемую для сброса предустановок в окне генератора карт.

В заголовке класса:

class MapGeneratorGui
{
  ...

У нас было определение объекта кнопки

...
IconButton resetPresetButton;
...

В конструкторе MapGenerator нам нужно было сконструировать кнопку с параметрами

...
, resetPresetButton(&global->utilitySprites->reset, // normal
                    &global->utilitySprites->reset, // hovered
                    &global->utilitySprites->resetWhite, // disabled
                    global->style->toolButtonRed())
...

Нам нужно было сделать listener этой кнопки

...
this->resetPresetButton.addActionListener(this);
...

Затем нам нужно было переопределить метод ActionListener в нашем MapGeneratorClass, чтобы мы могли считывать щелчки.

...
void onMouseClick(const agui::MouseEvent& event) override;
...

И, наконец, мы могли бы реализовать метод, где мы через if / else элементы, чтобы реализовать логику

void MapGeneratorGui::onMouseClick(const agui::MouseEvent& event)
{
  if (event.getSourceWidget() == &this->resetPresetButton)
    this->onResetSettings();
  else if (event.getSourceWidget() == &this->randomizeSeedButton)
    this->randomizeSeed();
 ...

Это слишком много шаблонов для одной кнопки с одним простым действием. У нас было более 500 мест, где мы делали actionListeners в коде, так что представьте, сколько раздутий.

Мы как бы заметили, что, когда мы используем лямбды для сигнализации обратных вызовов и тому подобных вещей в графическом интерфейсе, их гораздо приятнее использовать. Что, если бы мы сделали его основным способом создания графического интерфейса?

Мы решили полностью переписать способ его работы, поэтому вместо добавления listener и фильтрации из функций перехвата событий мы можем просто указать:

this->resetPresetButton.onClick(this, [this]{ this->onResetSettings(); });

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

А поскольку нам не нужно удерживать указатель для сравнений, мы можем полностью удалить его определение из класса и сделать его анонимным во многих местах следующим образом:

*this << agui::iconButton(&global->utilitySprites->reset,
                          global->style->toolButtonRed(),
                          this, [this]{ this->resetPreset(); })

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

The only way to go fast is to go well!

Пункт 2 — Ручное строительство

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

Логика ручного строительства — это монстр, потому что она уже поддерживает все:

Затем всю эту логику нужно умножить на 2 (когда вы ленитесь и копипастите), так как у вас могут быть обычные здания и здания-призраки.

А затем вы снова умножаете всю эту мерзость кода на 2. Почему? Потому что нам также нужно реализовать всю эту логику в режим скрытия задержки. Звучит уже плохо, но это еще не все, поскольку эта логика постоянно исправлялась и затрагивалась разными людьми на протяжении всей истории, ядро ​​кода было безумно длинным методом с кодом, похожим на горизонт, упомянутый Uncle Bob.

Теперь представьте, что вам нужно что-то изменить в этом коде, особенно если учесть, что в коде, естественно, было много неправильных идей или исправления только в каком-то варианте кода. Это отличный пример того, как ленивый долгосрочный дизайн приводит к снижению производительности.

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

Это напомнило мне цитату Lou после подобного рефакторинга: «Как только мы с этим закончим, мы с удовольствием добавим что-нибудь в этот код». Разве это не красиво? Это не только более эффективно и менее глючно, но и более увлекательно c таким работать, а работа над чем-то приятным, как правило, идет быстрее, независимо от других аспектов.

The only way to go fast is to go well!

Пункт 3 — GUI тесты

Нет, мы явно не дошли до этого без автоматических тестов, и мы уже упоминали о них несколько раз. (FFF-29FFF-288 …).Мы стараемся постоянно поднимать планку того, какие области кода покрываются тестами, и это заставляет нас охватить еще одну область — графический интерфейс. Это согласуется с постоянно повторяющимся недооценкой объема инженерной заботы, в которой нуждается графический интерфейс. То, что он вообще не тестировался, является частью этой недооценки, сколько раз это происходило, что мы делали релиз, и он просто крашился на чем-то глупом и  простом в графическом интерфейсе, просто потому, что у нас не было теста, который бы нажимал кнопки . И, в конце концов, оказалось совсем несложно автоматизировать тесты GUI.

У нас просто есть режим, в котором среда тестирования создается с графическим интерфейсом (даже когда тесты выполняются без графики). Мы объявили несколько вспомогательных методов, которые позволяют очень просто определить, куда мы хотим переместить курсор или что мы хотим щелкнуть, например:

TEST(ClearQuickbarFilter)
{
  TestScenarioGui scenario;
  scenario.getPlayer()->quickBar[0].setID(ItemFilter("iron-plate"));
  CHECK_EQUAL(scenario.player()->getQuickBar()[0].getFilter(), ItemFilter("iron-plate"));
  scenario.click(scenario.gameView->getQuickBarGui()->getSlot(ItemStackLocation(QuickBar::mainInventoryIndex, 0)),
                 global->controlSettings->toggleFilter);
  CHECK_EQUAL(scenario.player()->getQuickBar()[0].getFilter(), ItemFilter());
}

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

The only way to go fast is to go well!

Пункт 4 — TDD — Разработка через тестирование

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

Но это не TDD, и мне пришлось показать это «для чайников», чтобы я понял, насколько я был неправ.

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

The only way to go fast is to go well!

Пункт 5 — Тестовые зависимости

Это продолжение темы тестовых зависимостей из тестов графического интерфейса.

Если тесты должны быть действительно независимыми, в тесте C должны быть некоторые имитации A и B, поэтому тест C не зависит от правильной работы системы A + B. Похоже, что консенсус в том, что это приводит к более независимому дизайну и т. Д.

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

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

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

Если тесты не имеют какой-либо специальной структуры, ситуация, когда все 100 тестов терпят неудачу одновременно, очень неудачна, все, что вам остается, — это попытаться выбрать какой-нибудь тест полуслучайно и начать его отладку. Но это действительно заблуждение, когда какой-то сложный тестовый пример терпит неудачу в середине, и вы тратите много времени на его отладку, только чтобы понять, что это какая-то очень простая ошибка низкого уровня вызывает сбой.

Цель довольно проста, я хочу получить самый простой случай неудачи моего изменения.

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

Это пример тестового графика зависимости, относящегося к электрическим столбам:

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

The only way to go fast is to go well!

Пункт 6 — Покрытие тестов

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

Покрытие тестированием — это индикатор того, какие части кода выполняются при запуске приложения (что обычно означает запуск тестов в этом контексте). Я никогда раньше не использовал инструмент для измерения тестового покрытия, но, поскольку это была одна из тем, о которых говорил дядя Боб, я попытался использовать его впервые. Я нашел инструмент, который работает только в Windows, но требует наименьшего количества настроек, а именно OpenCppCoverage, который обеспечивает такой вывод HTML:

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

The only way to go fast is to go well!

Заключительные слова

The only way to go fast is to go well!

Если эта тема вас зацепила, если при чтении вы испытываете такие эмоции: «Я бы хотел, чтобы у моего начальника были такие приоритеты». Подумайте о том, чтобы устроиться на работу в Wube!

Comments: