SOLID принципы в C#: теория и практика с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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                

Блоги

Рекомендаций по книгам