SOLID принципы в C#: теория и практика
SOLID — это акроним пяти фундаментальных принципов объектно-ориентированного программирования и проектирования, сформулированных Робертом Мартином (дядюшкой Бобом). Эти принципы помогают разработчикам создавать гибкий, поддерживаемый и масштабируемый код. В этой статье мы разберем каждый принцип SOLID на языке C# с конкретными примерами кода.
1. Single Responsibility Principle (SRP) — Принцип единственной ответственности
Теория: Каждый класс должен иметь только одну причину для изменения. Другими словами, класс должен отвечать за одну четко определенную задачу.
Практика на C#: Рассмотрим пример нарушения SRP и его исправление.
Нарушение SRP:
public class OrderService
{
public void CreateOrder(Order order)
{
// Логика создания заказа
Console.WriteLine("Заказ создан");
// Логика отправки email
SendEmail("order@example.com", "Новый заказ");
// Логика логирования
File.WriteAllText("log.txt", "Заказ создан");
}
}
Класс OrderService отвечает за три разные задачи: создание заказа, отправку email и логирование. Это нарушает SRP.
Исправление с соблюдением SRP:
public class OrderService
{
private readonly IEmailService _emailService;
private readonly ILogger _logger;
public OrderService(IEmailService emailService, ILogger logger)
{
_emailService = emailService;
_logger = logger;
}
public void CreateOrder(Order order)
{
// Логика создания заказа
Console.WriteLine("Заказ создан");
_emailService.SendEmail("order@example.com", "Новый заказ");
_logger.Log("Заказ создан");
}
}
public interface IEmailService
{
void SendEmail(string to, string message);
}
public class EmailService : IEmailService
{
public void SendEmail(string to, string message)
{
Console.WriteLine($"Email отправлен на {to}");
}
}
public interface ILogger
{
void Log(string message);
}
public class FileLogger : ILogger
{
public void Log(string message)
{
File.WriteAllText("log.txt", message);
}
}
Теперь каждый класс отвечает только за свою задачу: OrderService — за управление заказом, EmailService — за отправку писем, FileLogger — за логирование.
2. Open-Closed Principle (OCP) — Принцип открытости/закрытости
Теория: Программные сущности (классы, модули, функции) должны быть открыты для расширения, но закрыты для модификации.
Практика на C#: Используем наследование и интерфейсы для расширения функциональности без изменения существующего кода.
Нарушение OCP:
public class PaymentProcessor
{
public void ProcessPayment(string paymentType, decimal amount)
{
if (paymentType == "CreditCard")
{
// Обработка кредитной карты
}
else if (paymentType == "PayPal")
{
// Обработка PayPal
}
// Добавление нового типа требует изменения класса
}
}
Исправление с соблюдением OCP:
public interface IPaymentMethod
{
void Process(decimal amount);
}
public class CreditCardPayment : IPaymentMethod
{
public void Process(decimal amount)
{
Console.WriteLine($"Обработка кредитной карты: {amount}");
}
}
public class PayPalPayment : IPaymentMethod
{
public void Process(decimal amount)
{
Console.WriteLine($"Обработка PayPal: {amount}");
}
}
public class PaymentProcessor
{
public void ProcessPayment(IPaymentMethod paymentMethod, decimal amount)
{
paymentMethod.Process(amount);
}
}
// Использование
var processor = new PaymentProcessor();
processor.ProcessPayment(new CreditCardPayment(), 100.50m);
processor.ProcessPayment(new PayPalPayment(), 200.75m);
Теперь для добавления нового способа оплаты (например, ApplePayPayment) достаточно создать новый класс, реализующий IPaymentMethod, без изменения PaymentProcessor.
3. Liskov Substitution Principle (LSP) — Принцип подстановки Барбары Лисков
Теория: Объекты в программе должны быть заменяемы экземплярами их подтипов без изменения корректности программы. Проще говоря, производный класс должен расширять базовый, не изменяя его поведение.
Практика на C#: Классический пример нарушения — наследование класса Square от Rectangle.
Нарушение LSP:
public class Rectangle
{
public virtual int Width { get; set; }
public virtual int Height { get; set; }
public int GetArea() => Width * Height;
}
public class Square : Rectangle
{
public override int Width
{
set { base.Width = base.Height = value; }
}
public override int Height
{
set { base.Width = base.Height = value; }
}
}
// Использование
Rectangle rect = new Square();
rect.Width = 5;
rect.Height = 10;
Console.WriteLine(rect.GetArea()); // Ожидается 50, но получим 100
Исправление с соблюдением LSP:
public abstract class Shape
{
public abstract int GetArea();
}
public class Rectangle : Shape
{
public int Width { get; set; }
public int Height { get; set; }
public override int GetArea() => Width * Height;
}
public class Square : Shape
{
public int Side { get; set; }
public override int GetArea() => Side * Side;
}
// Использование
Shape shape1 = new Rectangle { Width = 5, Height = 10 };
Shape shape2 = new Square { Side = 5 };
Console.WriteLine(shape1.GetArea()); // 50
Console.WriteLine(shape2.GetArea()); // 25
Теперь Square и Rectangle — независимые классы, наследующие общий абстрактный класс Shape, что полностью соответствует LSP.
4. Interface Segregation Principle (ISP) — Принцип разделения интерфейса
Теория: Клиенты не должны зависеть от интерфейсов, которые они не используют. Лучше иметь несколько специализированных интерфейсов, чем один большой.
Практика на C#: Разделим большой интерфейс на несколько маленьких.
Нарушение ISP:
public interface IWorker
{
void Work();
void Eat();
void Sleep();
}
public class HumanWorker : IWorker
{
public void Work() { /* работает */ }
public void Eat() { /* ест */ }
public void Sleep() { /* спит */ }
}
public