15 лет создания «Мира кораблей»: ключевые принципы и правила
В текущем году мы празднуем 11-летие проекта «Мир кораблей». Однако этот срок касается лишь релиза, в то время как стартовый коммит в репозитории появился 21 апреля 2011 года — целых 15 лет назад. За этот период кодовая база непрерывно масштабировалась, трансформировалась и усложнялась. В этой публикации мы хотим осветить фундаментальные принципы и регламенты, которые помогают нам удерживать кодовую базу в актуальном состоянии на столь долгой дистанции.
Материал подготовлен разработчиком клиентской логики, поэтому львиная доля сказанного затрагивает клиентскую часть и интерфейсы; у смежных команд «Мира кораблей» могут быть свои методологии. Представленные концепции не претендуют на абсолютную универсальность, но именно они на протяжении многих лет уберегают нас от погружения в неуправляемый легаси-код.
Ключевой ориентир — прагматичность
Во время собеседований я часто спрашиваю соискателей, какой именно код они считают качественным. Варианты звучат самые разные: «чистый», «хорошо организованный», «гибкий» и тому подобные. Все они по-своему верны, однако на мой взгляд, по-настоящему качественный код прежде всего прагматичен, то есть идеально решает поставленную бизнес-задачу.
Все остальные достоинства вытекают непосредственно из контекста задачи. Для масштабного проекта со значительным жизненным циклом критически важно, чтобы код отвечал следующим критериям:
-
Прозрачность и понятность. Во-первых, читать чужой код всегда сложнее, чем писать собственный. Во-вторых, над кодовой базой работает множество специалистов. Следовательно, мы обязаны минимизировать как порог входа для новых людей, так и вероятность ошибок из-за неверной интерпретации.
-
Логичность и предсказуемость. Объёмы кодовой базы огромны. Чем меньше неочевидных нюансов разработчику приходится держать в оперативной памяти, тем выше скорость и продуктивность его работы.
-
Гибкость для будущих доработок. У нас попросту нет ресурсов постоянно переделывать старый функционал под новые требования. Единственный выход — изначально закладывать масштабируемые архитектурные решения с расчетом на перспективу.
-
Минимальная связанность модулей. Проект насчитывает колоссальное количество элементов — только на Python реализовано свыше 11 тысяч классов. Если изменение в одном компоненте неизбежно тянет за собой сбои в других, эффективная разработка и внедрение новых фич станут невозможными.
Разумеется, данные постулаты работают не в ста процентах случаев. Скажем, накануне релиза крупного патча мы можем столкнуться с критическим багом, «идеальное» исправление которого потребует сложнейшей архитектурной переработки и долгих проверок.
В таких ситуациях приоритеты смещаются: мы применяем наиболее простой, прогнозируемый и безопасный хотфикс. Да, даже если он нарушает принципы абстракции и решает проблему локально. Как правило, подобная заплатка не идет в основную ветку, а применяется только для конкретного билда. К следующему обновлению мы обязательно заменяем её полноценным, проверенным и архитектурно верным решением.
Схожим образом обстоят дела со вспомогательным инструментарием: здесь мы можем сознательно пожертвовать микрооптимизацией ради максимальной простоты и читаемости. Внутренний софт, не попадающий к конечным игрокам, обычно пишется один раз, а дорабатывается спустя годы. Главное, чтобы он функционировал стабильно и был понятен специалисту, который заглянет в него через несколько лет. Потерянные доли миллисекунды на выполнение скрипта в данном случае не имеют никакого значения.
Масштабируемые архитектурные решения
Если прагматичный код — это хорошо, то идеальный код — тот, писать который вообще не потребовалось. Наличие стандартизированных подходов к типовым задачам избавляет от необходимости изобретать велосипед при каждом новом запросе.
Разумеется, невозможно в самом начале разработки спроектировать универсальный фреймворк на все случаи жизни. Поэтому, создавая новый компонент, мы стремимся сделать его максимально универсальным. Это не должна быть «поделка» под сиюминутную задачу — скорее, задел, который в дальнейшем легко адаптировать под аналогичные сценарии.
Подобный подход требует смотреть на проблему не через призму локального ТЗ, а глобально — оценивая всю категорию задач, с которыми мы столкнемся в будущем. Нередко выясняется, что нужный инструмент уже существует в системе, пусть и выглядит иначе, чем предполагал заказчик.
Важно понимать, что системное решение — это не гипотетический «швейцарский нож», напичканный невостребованным функционалом. Код должен быть в первую очередь расширяемым: решать текущую задачу «здесь и сейчас», но позволять легко наращивать возможности без разрушения изначальной архитектуры. При этом изоляция от других систем должна быть максимальной, чтобы локальный рефакторинг не приводил к каскадным сбоям.
Например, вводя в игру новый тип вооружения для временного ивента, мы не привязываем его механику жестко к условиям самого режима. Оружие просто устанавливается на выделенные для ивента корабли. Даже если геймплей уникален, вся логика режима инкапсулируется отдельно, в то время как управляемые ракеты или защитные щиты проектируются так, чтобы их можно было безболезненно перенести в любой другой режим или в случайные бои. Общая философия вооружений сводится к тому, чтобы любой корабль мог нести произвольный набор любых орудий.
Золотое правило здесь звучит так: «для каждой задачи должен существовать ровно один правильный способ решения». Это касается и проектирования: типовые проблемы устраняются стандартными методами. Если стандартный паттерн устарел, мы его эволюционируем, а не плодим новые сущности. Истории, когда одну и ту же задачу в разных частях проекта решали по-разному, у нас периодически случались, но в итоге мы всегда оставляли только один эталонный вариант.
Простой код писать сложно
Практика показывает, что для написания запутанного и сложного кода гениальностью обладать не нужно. А вот добиться того же результата с помощью лаконичного и простого кода — настоящее искусство. Ограничения дизайна и объем задач и так привносят в разработку избыточную сложность, поэтому мы всеми силами избегаем переусложнений.
Мы активно задействуем проверенные паттерны проектирования (фабрики, MVC, MVVM и прочие), но исключительно там, где в них есть реальная необходимость. Никакой «архитектуры ради архитектуры» быть не должно. Главный критерий прост: архитектура призвана облегчать понимание кодовой базы, а не усложнять его. Если некий интерфейс используется единожды или фабрика собирает пару объектов, от этих паттернов лучше отказаться.
Искоренение «мёртвого» кода
В процессе проектирования гибких систем всегда велик соблазн переусердствовать «на вырост»:
-
Заложить функционал, который в данный момент никому не нужен;
-
Оставить старые участки кода после проведения рефакторинга.
Так появляется мертвый (неиспользуемый) код. На первый взгляд — ну лежит он себе в репозитории и есть не просит. Однако на практике это порождает серьезные проблемы:
-
Мертвый код неотличим от живого. В нем все равно приходится разбираться, тратя ментальный ресурс при реализации актуальных фич;
-
Невызываемый код не покрывается тестами QA. Мы держим в уме наличие функционала, но фактически он не работает. Так, однажды мы обнаружили, что код «на перспективу» был принципиально неработоспособен спустя целых два года после увольнения его автора.
Работа на опережение
Когда программный продукт живет и развивается годами, подхода «лишь бы работало здесь и сейчас» уже недостаточно. Необходимо учитывать, что код будут поддерживать другие люди. Исходя из этого, мы выработали ряд обязательных регламентов:
-
Комментарии обязательны только для неочевидных, временных или спорных решений. Комментировать абсолютно всё бессмысленно — во-первых, так никто не делает, во-вторых, формируется полезная привычка: раз есть комментарий, значит, там зарыта важная архитектурная особенность.
-
Качественный нейминг должен емко отражать суть переменной или класса. Мы уделяем именованию сугубо пристальное внимание: название не должно превращаться в поэму, но обязано точно передавать назначение сущности. Более того, нейминг порой служит индикатором архитектурных проблем: если вы не можете сформулировать задачу класса парой слов, скорее всего, он перегружен обязанностями и его пора декомпозировать.
-
Не пиши плохой код и не закрывай глаза на чужие костыли. Ошибаются все, и порой мы тоже коммитим по пятницам вечером. Но это не оправдание для того, чтобы игнорировать технический долг или тиражировать плохие практики со словами «тут исторически так написано». Если вы правите метод и видите рядом сомнительное решение — поднимите историю коммитов и приведите код в порядок. Главное — не забудьте предупредить QA.
Командная коммуникация
У нас масштабный проект и огромный штат разработчиков, но универсальных солдат, знающих абсолютно каждый уголок кодовой базы, нет. Инженеры распределены по направлениям (клиент, сервер, игровая логика, интерфейсы и т.д.), которые мы называем гильдиями. Здесь также действуют строгие правила взаимодействия:
-
У любого модуля должен быть четкий владелец. За каждый компонент отвечает конкретная гильдия и лично её лид (гильдмастер).
-
Ветка не попадает в мастер без апрувов от всех заинтересованных направлений. Это правило проверяется неукоснительно при каждом слиянии.
-
Столкнувшись с багом из зоны ответственности другой гильдии, передайте тикет профильной команде. Бюрократии станет чуть больше, а процесс затянется, зато владельцы модуля будут в курсе проблем, а исправление получится качественным.
-
Внося правки в чужой код, обязательно согласуйте их с авторами и запросите ревью. Это позволяет сохранять коллективную ответственность и не превращать файлы в «беспризорные» зоны.
-
Информируйте QA о характере изменений и зонах риска. Иногда локальный рефакторинг тянет за собой смежный функционал или затрагивает ядро, от которого зависят десятки подсистем. Из-за этого добавление нового типа вооружений может неожиданно сломать, например, миникарту. Без детального тест-плана такие баги неизбежно ускользнут от внимания.
Заключение
Разумеется, к описанным стандартам мы пришли не сразу. Путь был тернистым, через множество рефакторингов и набитых шишек.
Некоторые архитектурные просчеты давали о себе знать лишь спустя годы. Иногда выбранный вектор развития оказывался ошибочным, и тогда приходилось переписывать колоссальные пласты проекта.
Случалось и так, что написанные наспех костыли жили годами, обрастая толстым слоем легаси. В конечном итоге на их латание уходило куда больше времени и сил, чем потребовалось бы на полное переписывание модуля с нуля в рамках понятной и прозрачной архитектуры.
Надеюсь, наш опыт окажется полезен при проектировании ваших систем. А если вы разделяете наши подходы или хотите дополнить этот свод правил — добро пожаловать в комментарии!
Обзоры Control Resonant появятся почти за неделю до релиза игры
Специалисты iFixit выпустили 18 инструкций по самостоятельному ремонту Steam Machine
Для хоррора Hellraiser: Revival выпустили демоверсию и новый трейлер с Алым Храмом
Анонсирована игра Lollipop Chainsaw 2 Back2Back, в которой Джульетта вернется только на PS5
Директор объяснил, почему в The Blood of Dawnwalker 2 может не быть таймера
Психологический хоррор Doki Doki Literature Club вернулся на Android спустя пять месяцев после удаления из Google Play
Разработчики Dredge открыли новую студию для создания «рогалика» Nothing Lost
Новому фильму Зака Креггера предрекают более 80 млн долларов за первый уикенд в прокате