- Functional C#: Immutability
- Functional C#: Primitive obsession
- Functional C#: Non-nullable reference types
- Functional C#: работа с ошибками
Immutability (неизменяемость)
Наибольшая проблема в мире enterprise разработки — это борьба со сложнотью. Читаемость кода — это пожалуй первое чего мы должны стараться достичь при написании любого более-менее сложного проекта. Без этого наша способность понимать код и принимать на основе этого разумные решения значительно ухудшается.
Помогают ли нам изменяемые объекты при чтении кода? Давайте рассмотрим пример:
// Create search criteria
var queryObject = new QueryObject<Customer>(name, page: 0, pageSize: 10);
// Search customers
IReadOnlyCollection<Customer> customers = Search(queryObject);
// Adjust criteria if nothing found
if (customers.Count == 0)
AdjustSearchCriteria(queryObject, name);
// Is queryObject changed here?
Search(queryObject);
Изменился ли queryObject к моменту поиска кастомеров во второй раз? Может быть, да. А может, и нет. Это зависит от того, был ли этот объект изменен методом AdjustSearchCriteria. Чтобы выяснить это, нам необходимо заглянуть внутрь этого метода, его сигнатура не дает нам достаточной информации.
Сравните это со следующим кодом:
// Create search criteria
var queryObject = new QueryObject<Customer>(name, page: 0, pageSize: 10);
// Search customers
IReadOnlyCollection<Customer> customers = Search(queryObject);
if (customers.Count == 0)
{
// Adjust criteria if nothing found
QueryObject<Customer> newQueryObject = AdjustSearchCriteria(queryObject, name);
Search(newQueryObject);
}
В этом примере ясно, что AdjustSearchCriteria создает новый объект критерия и использует его в дальнейшем для нового поиска.
Так в чем проблема с изменяемыми структурами данных?
- Сложно обдумывать код, если нет уверенности меняются ли данные, передающиеся от одного метода к другому.
- Сложно сделить за ходом выполнения программы если вам приходится углубляться на несколько уровней вниз по стеку.
- В случае с многопоточным приложением, понимание и отладка кода усложняется многократно.
Как создавать неизменямые типы
В будущих версиях C# возможно появится ключевое слово immutable. С его помощью можно будет понимать является ли тип неизменямым просто глядя на его сигнатуру. Пока же нам приходится пользоваться тем, что есть.
Если вы имеете сравнительно простой класс, рассмотрите возможность сделать его неизменямым. Этот гайд-лайн коррелирует с понятием Value Objects.
Возьмем для примера класс ProductPile, описывающий какое-то количество продуктов на продажу:
public class ProductPile
{
public string ProductName { get; set; }
public int Amount { get; set; }
public decimal Price { get; set; }
}
Чтобы сделать его неизменяемым, мы можем пометить его свойства как read-only и добавить конструктор:
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;
}
}
Теперь предположим, что вам необходимо уменьшать свойство Amount на единицу каждый раз когда вы продаете один из продуктов. Вместо того, чтобы изменять имеющийся объект, мы может создавать новый на основе имеющегося:
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);
}
}
Что нам это дает?
- Имея неизменяемый тип, нам необходимо валидировать его контракты только единожды, в конструкторе. После этого мы можем быть абсолютно уверены, что объет находится в корректном состоянии.
- Объекты неизменяемых типов потокобезопасны.
- Улучшается читаемость кода, т.к. больше нет необходимости углубляться в стек для того, чтобы убедиться, что переменные, с которыми работает метод, не были изменены.
Ограничения
Конечно, каждая полезная практика имеет свою цену. В то время как небольшие классы пользуются преимуществами неизменяемости в полной мере, такой подход не всегда применим в случае с большими типами.
В первою очередь, неизменяемость несет в себе потенциальные проблемы с производительностью. Если объект довольно большой, необходимость создавать его копию при каждом изменении может стать проблемой.
Хорошим примером тут будут неизменяемые коллекции. Авторы учли потенциальные проблемы с производительностью и добавили специальный класс Builder, который позволяет изменять состояние коллекций. После того, как коллекция приведена к необходимому состоянию, ее можно финализировать, конвертировав в неизменяемую:
var builder = ImmutableList.CreateBuilder<string>();
builder.Add(“1”); // Adds item to the existing object
ImmutableList<string> list = builder.ToImmutable();
ImmutableList<string> list2 = list.Add(“2”); // Creates a new object with 2 items
Заключение
В большинстве случаев, неизменяемые типы (особенно если они довольно просты) делают код лучше.
Остальные статьи в серии:
- Functional C#: Immutability
- Functional C#: Primitive obsession
- Functional C#: Non-nullable reference types
- Functional C#: работа с ошибками
Английская версия статьи: Functional C#: Immutability
Комментарии (17)
roboter
15.09.2015 14:37+1В первом варианте выполнение Search(queryObject); не зависит от if, во втором зависит :)
stepik777
15.09.2015 19:54+1C#, к сожалению, плохо подходит под такой стиль — переменные изменяемы по умолчанию, управляющие структуры (if, switch) нельзя использовать как выражения.
Bronx
16.09.2015 07:26Вторая проблема решается обильным использование тернарного условного оператора, хотя многие его не любят (незаслуженно, имхо).
Bronx
16.09.2015 07:15Ещё ограничений:
* сериализация (например, стандартный XmlSerializer требует публичных read-write свойств и конструкторы без параметров)
* ORM (например, EntityFramework плохо дружит с read-only коллекциями)mayorovp
16.09.2015 09:35ORM в принципе плохо дружит с неизменяемостью. Просто потому что там все построено на отслеживании изменений в сущностях.
vkhorikov
16.09.2015 14:42По поводу ORM — обычная практика в таких случаях создавать интернал коллекцию для ORM и внешнюю read-only для клиентов класса
Bronx
17.09.2015 03:05+1Tогда перестаёт работать конструкция типа db.Orders.Include(o => o.InternalOrderLines), вылетает с «A specified Include path is not valid. The EntityType 'Order' does not declare a navigation property with the name 'InternalOrderLines'.»
astudent
Я бы посоветовал убрать из кода private set;, это позволит избежать случайной ошибки сделать класс изменяемым.
A1ien
Тогда не получиться инициализировать эти свойства из конструктора.
AWE64
Есть ещё readonly поля.
astudent
Вот github.com/dotnet/roslyn/wiki/New-Language-Features-in-C%23-6#getter-only-auto-properties
vkhorikov
Вы правы. Пост писался еще до 6го шарпа
impwx
Свойства
readonly
были еще до C# 6.P.S. Почему статья не оформлена как перевод?
vkhorikov
С ридонли полями примерно так же как и с private set пропертями в плане читаемости. Т.е. для того чтобы понять является ли класс изменяемым нам все равно нужно смотреть внутрь этого класса, по сигнатуре это непонятно. Тут бы помогло ключевое слово immutable.
>P.S. Почему статья не оформлена как перевод?
Я думаю свои статьи не очень правильно оформлять переводом, хотя тут не уверен насчет правил хабра.
impwx
Прошу прощения, не обратил внимания, что изначальная статья тоже ваша. Тогда помечать как перевод не нужно.
sentyaev
Что вам мешает сделать так:
vba
Тут я с вами (увж автор) не соглашусь, readonly пресекают изменяемость на корню а вот private set просто не позволяют изменить содержимое снаружи класса. В плане читаемости, когда видишь такое дело тут же ждешь подвоха, а нет ли тут moving parts которые провоцируются внутренним поведением класса.