Если ты только начал программировать на 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!