В этом году мы отмечаем 11-летие «Мира кораблей». Но это только 11 лет в релизе, а первый коммит в репозиторий был сделан 21 апреля 2011 года — 15 лет назад. Всё это время кодовая база росла, менялась и усложнялась. В этой статье мы поделимся правилами и принципами, которыми мы руководствуемся, чтобы поддерживать и развивать кодовую базу на такой длинной дистанции.

Автор — программист клиентской логики, поэтому большая часть сказанного относится к клиенту и UI, у других команд «Мира кораблей» могут быть свои подходы. Изложенные здесь правила не всегда универсальны, но это то, что на протяжении многих лет спасает нас от скатывания в неподдерживаемое легаси.

Главный принцип — целесообразность

На собеседованиях я часто интересуюсь, какой код кандидаты считают хорошим. Отвечают разное: «легко читаемый», «структурированный», «расширяемый» и т.п. Это хорошие ответы, но лично я считаю, что хороший код — это, в первую очередь, целесообразный код. То есть решение, которое максимально соответствует своей задаче.

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

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

  • Логичным и предсказуемым. Кодовая база — большая. Чем меньше нюансов её работы приходится держать в голове программистам, тем проще и быстрее им работается.

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

  • По минимуму связанным с другими системами. У нас огромное количество систем — более 11 тысяч классов только на Python. Если изменения в одной системе будут менять поведение второй и третьей систем, то эффективно разрабатывать игру и добавлять новые фичи мы не сможем.

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

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

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

Системные решения

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

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

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

Важно помнить, что системное решение — это не «швейцарский нож», предусматривающий сиюминутное решение десятка кейсов, о которых не просили. Код должен быть именно расширяемым. То есть решать ровно ту задачу, которая поставлена изначально, но предусматривать решение схожих задач за счёт небольших доработок, не влияющих на изначальную архитектуру. А ещё такие решения должны быть минимально завязаны на существующих системах, чтобы рефакторинг в одной из них не ломал остальные.

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

Главное правило здесь: «должен быть только один правильный способ сделать что-либо». Это касается и дизайна: стандартные проблемы должны решаться стандартными методами. Если стандартный метод не подходит, это повод его улучшить, а не писать новый. У нас периодически бывало так, что в разных местах одну и ту же задачу решали разными способами, но оставался из них в итоге только один.

Писать простой код — сложно

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

Мы активно пользуемся паттернами программирования вроде фабрик, MVC, MVVM и прочих, но только там, где они действительно нужны. В остальных случаях не должно быть никакой «архитектуры ради архитектуры». Основной принцип здесь в том, что архитектура должна упрощать понимание кода, а не усложнять его. Если используется какой-то паттерн, он должен использоваться для решения массовых задач. А вот если какой-то интерфейс используется один раз в одном месте или фабрика конструирует лишь пару объектов, значит можно обойтись без них.

Нет «мёртвому» коду

При разработке гибкой и расширяемой архитектуры велик соблазн расширить её «на будущее»:

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

  • Оставить старую функциональность при рефакторинге.

Такой подход порождает «мёртвый» код — в проекте он есть, но на практике не используется. Казалось бы, ну лежит и лежит. Есть же не просит? Действительно не просит, но проблемы тут есть:

  • «Мёртвый» код сложно отличить от «живого». В нём всё равно приходится разбираться и, оглядываясь на него, реализовывать «живые» решения;

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

Решаем проблемы завтрашних нас

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

  • Комментарии в коде для неочевидных, временных и спорных решений — обязательны. И только для них — вообще всё комментировать не нужно. Во-первых, этого никто не будет делать. Во-вторых, такой подход сразу вырабатывает паттерн: «есть комментарий — есть какая-то важная особенность, которую надо учесть».

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

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

Коммуникации в команде

У нас довольно много кода и программистов, но «универсалов», работающих со всей кодовой базой, нет. Внутри компании разработчики разделены по направлениям: клиент, сервер, игровая логика, UI и т. д. — у нас они называются гильдиями. Тут тоже есть несколько обязательных правил:

  • Не должно быть «ничейного» кода. За любой модуль отвечает какое-то одно направление и лично лид этого направления — гильдмастер.

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

  • Чините баг и поняли, что он в зоне ответственности другой гильдии? Передайте его им. Да, бюрократии будет больше, а процесс продлится дольше, но зато владелец кода будет в курсе проблем, а сам фикс будет качественнее.

  • Меняете код другой гильдии? Посоветуйтесь с ней и получите от её участников ревью. Так владение кодом и ответственность за него не размоется, а файлы, которые правят сразу несколько человек, не превратятся в «ничейный» код.

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

Заключение

Конечно, перечисленные выше правила были с нами не всегда. Чтобы прийти к ним, мы набили много шишек и прошли через множество рефакторингов.

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

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

Надеюсь, наш опыт поможет вам в проектировании систем. А если у вас есть схожий опыт или вам есть чем дополнить наш список принципов и правил, ждём вас в комментариях!

Комментарии (1)


  1. nerudo
    18.09.2026 10:47

    Если вас пытают новые хозяева, просто дайте знак - ничего не пишите под этим комментарием.