Введение: Почему SOLID — основа качественного C# кода
Разработка на C# в экосистеме .NET требует не только знания синтаксиса, но и умения проектировать гибкие, поддерживаемые и тестируемые приложения. SOLID — это аббревиатура пяти фундаментальных принципов объектно-ориентированного проектирования, сформулированных Робертом Мартином. Они помогают разработчикам избегать "жёстких" зависимостей, дублирования кода и проблем при масштабировании.
В этой статье мы разберём каждый из пяти принципов SOLID применительно к C#: от теории до реальных примеров кода. Вы узнаете, как рефакторинг с учётом SOLID делает ваш код чище, а поддержку проектов — проще.
1. Single Responsibility Principle (SRP) — Принцип единственной ответственности
Теория
Класс должен иметь только одну причину для изменения. Другими словами, каждый класс отвечает ровно за одну функциональность или бизнес-логику. Если класс делает слишком много, его становится сложно тестировать, изменять и переиспользовать.
Практика: нарушение SRP
public class OrderProcessor
{
public void ProcessOrder(Order order)
{
// 1. Валидация заказа
if (order.Total <= 0) throw new Exception("Invalid order");
// 2. Сохранение в базу данных
using var db = new AppDbContext();
db.Orders.Add(order);
db.SaveChanges();
// 3. Отправка email-уведомления
SmtpClient client = new SmtpClient();
client.Send("admin@shop.com", order.CustomerEmail, "Order confirmed", "Thank you!");
}
}
Здесь OrderProcessor нарушает SRP: он отвечает за валидацию, работу с БД и отправку email. Изменение логики отправки уведомлений потребует изменения этого же класса.
Рефакторинг с SRP
public class OrderValidator
{
public bool Validate(Order order) => order.Total > 0;
}
public class OrderRepository
{
public void Save(Order order)
{
using var db = new AppDbContext();
db.Orders.Add(order);
db.SaveChanges();
}
}
public class EmailService
{
public void SendConfirmation(string email)
{
SmtpClient client = new SmtpClient();
client.Send("admin@shop.com", email, "Order confirmed", "Thank you!");
}
}
public class OrderProcessor
{
private readonly OrderValidator _validator;
private readonly OrderRepository _repository;
private readonly EmailService _emailService;
public OrderProcessor(OrderValidator validator, OrderRepository repository, EmailService emailService)
{
_validator = validator;
_repository = repository;
_emailService = emailService;
}
public void ProcessOrder(Order order)
{
if (!_validator.Validate(order)) throw new Exception("Invalid order");
_repository.Save(order);
_emailService.SendConfirmation(order.CustomerEmail);
}
}
Теперь каждый класс имеет одну зону ответственности. Это упрощает тестирование и модификацию.
2. Open/Closed Principle (OCP) — Принцип открытости/закрытости
Теория
Программные сущности (классы, модули, функции) должны быть открыты для расширения, но закрыты для модификации. То есть вы должны иметь возможность добавлять новую функциональность, не изменяя существующий код.
Практика: нарушение OCP
public class ShapeDrawer
{
public void DrawShape(object shape)
{
if (shape is Circle circle)
Console.WriteLine($"Drawing circle with radius {circle.Radius}");
else if (shape is Rectangle rect)
Console.WriteLine($"Drawing rectangle {rect.Width}x{rect.Height}");
// При добавлении нового типа фигуры — нужно изменять этот метод!
}
}
Рефакторинг с OCP через абстракцию
public abstract class Shape
{
public abstract void Draw();
}
public class Circle : Shape
{
public double Radius { get; set; }
public override void Draw() => Console.WriteLine($"Drawing circle with radius {Radius}");
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override void Draw() => Console.WriteLine($"Drawing rectangle {Width}x{Height}");
}
public class ShapeDrawer
{
public void DrawShape(Shape shape) => shape.Draw(); // Закрыто для модификации
}
Теперь для добавления новой фигуры (например, Triangle) достаточно создать новый класс, наследующий Shape, — код ShapeDrawer не трогается.
3. Liskov Substitution Principle (LSP) — Принцип подстановки Барбары Лисков
Теория
Объекты в программе должны быть заменяемыми экземплярами их подтипов без изменения корректности программы. Если класс B наследует класс A, то мы должны иметь возможность использовать B везде, где ожидается A, без неожиданных побочных эффектов.
Практика: нарушение LSP
public class Bird
{
public virtual void Fly() => Console.WriteLine("Flying");
}
public class Penguin : Bird
{
public override void Fly() => throw new Exception("Penguins can't fly!"); // Нарушение LSP
}
Если клиентский код ожидает, что все птицы могут летать, подстановка Penguin вызовет исключение.
Решение: правильная иерархия
public abstract class Bird { }
public interface IFlyable
{
void Fly();
}
public class Sparrow : Bird, IFlyable
{
public void Fly() => Console.WriteLine("Sparrow flying");
}
public class Penguin : Bird { } // Не реализует IFlyable — логично
Используйте интерфейсы для выделения поведения. Тогда код, работающий с IFlyable, гарантированно не сломается.
4. Interface Segregation Principle (ISP) — Принцип разделения интерфейсов
Теория
Клиенты не должны зависеть от интерфейсов, которые они не используют. Вместо одного "толстого" интерфейса лучше создать несколько специализированных.
Практика: нарушение ISP
public interface IWorker
{
void Work();
void Eat();
void Sleep();
}
public class Robot : IWorker
{
public void Work() => Console.WriteLine("Working");
public void Eat() => throw new Exception("Robots don't eat"); // Ненужный метод
public void Sleep() => throw new Exception("Robots don't sleep");
}
Рефакторинг с ISP
public interface IWorkable
{
void Work();
}
public interface I