Функциональный C#. Неизменяемые объекты
Самая большая проблема корпоративного ПО — сложность кода. Именно поэтому его читабельность считается одним из наиболее важных аспектов программирования. Все говорят, что код должен быть лаконичен, иначе его будет сложно и анализировать, и проверять на корректность.
Приведём пример:
// Создаём критерии поиска var queryObject = new QueryObject<Customer>(name, page: 0, pageSize: 10); // Ищем клиентов IReadOnlyCollection<Customer> customers = Search(queryObject); // Регулируем критерии, если никто не будет найден if (customers.Count == 0) { AdjustSearchCriteria(queryObject, name); // Поменяется ли здесь queryObject? Search(queryObject); }
Итак, изменится ли объект запроса к моменту выполнения второго поиска? Возможно, да, но, возможно, и нет. И зависеть это будет от того, удастся ли нам что-либо найти после первого поиска. Кроме того, это будет зависеть и от того, поменяются ли критерии поиска после выполнения метода AdjustSearchCriteria. Говоря простым языком, мы не сможем заранее знать, поменяется ли объект запроса во 2-м поиске.
А теперь рассмотрим следующий код:
// Создаём критерии поиска var queryObject = new QueryObject<Customer>(name, page: 0, pageSize: 10); // Ищем клиентов IReadOnlyCollection<Customer> customers = Search(queryObject); if (customers.Count == 0) { // Регулируем критерии, если никто не будет найден QueryObject<Customer> newQueryObject = AdjustSearchCriteria(queryObject, name); Search(newQueryObject); }
А вот тут сразу становится всё ясно: после того, как мы ничего не найдём при первом поиске, метод AdjustSearchCriteria создаст новые критерии, которые уже станут применяться во 2-м поиске.
Подытожим: какие есть проблемы при работе со структурами данных, подвергающимися изменениям: 1. Сложно оценивать написанный код, когда мы не можем быть уверены, изменятся ли конкретные данные либо нет. 2. Довольно трудно следовать за многочисленными отсылками, когда вам потребовалось посмотреть не только на сам метод, но и на функции, вызываемые в нём. 3. Если вы пишете многопоточное приложение, отслеживание и отладка кода станут еще труднее.
Как описывать неизменяемые объекты?
Когда у вас есть относительно простой класс, вам всегда следует рассматривать вопрос о том, как сделать его неизменяемым. Данное правило коррелирует с термином Value Objects — они просты, их просто сделать неизменяемыми.
Как же тогда описать неизменяемые объекты? Представьте, что у нас есть класс ProductPile, который представляет некоторые продукты, выставленные нами на продажу:
public class ProductPile { public string ProductName { get; set; } public int Amount { get; set; } public decimal Price { get; set; } }
Дабы сделать поля класса ProductPile неизменяемыми, отметим их доступными лишь для чтения и создадим конструктор:
public class ProductPile { public string ProductName { get; private set; } public int Amount { get; private set; } public decimal Price { get; private set; } public ProductPile(string productName, int amount, decimal price) { Contracts.Require(!string.IsNullOrWhiteSpace(productName)); Contracts.Require(amount >= 0); Contracts.Require(price > 0); ProductName = productName; Amount = amount; Price = price; } public ProductPile SubtractOne() { return new ProductPile(ProductName, Amount – 1, Price); } }
Чего нам удалось добиться такой организацией класса: 1. Мы можем не волноваться о корректности данных, т. к. проверять значение будем лишь один раз в конструкторе. 2. Теперь мы полностью уверены, что значения объектов всегда корректны. 3. Объекты автоматически становятся потокобезопасными. 4. Повысилась читаемость кода, ведь не нужно проверять, не поменялись ли значения объектов.
Ограничения в использовании
Разумеется, у всего есть цена. Высказанная идея применима в простых и маленьких классах, но неприменима в больших. Во-первых, речь идёт о производительности: если объект получается слишком большой, создание его копий каждый раз при изменениях какого-либо параметра не самым лучшим образом отразится на быстродействии приложения.
Здесь неплохим примером являются неизменяемые коллекции (immutable collections). Авторы этих коллекций учли проблемы с производительностью и добавили класс Builder, позволяющий «мутировать» и изменять коллекцию:
var builder = ImmutableList.CreateBuilder<string>(); builder.Add("1"); // Добавляем строки в существующий объект ImmutableList<string> list = builder.ToImmutable(); ImmutableList<string> list2 = list.Add("2"); // Создаём объект с 2 строками
Во-вторых, когда вы попытаетесь сделать изменчивый по природе класс неизменяемым, вы столкнётесь со множеством проблем. Однако, пусть вас это не останавливает )).
Итог
Рассматривайте все плюсы и минусы перевода объектов класса к неизменяемым, оценивайте затраты и не забывайте рассуждать здраво. В конечном итоге, вы убедитесь в том, что в большинстве ситуаций вы получите явную выгоду. Естественно, лишь в том случае, если ваш класс достаточно простой и маленький.
По материалам: «Functional C#: Immutability»