Введение: две стороны одной монеты
Представьте, что вы строите дом. Можно взять готовые блоки (классы, объекты, наследование) и собрать из них сложную конструкцию — это объектно-ориентированное программирование (ООП). А можно мыслить как математик: "я беру данные, применяю к ним чистую функцию и получаю новый результат, ничего не меняя" — это функциональное программирование.
В C# удивительным образом уживаются обе парадигмы. И если вы пишете на этом языке, рано или поздно зададитесь вопросом: "А что, если я делаю это неправильно? Может, стоит больше использовать LINQ и меньше классов?"
В этой статье мы не будем спорить, что лучше. Мы разберёмся, чем функциональный подход отличается от ООП в C#, когда каждый из них реально помогает, а когда — вредит. И да, будет много кода. Поехали!
1. ООП в C#: классика, которая работает
Объектно-ориентированное программирование — это про состояние и поведение. У вас есть класс, который хранит данные (поля, свойства) и методы, которые эти данные изменяют.
Пример: приложение для управления заказами в интернет-магазине.
public class Order
{
public int Id { get; set; }
public List<string> Items { get; private set; }
public decimal TotalPrice { get; private set; }
public Order(int id)
{
Id = id;
Items = new List<string>();
TotalPrice = 0;
}
public void AddItem(string itemName, decimal price)
{
Items.Add(itemName);
TotalPrice += price;
}
public void ApplyDiscount(decimal percent)
{
TotalPrice -= TotalPrice * (percent / 100);
}
}
Что мы видим? Инкапсуляция: TotalPrice нельзя изменить напрямую — только через методы. Состояние объекта меняется со временем: мы добавляем товары, применяем скидки. Это чистое ООП.
Плюсы ООП в C#
- Понятная модель реального мира: Заказ, Клиент, Товар — всё как в жизни.
- Повторное использование через наследование и интерфейсы.
- Управление сложностью за счёт разбиения на маленькие объекты.
Минусы ООП
- Побочные эффекты: один метод может менять состояние, и вы легко теряете контроль.
- Сложность тестирования: нужно создавать объекты, мокать зависимости.
- Избыточность: иногда проще написать функцию, чем создавать целый класс.
2. Функциональный подход в C#: LINQ и не только
Функциональное программирование — это про чистые функции (без побочных эффектов) и неизменяемость данных. В C# оно особенно заметно в LINQ, лямбда-выражениях и работе с коллекциями.
Давайте решим ту же задачу — управление заказом — но функционально.
// Неизменяемая запись (record)
public record Order(int Id, List<string> Items, decimal TotalPrice);
public static class OrderFunctions
{
public static Order AddItem(Order order, string item, decimal price)
{
var newItems = order.Items.Append(item).ToList();
return order with { Items = newItems, TotalPrice = order.TotalPrice + price };
}
public static Order ApplyDiscount(Order order, decimal percent)
{
return order with { TotalPrice = order.TotalPrice * (1 - percent / 100) };
}
}
// Использование
var order = new Order(1, new List<string>(), 0);
order = OrderFunctions.AddItem(order, "Ноутбук", 50000);
order = OrderFunctions.ApplyDiscount(order, 10);
Заметили? Мы не меняем исходный объект. Каждый раз создаётся новый экземпляр с помощью with. Это исключает случайные изменения.
А теперь — магия LINQ. Представьте, что у вас есть список заказов, и нужно найти все, где сумма больше 1000:
var expensiveOrders = orders
.Where(o => o.TotalPrice > 1000)
.OrderByDescending(o => o.TotalPrice)
.Select(o => new { o.Id, o.TotalPrice })
.ToList();
Это чистая функция: orders не изменился, мы просто получили новую коллекцию. Функциональный стиль C# делает код декларативным — вы говорите "что" сделать, а не "как".
Плюсы функционального подхода
- Предсказуемость: функция всегда возвращает одно и то же для одних и тех же входных данных.
- Лёгкое тестирование: не нужно мокать объекты, просто передал данные — проверил результат.
- Параллелизм: с неизменяемыми данными нет блокировок и гонок.
Минусы функционального подхода
- Производительность: создание новых объектов — это затраты памяти. Для больших данных может быть проблемой.
- Крутая кривая обучения: не всем легко привыкнуть к рекурсии и отсутствию циклов.
- Не всегда уместно: для простых CRUD-приложений функциональный стиль избыточен.
3. Гибридный подход: лучшее из двух миров
Современный C# — это мультипарадигменный язык. Microsoft явно поощряет гибридный стиль, когда вы используете ООП для архитектуры, а функциональные приёмы — внутри методов.
Вот типичный пример из реального проекта:
public class UserService
{
private readonly IUserRepository _repository;
public UserService(IUserRepository repository) // DI — это ООП
{
_repository = repository;
}
public async Task<List<UserDto>> GetActiveUsersAsync()
{
var users = await _repository.GetAllAsync();
// Чисто функциональная обработка
return users
.Where(u => u.IsActive)
.Select(u => new UserDto
{
Id = u.Id,
FullName = $"{u.FirstName} {u.LastName}",
Age = CalculateAge(u.BirthDate)
})
.OrderBy(u => u.FullName)
.ToList();
}
// Чистая функция
private int CalculateAge(DateTime birthDate)
=> DateTime.Now.Year - birthDate.Year -
(DateTime.Now.DayOfYear < birthDate.DayOfYear ? 1 : 0);
}
Что здесь происходит?
- ООП: класс
UserService, внедрение зависимости, работа с репозиторием. - Функциональный стиль: LINQ-цепочки, иммутабельные коллекции, чистая функция
CalculateAge.
Такой подход — золотая середина. Вы не пи