Это первая статья из небольшой серии, посвященной программированию на C# в функциональном стиле. Серия не про LINQ, как можно было бы подумать, а про более фундаметальные вещи. Навеяно F#-ом.



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

Комментарии (17)


  1. astudent
    15.09.2015 10:37

    Я бы посоветовал убрать из кода private set;, это позволит избежать случайной ошибки сделать класс изменяемым.


    1. A1ien
      15.09.2015 11:04

      Тогда не получиться инициализировать эти свойства из конструктора.


      1. AWE64
        15.09.2015 11:23
        +3

        Есть ещё readonly поля.



    1. vkhorikov
      15.09.2015 14:51

      Вы правы. Пост писался еще до 6го шарпа


      1. impwx
        15.09.2015 15:22
        +1

        Свойства readonly были еще до C# 6.

        P.S. Почему статья не оформлена как перевод?


        1. vkhorikov
          15.09.2015 15:34

          С ридонли полями примерно так же как и с private set пропертями в плане читаемости. Т.е. для того чтобы понять является ли класс изменяемым нам все равно нужно смотреть внутрь этого класса, по сигнатуре это непонятно. Тут бы помогло ключевое слово immutable.

          >P.S. Почему статья не оформлена как перевод?
          Я думаю свои статьи не очень правильно оформлять переводом, хотя тут не уверен насчет правил хабра.


          1. impwx
            15.09.2015 15:36

            Прошу прощения, не обратил внимания, что изначальная статья тоже ваша. Тогда помечать как перевод не нужно.


          1. sentyaev
            16.09.2015 11:58

            Что вам мешает сделать так:

            private readonly string name;
            public string Name { get; }
            public class SomeClass(string name) {
                this.name = name;
            }
            


          1. vba
            18.09.2015 10:30

            Тут я с вами (увж автор) не соглашусь, readonly пресекают изменяемость на корню а вот private set просто не позволяют изменить содержимое снаружи класса. В плане читаемости, когда видишь такое дело тут же ждешь подвоха, а нет ли тут moving parts которые провоцируются внутренним поведением класса.


  1. roboter
    15.09.2015 14:37
    +1

    В первом варианте выполнение Search(queryObject); не зависит от if, во втором зависит :)


  1. stepik777
    15.09.2015 19:54
    +1

    C#, к сожалению, плохо подходит под такой стиль — переменные изменяемы по умолчанию, управляющие структуры (if, switch) нельзя использовать как выражения.


    1. Bronx
      16.09.2015 07:26

      Вторая проблема решается обильным использование тернарного условного оператора, хотя многие его не любят (незаслуженно, имхо).


  1. Bronx
    16.09.2015 07:15

    Ещё ограничений:

    * сериализация (например, стандартный XmlSerializer требует публичных read-write свойств и конструкторы без параметров)
    * ORM (например, EntityFramework плохо дружит с read-only коллекциями)


    1. mayorovp
      16.09.2015 09:35

      ORM в принципе плохо дружит с неизменяемостью. Просто потому что там все построено на отслеживании изменений в сущностях.


    1. vkhorikov
      16.09.2015 14:42

      По поводу ORM — обычная практика в таких случаях создавать интернал коллекцию для ORM и внешнюю read-only для клиентов класса


      1. Bronx
        17.09.2015 03:05
        +1

        Tогда перестаёт работать конструкция типа 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'.»