Ответ
SOLID — это акроним пяти фундаментальных принципов объектно-ориентированного проектирования и программирования, направленных на создание понятного, гибкого и поддерживаемого кода.
1. Принцип единственной ответственности (Single Responsibility Principle - SRP)
Один класс должен иметь одну и только одну причину для изменения.
Класс должен решать строго одну задачу или отвечать за одну функциональность. Это упрощает тестирование, понимание и снижает связность.
// НАРУШЕНИЕ: Класс отвечает и за логику заказа, и за его сохранение, и за логирование.
public class OrderProcessor
{
public void Process(Order order)
{
// Валидация заказа...
// Применение бизнес-правил...
this.SaveToDatabase(order); // Ответственность за персистентность
this.Log($"Order {order.Id} processed"); // Ответственность за логирование
}
private void SaveToDatabase(Order order) { /* ... */ }
private void Log(string message) { /* ... */ }
}
// СОБЛЮДЕНИЕ: Разделение ответственностей.
public class OrderProcessor // Отвечает только за бизнес-логику обработки
{
private readonly IOrderRepository _repository;
private readonly ILogger _logger;
// Зависимости внедряются извне (DIP)
public OrderProcessor(IOrderRepository repo, ILogger logger)
{
_repository = repo;
_logger = logger;
}
public void Process(Order order)
{
// Валидация и бизнес-правила...
_repository.Save(order); // Делегируем сохранение
_logger.Log($"Order {order.Id} processed"); // Делегируем логирование
}
}
public interface IOrderRepository { void Save(Order order); } // Ответственность за данные
public interface ILogger { void Log(string message); } // Ответственность за логи
2. Принцип открытости/закрытости (Open/Closed Principle - OCP)
Программные сущности должны быть открыты для расширения, но закрыты для модификации.
Новую функциональность следует добавлять через создание новых классов (наследование, композиция), а не изменяя код существующих, уже протестированных.
// НАРУШЕНИЕ: Для добавления новой фигуры нужно менять метод AreaCalculator.
public class AreaCalculator
{
public double CalculateArea(object shape)
{
if (shape is Rectangle r) return r.Width * r.Height;
if (shape is Circle c) return Math.PI * c.Radius * c.Radius;
// Добавляем новый `if` для Triangle -> ИЗМЕНЕНИЕ КЛАССА!
throw new ArgumentException("Unknown shape");
}
}
// СОБЛЮДЕНИЕ: Используем абстракцию. Новые фигуры расширяют систему, не изменяя калькулятор.
public abstract class Shape
{
public abstract double CalculateArea(); // Закрыт для изменений (абстрактный)
}
public class Rectangle : Shape { /* ... */ public override double CalculateArea() => Width * Height; }
public class Circle : Shape { /* ... */ public override double CalculateArea() => Math.PI * Radius * Radius; }
public class Triangle : Shape { /* ... */ public override double CalculateArea() => Base * Height / 2; } // РАСШИРЕНИЕ
public class AreaCalculator
{
// Метод теперь зависит от абстракции Shape. Он ЗАКРЫТ для изменений.
public double CalculateArea(Shape shape) => shape.CalculateArea();
}
3. Принцип подстановки Барбары Лисков (Liskov Substitution Principle - LSP)
Объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности программы.
Наследник не должен ужесточать предусловия или ослаблять постусловия базового класса. Клиентский код, работающий с базовым классом, должен корректно работать и с любым его наследником.
// НАРУШЕНИЕ: Класс Square нарушает контракт класса Rectangle (можно менять ширину и высоту независимо).
public class Rectangle { public virtual int Width { get; set; } public virtual int Height { get; set; } }
public class Square : Rectangle
{
public override int Width { set { base.Width = base.Height = value; } }
public override int Height { set { base.Width = base.Height = value; } }
}
// Клиентский код ломается:
void TestArea(Rectangle r)
{
r.Width = 5;
r.Height = 4;
Console.WriteLine(r.Width * r.Height); // Ожидает 20, но для Square получит 16.
}
4. Принцип разделения интерфейса (Interface Segregation Principle - ISP)
Много специализированных интерфейсов лучше, чем один универсальный.
Клиенты не должны зависеть от методов, которые они не используют. Большие "жирные" интерфейсы приводят к тому, что классы вынуждены реализовывать ненужные методы (пустыми или с исключениями).
// НАРУШЕНИЕ: Принтер-сканер вынужден реализовывать метод Fax, который ему не нужен.
public interface IMultiFunctionDevice
{
void Print(Document d);
void Scan(Document d);
void Fax(Document d); // Не всем устройствам нужен факс!
}
public class OldPrinter : IMultiFunctionDevice
{
public void Print(Document d) { /* OK */ }
public void Scan(Document d) { throw new NotImplementedException(); } // НЕ НУЖЕН!
public void Fax(Document d) { throw new NotImplementedException(); } // НЕ НУЖЕН!
}
// СОБЛЮДЕНИЕ: Разделяем на мелкие интерфейсы.
public interface IPrinter { void Print(Document d); }
public interface IScanner { void Scan(Document d); }
public interface IFax { void Fax(Document d); }
public class OldPrinter : IPrinter { public void Print(Document d) { /* ... */ } } // Только то, что нужно
public class Photocopier : IPrinter, IScanner { /* ... */ } // Композиция интерфейсов
5. Принцип инверсии зависимостей (Dependency Inversion Principle - DIP)
Зависимости должны строиться относительно абстракций, а не деталей.
- Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Этот принцип лежит в основе внедрения зависимостей (Dependency Injection) и использования IoC-контейнеров.
// НАРУШЕНИЕ: Класс высокоуровневой логики напрямую зависит от конкретной реализации низкоуровневой (базы данных).
public class OrderService
{
private readonly SqlServerOrderRepository _repository; // Зависимость от детали
public OrderService()
{
_repository = new SqlServerOrderRepository(); // Жёсткое создание зависимости
}
public void ProcessOrder(Order order) => _repository.Save(order);
}
// СОБЛЮДЕНИЕ: Зависим от абстракции (интерфейса). Конкретная реализация внедряется извне.
public class OrderService // Модуль верхнего уровня (бизнес-логика)
{
private readonly IOrderRepository _repository; // Зависимость от абстракции
// Внедрение зависимости через конструктор (Constructor Injection)
public OrderService(IOrderRepository repository) // Абстракция, а не деталь
{
_repository = repository; // Может быть SqlServer, PostgreSQL, InMemory репозиторий
}
public void ProcessOrder(Order order) => _repository.Save(order);
}
// Абстракция, от которой зависят и OrderService, и все репозитории
public interface IOrderRepository { void Save(Order order); }
// Деталь (низкоуровневый модуль) зависит от абстракции
public class SqlServerOrderRepository : IOrderRepository { public void Save(Order order) { /* ... */ } }
public class PostgreSQLOrderRepository : IOrderRepository { public void Save(Order order) { /* ... */ } }
Итог: Следование SOLID приводит к созданию кода с низкой связностью (loose coupling) и высокой связностью (high cohesion), что значительно упрощает его тестирование, расширение и поддержку в долгосрочной перспективе.
Видео-ответы
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶
▶