Исходный код Doom 3 – будь проще
Если вы забьете в поисковик запрос «лучший исходный код на C++», чаще всего в выдаче будет встречаться упоминание исходного кода Doom 3 вкупе с отзывами наподобие этого:
«Я провел немало времени, изучая исходный код Doom3. Думаю, это самый чистый и приятный код из всех виденных мною».
Doom 3 – это видеоигра, разработанная студией id Software и выпущенная компанией Activision. Игра принесла id Software коммерческий успех, разойдясь тиражом более 3,5 миллионов копий.
Спустя 7 лет после выхода игры, 23 ноября 2011 года студия id Software представила исходный код своего старого движка Doom 3. Код был тщательно изучен разработчиками, и вот, на примере CoderGears Blog, какого мнения придерживается большинство из них:
Doom 3 написан на C++, таком разнообразном языке программирования, что с его помощью можно создать как безупречно изящный код, так и жутких поганищ, от одного вида которых режет глаза. К счастью, id Software решила придерживаться паттернов, близких к парадигме «Си с классами», поэтому мозг легко воспринимает этот код:
- Никаких исключений
- Никаких ссылок (используйте указатели)
- Минимальное использование шаблонов
- Const везде
- Классы
- Полиморфизм
- Наследование
Многие эксперты по C++ больше не рекомендуют подход «Си с классами», однако Doom3 создавали в период с 2000 до 2004 года, так что разработчики просто не имели под рукой современных механизмов программирования на C++.
Давайте углубимся в код игры при помощи CppDepend (инструмент, который помогает разработчику получить нужную информацию из исходного кода, проверка правильности иерархической структуры проекта, отслеживание внесенных изменений со времени последней ревизии или оценка качества кода) и узнаем, что делает его таким особенным.
Doom 3 поделен на модули при помощи нескольких проектов. Ниже представлен список этих проектов и немного статистических данных об их типах:
А вот график зависимостей, показывающий отношения между ними:
Doom 3 использует много глобальных функций. Тем не менее, основную работу берут на себя классы.
Модель данных определяется при помощи структур. Для того, чтобы составить представление об использовании структур в исходном коде, взгляните на представленное ниже изображение, где структуры отображены в виде синих прямоугольников. Основание кода здесь представлено посредством метода Treemap. Тримапинг – это метод отображения древовидной структуры данных в виде сцепленных прямоугольников. Представленная древовидная структура – это стандартная иерархия кода:
- Проект содержит область имен.
- Область имен содержит типы.
- Типы содержат методы и области.
Как видите, многие структуры явно определены. К примеру, более 40% типов DoomDLL – это структуры. Они систематически используются для определения модели данных. Эта практика используется во многих проектах, однако подобных подход чреват проблемами в случае с многопотоковыми приложениями. И в самом деле, структуры с общедоступными областями нельзя назвать неизменяемыми.
У неизменяемых объектов есть одно большое преимущество: они значительно упрощают параллельное программирование. Задумайтесь: почему написание качественной многопотоковой программы – такая сложная задача? Дело в том, что существует проблема синхронизации доступа потоков к ресурсам (объектам или другим ресурсам ОС). Почему так сложно настроить синхронизацию доступа? Потому что сложно гарантировать отсутствие режима конкуренции между множественными доступами к чтению и записи, которые возникают при обращении множества потоков к множеству объектов. А что, если доступы к записи исчезнут? Другими словами, что, если состояние объектов, к которым получают доступ потоки, не будет меняться? В таком случае необходимость в синхронизации отпадает!
Давайте найдем классы, в которых есть как минимум один базовый класс:
Почти 40% структур и классов имеют базовый класс. Как правило, одним из преимуществ наследования в объектно-ориентированном программировании является полиморфизм. Синим цветом здесь обозначены виртуальные методы, заданные в исходном коде:
Более 30% методов – виртуальные. Правда, лишь немногие из них полностью виртуальные. Вот список всех заданных абстрактных классов:
Только 52 из них – абстрактные классы; чистых интерфейсов здесь 35, то есть все их виртуальные методы чистые.
Давайте поищем методы с использованием RTTI.
Только несколько методов используют RTTI.
Подведем итоги: только базовые концепции ООП, никаких продвинутых шаблонов разработки, никакого злоупотребления интерфейсами и абстрактными классами, ограниченное использование RTTI, определение данных в виде структур.
Пока наш код ничем особо не отличается от многих других проектов на басе «Си с классами», которые так критикуют многие программисты на C++.
Вот несколько интересных решений от разработчиков Doom 3, которые помогут нам понять их секрет:
Используйте общий базовый класс с полезными сервисами
Многие классы наследуют от idClass:
idClass предоставляет следующие сервисы:
- Создание представителей класса.
- Управление классом type_info
- Управление событиями
Облегчите построчную обработку
Как правило, string – наиболее часто используемый тип данных в проекте. Зачастую обработка ведется именно при помощи string, а для управления ими нам потребуются функции.
Doom 3 использует класс idstr, который содержит почти все нужные методы управления строками. Вам не нужно задавать собственный метод, как часто бывает со многими строковыми классами, которые предоставляют другие фреймворки.
Исходный код отделен от GUI-фреймворка (MFC)
Во многих проектах, где задействуется MFC, код тесно сплетен с их типами, и вы находите типы MFC в самых разных частях кода.
Что же касается Doom3, код здесь отделен от MFC, и только GUI-классы и MFC имеют прямую зависимость. Взгляните на этот запрос CQLinq:
Подобное решение значительно влияет на производительность. И в самом деле, фреймворк MFC – это вотчина разработчиков GUI, поэтому другие разработчики не должны тратить свое время на MFC.
Очень хорошая вспомогательная библиотека idlib
Почти во всех проектах очень часто используются вспомогательные классы. Это показано в результатах следующего запроса:
Как видите, больше всего здесь именно вспомогательных классов. Если разработчики не используют качественный фреймворк для вспомогательных классов, они тратят большую часть времени на борьбу с техническим слоем.
Idlib обеспечивает вас полезными классами со всеми необходимыми методами для управления строками, контейнерами и памятью. Это облегчает работу разработчикам и позволяет им уделять больше внимания логике.
Реализация проста для понимания
В Doom 3 используется встроенный компилятор, а программисты на C++ знают, что написание парсеров и компиляторов – весьма трудоемкая задача. С другой стороны, реализация Doom 3 проста для понимания, а код игры чист и изящен.
Вот график зависимости используемых компилятором классов:
А это фрагмент программы из исходного кода компилятора:
Мы изучили исходный код многих парсеров и компиляторов, но нам впервые попадается компилятор с таким легким для понимания кодом, что можно сказать обо всем исходном коде Doom 3. Это настоящая магия. Исследуя исходный код Doom 3, так и хочется воскликнуть: «Как же он прекрасен!»
Выводы
Несмотря на то, что в Doom3 используются самые базовые дизайнерские решения, разработчики постарались сделать так, чтобы они могли сосредоточиться на игровой логике.
С другой стороны, при использовании метода «Си с классами» нужно четко понимать, что вы делаете. Нужно быть настоящим профессионалом не хуже разработчиков Doom 3. Начинающим программистам не рекомендуется рисковать и игнорировать современные рекомендации по C++.