Если ты только начал программировать на Unity C#, то наверняка слышал про SOLID.
На каждом собесе спрашивают: "Что такое SOLID? Зачем он нужен? Приведи пример". Но когда ты новичок, кажется, что это что-то сложное и непонятное. Давай разберёмся вместе - просто, по-человечески и с примерами из жизни.
Что такое SOLID?
Вот стандартное описание которое пишут всегда:
SOLID - это пять простых правил, которые помогают писать понятный и удобный код. Если следовать этим правилам, твой проект будет проще поддерживать, дорабатывать и не будет превращаться в кашу.
SOLID - это аббревиатура:
S - Single Responsibility Principle (Принцип единственной ответственности)
O - Open/Closed Principle (Принцип открытости/закрытости)
L - Liskov Substitution Principle (Принцип подстановки Барбары Лисков)
I - Interface Segregation Principle (Принцип разделения интерфейса)
D - Dependency Inversion Principle (Принцип инверсии зависимостей)
Но зачем все таки нужен SOLID. Чем он помогает. И что вообще это такое?
Когда ты только начинаешь программировать, кажется, что можно просто писать код, и всё будет работать. Но со временем проект растёт, появляются новые фичи, баги, задачи.
Если не соблюдать SOLID, код становится запутанным, его сложно менять, а ошибки появляются всё чаще.
Представь, что ты строишь дом. Сначала всё просто - фундамент, стены, крыша. Но если не следовать правилам строительства, дом быстро начнёт разваливаться: где-то трещина, где-то дверь не закрывается, а если захочешь пристроить балкон - придётся ломать полдома. Так и с кодом: без принципов всё держится на честном слове, и любое изменение может всё сломать.
SOLID - это как инструкция по строительству: если ей следовать, твой проект будет расти без хаоса. Ты сможешь:
Легко читать и понимать свой код (даже если вернёшься к нему через месяц или полгода)
Быстро добавлять новые фичи, не боясь, что что-то сломается
Легко находить и исправлять ошибки
Не бояться изменений - твой код будет гибким и устойчивым
Работать в команде: другим будет проще разобраться в твоём проекте
Пример из Unity:
Если ты пишешь игру и не следуешь SOLID, то через пару месяцев твой скрипт PlayerController может вырасти до 1000 строк, где намешано всё: движение, стрельба, здоровье, управление UI и даже музыка. Исправить баг или добавить новую механику становится мучением. А если ты разделяешь код по принципам SOLID - каждый скрипт отвечает только за своё, и менять или расширять игру становится легко и приятно.
Примеры из жизни и кода для каждого принципа:
S - Single Responsibility Principle
Один класс - одна ответственность
Один человек - одна задача.
Пример:
В кафе есть бариста, который варит кофе, и повар, который готовит еду. Если бариста начнёт готовить бургеры, а повар - варить кофе, будет бардак. Пусть каждый делает своё дело.
Пример в коде:
// Класс только для движения игрока
public class PlayerMover : MonoBehaviour {
public void Move(Vector3 direction) {
transform.Translate(direction);
}
}
Что происходит в коде:
Класс PlayerMover отвечает только за движение игрока и не занимается ничем другим. Это и есть принцип одной ответственности.
O - Open/Closed Principle
Открыт для расширения, закрыт для изменения
Добавляй новое, не ломая старое.
Пример:
В телефоне можно поставить новое приложение, не перепрошивая всю систему. Так и в коде: добавляешь новую фичу - не трогаешь рабочие части.
Хочешь добавить новый тип врага - не переписывай весь GameManager, а просто добавь новый класс врага.
Пример в коде:
// Базовый класс врага
public abstract class Enemy : MonoBehaviour {
public abstract void Attack();
}
// Добавляем новый тип врага без изменения существующего кода
public class Zombie : Enemy {
public override void Attack() { /* ... */ }
}
public class Robot : Enemy {
public override void Attack() { /* ... */ }
}
Что происходит в коде:
Ты можешь добавлять новых врагов (Zombie, Robot), не меняя код базового класса Enemy. Старый код не ломается, а расширяется.
Почему используется abstract class?
Абстрактный класс Enemy - это как общий шаблон для всех врагов. В нём описано, что у любого врага должна быть атака (метод Attack), но как именно атаковать - решает каждый конкретный враг (Zombie, Robot). Благодаря этому ты можешь создавать новых врагов, просто наследуя этот шаблон, и не трогать уже написанный код. Это и есть суть принципа: расширяем, не изменяя старое.
L - Liskov Substitution Principle
Замена без сюрпризов.
Пример:
Ты арендуешь машину. Какая бы марка ни была - главное, чтобы она ехала и тормозила. Так и в коде: если функция ждёт "животное", можно подставить "кошку" или "собаку", и всё будет работать.
Пример в коде:
// Любой Enemy можно подставить
void DamageEnemy(Enemy enemy) {
enemy.Attack();
}
Что происходит в коде:
Функция DamageEnemy работает с любым наследником Enemy - не важно, зомби это или робот. Всё будет работать одинаково.
I - Interface Segregation Principle
Лучше несколько маленьких меню, чем одно огромное.
Пример:
В ресторане есть отдельное меню для напитков и отдельное для десертов. Никто не хочет искать среди 50 страниц мороженое.
Пример в коде:
// Маленькие интерфейсы
public interface IShoot {
void Shoot();
}
public interface IReload {
void Reload();
}
// Пистолет реализует оба интерфейса
public class Pistol : IShoot, IReload {
public void Shoot() { /* стреляет */ }
public void Reload() { /* перезаряжается */ }
}
// Граната реализует только стрельбу
public class Grenade : IShoot {
public void Shoot() { /* взрывается */ }
// Нет метода Reload
}
Что происходит в коде:
Вместо одного большого интерфейса для оружия, есть отдельные интерфейсы для стрельбы и перезарядки. Класс может реализовать только то, что ему нужно.
Что такое interface?
Interface - это как договор или инструкция, в которой написано, какие методы должны быть у класса. Но сам interface не содержит реализацию - он только говорит: "Если ты реализуешь этот interface, у тебя обязательно должен быть такой-то метод". Например, если класс реализует IShoot, значит, в нём обязательно будет метод Shoot(). Это помогает делать код более гибким и понятным.
D - Dependency Inversion Principle
Зависеть от абстракций, а не от деталей.
Пример:
Ты вызываешь такси через приложение. Тебе всё равно, какая машина приедет - главное, чтобы она довезла до точки Б.
Пример в коде:
// Работаем с абстракцией
public class WeaponUser {
private IShoot weapon;
public WeaponUser(IShoot weapon) {
this.weapon = weapon;
}
public void UseWeapon() {
weapon.Shoot();
}
}
Что происходит в коде:
Класс WeaponUser работает с любым оружием, которое реализует интерфейс IShoot. Можно легко подменить пистолет на лазер - код не изменится.
Итог
SOLID - это пять простых принципов, которые помогают не превращать твой код в кашу.
Каждый принцип - это напоминание: не усложняй, не повторяйся, не делай лишнего, не мешай всё в одну кучу и не делай код зависимым.
А так же рекомендую изучить принципы KISS, DRY, YAGNI и BDUF - они отлично дополняют SOLID!
Комментарии (6)
SadOcean
26.08.2025 12:38Статья, как водится, пустовата.
Возможно полезнее объяснять, не что делать, но почему.
Вот, например, другой разбор.
Не то, как вам следует использовать SOLID, а как его использовала сама Unity и где можно поучиться.
Single responsibility principle - про единую ответственность.
Это необходимо для организации, чтобы код был написан так, чтобы смежные изменения затрагивали ограниченное количество файлов, а не расползались по системе. Важно тут не то, что функция/класс делает что-то одно, а то, что она делает это в интересах одной цели.
Unity великолепно следует этому принципу и поставила его во главу угла. Любая сцена состоит из объектов и компонентов на них. Компоненты - буквальное воплощение принципа. Каждый имеет свою задачу и независим от других. Если ошибка будет в модуле, ответственном за текст - вам не нужно менять компоненты, ответственные за физику.
В некоторых случаях компоненты могут быть связаны, но даже тут они сгруппированы или имеют прямые ссылки друг на друга. К примеру кнопка явно использует графический компонент (например картинку) как свою визуализацию.
Вам следует стараться писать ваши логические компоненты в той же парадигме, насколько это возможно.
Иногда для сложных систем этого недостаточно - многие объекты должны знать друг о друге. На это призваны ответить различные паттерны организации и принципы, которые выходят за рамки этого принципа, тем не менее он остается полезен.
Open close principle - есть много трактовок этого принципа, но в общем случае это можно понимать как "пишите код, который нужно дополнять, а не переписывать".
Тут есть множество нюансов, но юнити - хороший пример, который предполагает, что вы будете писать свои компоненты, которые могут легко взаимодействовать со встроенными. Если вам нужно окно - ваше окно может использовать кнопки, тексты и картинки из стандартных компонентов.
Если нужно будет сделать другое - можно выкинуть код этого компонента и написать новый - виджеты не пострадают.
Liscov Substitution Principle - тут довольно просто, все дети должны вести себя как минимум как родители.
Основная причина - те, кто используют список базовых классов должны получать базовое же поведение, иначе старый и протестированный код можно поломать, написав новые классы (что очень тупо - ошибка то в новых классах, почему страдать должны старые?)
Если базовый класс поддерживает операции "нарисуй себя" и "обработай нажатие", то все наследники должны это делать, пусть и по своему. Как минимум они не должны падать с ошибкой или делать кульбиты при отрисовке. Если они реализуют пустую функцию-заглушку - это тоже может быть признаком плохого кода (хотя тут могут быть нюансы)
Unity использует это регулярно - у него есть большая иерархия компонентов и пользователь может наследоваться от многих из них и получать стандартное поведение компонентов.
Важно отметить, что зачастую не используется прямое наследование (Например Unity находит и вызывает метод Update как то хитро и скрыто).
Тем не менее Unity написала тысячи, а пользователи - сотни тысяч и миллионов компонентов, которые ведут себя как компоненты. Что позволяет вам их легко дополнять и даже переиспользовать чужие.
Interface Segregation Principle - тут все просто, интерфейсы должны быть маленькими и специализированными.
Основная причина - меньше неиспользуемого кода, более понятные компоненты.
Unity использует это даже без интерфейсов со своими основными методами - Update(), OnEnable(), OnDisable(), OnCollisionEnter(collider)
Другой пример - интерфейсы для UI
- IPointerClickHandler
- IPointerDownHandler
- IPointerEnterHandler
- IPointerExitHandler
- IPointerUpHandler
Как видно они связаны и, например, для реализации drag and drop нужно унаследовать несколько. Тем не менее для простых компонентов можно унаследовать лишь один и избежать пустого кода.
Dependency Inversion Principle - это пожалуй самый важный принцип для организации высокоуровневых связей.
Если писать код напрямую, то можно получить очень связанную лапшу - ведь кнопка вызывает игру, игра загружает сцену, сцена загружает солдатика а тот загружает пули.
Если каждый компонент из списка будет знать друг о друге, то во первых будет месиво, во вторых - код будет хрупким. Удали что-то из списка (например солдатика) и у тебя поломаются сразу сцена и пули.
Чтобы этого избежать, прямые зависимости нужно ограничивать только необходимыми местами, а связи осуществлять через абстракции.
Примеры из Unity
- OnCollisionEnter - вам не нужно знать про коллайдер физики и его тип. А коллайдеру физики не нужно знать про ваш компонент (и все возможные пользовательские компоненты). Вы оба знаете только про абстракцию - интерфейс (в данном случае это метод и Unity использует хак, но принцип тот же). Таким образом любая реализация физики может вызвать столкновение (просто вызвать OnCollisionEnter на всех компонентах, какими бы они ни были). Вы даже можете сделать свой тип коллайдера.
А любой логический компонент может быть потребителем события столкновения. Ему не нужно знать про существующие типы коллайдеров и с каким коллайдером мы работаем сейчас.
Получается компоненты можно расширять в 2 -х направлениях совершенно не беспокоясь о том, как они будут дружить, и все они не будут знать друг о друге ничего, потому что не нужно.
Другой пример абстракций - это свойства базового компонента. Unity знает, что компонент может быть включен и выключен (enable)
И если он включен - нужно вызывать Update().
Движок не интересует, что это за компонент и как он работает.
Именно за счет этого Unity так мощна а пользователи могут легко делать и переиспользовать компоненты.
Jijiki
26.08.2025 12:38а как нарисовать в Юнити на gpu-карточке точку или линию ?) цветную без текстуры например, разного размера
и в 3д камерой обойти точку/линию вокруг )
Emelian
Лучше бы вместо принципов программирования объясняли бы технологию программирования. Например, как правильно организовывать интерфейс пользователя в программе обучения иностранному языку?
Там известные принципы мало чем помогают, ООП – еще немного рулит. А под GUI нужно еще выстраивать программную логику, поскольку логика сильно зависит от «графики». Чтобы оптимизировать логику, нужно оптимизировать взаимодействие графических примитивов. А там не все очевидно, тем более, что «графика» очень часто не своя, а чужая.
Я себе чуть голову не сломал, когда занимался разработкой соответствующего алгоритма. Одних только блок-схем нарисовал, для себя, более двадцати. И в текстовом виде описывал алгоритм десятки раз, с учетом его фрагментов. В итоге, проблему решил и опубликовал в последних статьях, но главный принцип программирования, который я вынес оттуда, это: «Метод здравого смысла».
А все эти солиды, киссы, дрюи и тому подобное интеллектуальное бла-бла-бла, по большому счету, мало что дают. Анализируйте, внимательно, хороший опенсорсный код и развивайте свой здравый смысл, и будет вас счастье!
Jijiki
скорее всего шейдерами в рамках Юнити, но вообще можно проще обойтись надо помимо отрисовки еще разобраться с тем как работает процесс, потомучто игра это процесс, надо чтобы процесс был зарегестрирован и приложение получало валидные состояния, а не висело в топе поидее, потомучто на простое будет нагрузка и ну надо разобраться как работает планировщик виндовс всё таки, и как это дело устроено в Юнити
просто Delay(потомучто надо чтобы от процесса(относительно планировщика) было управление текущим состоянием процесса, а не просто пропуск кеш линий) может не помочь процесс может получить пограничное состояние со всеми вытекающими, соответственно отсюда тянется ветка к изучению как это устроено