Загрузка...
Загрузка...
SOLID — это набор пяти принципов объектно-ориентированного проектирования, которые помогают писать код, легко поддающийся поддержке, тестированию и расширению. Эти принципы были сформулированы Робертом Мартином (Robert C. Martin), известным также как «Дядюшка Боб».
Каждый класс или модуль должен иметь единственную обязанность и, соответственно, единственную причину для изменения.
Зачем?
Пример (JS)
// Плохо: класс делает слишком много вещей — сохраняет данные и логирует
class UserService {
saveUser(user) {
// ...код для сохранения в базу
Logger.log(`User ${user.name} saved`);
}
}
// Лучше: выносим логику логирования в отдельный класс/сервис
class UserService {
constructor(logger) {
this.logger = logger;
}
saveUser(user) {
// ...код для сохранения в базу
this.logger.log(`User ${user.name} saved`);
}
}
class Logger {
log(message) {
console.log(`[LOG]: ${message}`);
}
}
Классы и модули должны быть открыты для расширения, но закрыты для изменения.
Простыми словами — модули надо проектировать так, чтобы их требовалось менять как можно реже, а расширять функциональность можно было с помощью создания новых сущностей и композиции их со старыми.
Зачем?
// Плохо: каждый раз добавляем новое условие в код
function getDiscount(user) {
if (user.type === 'regular') {
return 5;
}
if (user.type === 'vip') {
return 10;
}
// ...
}
// Лучше
class RegularUser {
getDiscount() {
return 5;
}
}
class VipUser {
getDiscount() {
return 10;
}
}
function showDiscount(user) {
// Не изменяем логику, а просто вызываем метод
return user.getDiscount();
}
Здесь мы можем легко добавить нового пользователя, реализовав его класс со своим методом getDiscount. При этом функцию showDiscount не придётся менять.
Классы-наследники должны быть полностью заменяемы своими родительскими классами. Если где-то ожидается объект родительского типа, мы должны иметь возможность подставить объект дочернего типа без нарушения работы программы.
Простыми словами — классы-наследники не должны противоречить базовому классу. Например, они не могут предоставлять интерфейс ýже базового. Поведение наследников должно быть ожидаемым для функций, которые используют базовый класс.
Зачем?
// Плохо: Square ломает поведение Rectangle
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
// Нарушаем LSP: при изменении ширины или высоты должно
// меняться и другое измерение, но это ломает логику родителя
setWidth(width) {
this.width = width;
this.height = width;
}
setHeight(height) {
this.width = height;
this.height = height;
}
}
В этом случае, если мы в коде ожидаем Rectangle и вызываем методы setWidth и setHeight, для Square получим не то поведение, которое предполагает базовый класс (у квадрата всегда ширина равна высоте).
Лучше — не наследовать Square от Rectangle, а либо использовать общий абстрактный класс (например, Shape), либо применить композицию. Таким образом, мы избегаем ломки базовой логики наследованием.
Клиенты (классы, модули) не должны зависеть от интерфейсов (или контрактов), которые они не используют. Если интерфейс «жирный» и содержит слишком много методов, лучше разбить его на несколько специализированных интерфейсов.
Зачем?
// Плохо: один класс, который содержит все методы
// (например, Animal с eat, walk, swim, fly)
class Animal {
eat() {}
walk() {}
swim() {}
fly() {}
}
// Класс Dog может не плавать, а класс Bird не ходить по земле
// и т.д. - получаются пустые или неиспользуемые методы.
// Лучше: разделяем ответственность
class Eatable {
eat() {}
}
class Walkable {
walk() {}
}
class Flyable {
fly() {}
}
// Теперь классы, которым надо летать, просто расширяют Flyable,
// а те, которым надо ходить, — Walkable, и так далее
class Dog extends Eatable {
// Дополнительно, если хотим, мы можем насладовать Dog от Walkable
// но не добавлять методы Flyable и Swimable, которые ему не нужны
}
Зависимости должны строиться на уровне абстракций, а не конкретных реализаций. Модули верхнего уровня не должны зависеть от модулей нижнего уровня напрямую — оба типа модулей зависят от абстракций.
Зачем?
// Плохо: класс напрямую зависит от конкретной реализации
class UserService {
constructor() {
this.db = new MySQLDatabase();
}
getUsers() {
return this.db.query('SELECT * FROM users');
}
}
// Лучше: через абстракцию (интерфейс/контракт)
class UserService {
constructor(database) {
// database — это абстрактный контракт, который должен уметь .query()
this.database = database;
}
getUsers() {
return this.database.query('SELECT * FROM users');
}
}
// Теперь мы можем подменять конкретную реализацию
class MySQLDatabase {
query(sql) {
// запрос в MySQL
}
}
class MongoDatabase {
query(sql) {
// запрос в Mongo
}
}
// В разных окружениях/приложениях
// мы можем передавать нужную реализацию в конструктор UserService
const userService = new UserService(new MySQLDatabase());
Здесь UserService не знает конкретных деталей реализации базы данных, он работает через абстракцию, которую предоставляют классы MySQLDatabase или MongoDatabase.