Pull to refresh

Принципы SOLID vs KISS. Как найти баланс?

Level of difficultyEasy
Reading time4 min
Views17K

Привет, Хабр! Сегодня я хотел бы поделиться опытом, как писать код так, чтобы системы в проекте были с одной стороны гибкими и модульными, а с другой — простыми и компактными.

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

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

  • Жирные God-Object классы, которые делают все;

  • Мелкие классы, сплетенные в Spagetti-Code.

Знакомо? И в том и в другом случае неудобно поддерживать проект, поскольку в первом случае нарушен принцип единственной ответственности, который гласит, что каждый класс должен иметь одну причину для изменения; а во втором — нарушен принцип KISS, который говорит о том, что более простые системы лучше поддерживаются, в них меньше багов, и они реже дают сбои.

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

Это значит, что иногда нужно нарушать принципы SOLID ради простоты и компактности системы, и закладывать гибкость только в тех местах, где это действительно нужно. Никакого "сделаю тут более универсально на будущее" не приветствуется, потому что вы никогда не знаете на 100%, в каких местах ваш проект будет развиваться. В результате вы потеряете время, силы и энергию. Особенно печально будет, когда поймете, что ваше "универсальное решение" не состыкуется с реальным техническим заданием.

Тут же парирую: если команда точно знает проект, будущий объем задач, модули и архитектуру, то тогда можно заранее заложить точки гибкости.

Теперь расскажу, как это делаю я:

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

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

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

  • Критическое нарушение SRP. Класс нужно разбить по SRP, если он выполняет ответственности разного рода или в нем прослеживается сильное зацепление. Например, система врагов, которая помимо управления противниками, занимается квестами игрока; или класс Race на 600+ строк кода, который управляет машинками противников (ИИ), запускает обратный отсчет и контролирует чекпоинты, которые проехал игрок. Это реальные примеры, которые я видел в GameDev.

  • Некритическое нарушение SRP: Класс желательно разбить по SRP, если он выполняет группу ответственностей одного рода. Например, менеджер квестов, который занимается созданием, хранением квестов, а также их генерацией и выдачей награды. Несмотря на то, что менеджер имеет много ответственностей, это класс на 300 строк легко читается, поскольку все ответственности сфокусированы вокруг квестов, и в нем соблюдается высокая связность. Если этот менеджер будет в процессе разработки и дальше развиваться, то его можно будет подразбить на более маленькие классы, с целью повышения читаемости кода. Другой пример, который я встречал в разработке, — это жирный класс пользовательского интерфейса игрока, который имеет много ответственностей, но в целом он отвечает за логику представления. Такой класс тоже можно подразбить, если какая-то логика View будут переиспользоваться, или класс будет разрастаться.

Теперь расскажу, как рефакторить спагетти-код.

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

  • Второй вариант более сложный. Нужно нарисовать диаграмму-классов и выписать все ответственности, которые есть в этой "паутине". После этого у вас будет понимание, какие классы можно объединить, а какие наоборот разделить по ответственностям. Другими словами применить шаблоны GRASP: низкое зацепление и высокая связность.

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

  • делай раз: пишем код, который будет работать;

  • делай два: рефакторим код, соблюдая баланс SOLID / KISS.

Таким образом, искать золотую середину в коде сложно. Нужен огромный опыт практики, самоанализа, знание шаблонов GRASP, паттернов GoF и принципов SOLID, KISS, DRY, YAGNI. Надеюсь, что изложенный материал поможет вам писать код лучше.

В завершении скажу, что я буду разрабатывать игровые механики 26-го декабря в 19:00 по МСК на Youtube у себя на канале. Более подробная информация будет на онлайн-курсе. Также, если у вас будет желание посетить мой телеграмм канал, буду рад!

Предыдущие стримы по разработке игр: Введение в атомарный подходКомпоненты и секции.

Благодарю! ?

Tags:
Hubs:
Total votes 9: ↑4 and ↓5-1
Comments40

Articles