Борьба с побочными эффектами

Простыми словами, побочный эффект или side effect это когда изменение некоторого свойства в одном месте программы непредсказуемо влияет на поведение другой ее части (или многих частей). Побочные эффекты являются ничем иным как багами, исследование которых может потребовать глубокого дебага, продолжительностью от нескольких минут до нескольких часов.

Одним из известных способов борьбы с побочными эффектами является использование неизменяемых типов данных. Однако частое «изменение» таких типов может привести к проседанию производительности по причине регулярных аллокаций и работы сборщика мусора. В таком случае вместо создания неизменяемых типов можно выбрать несложные техники, которые также устранят побочные эффекты.

Классы Employee и Compensation

Разберем проблему и способы ее решения на двух классах Employee и Compensation.

public class Compensation
{
    public decimal Gross { getset; }
    public string Currency { getset; }
}

public class Employee
{
    public int Id { getset; }
    public Compensation Compensation { getset; }
}

Описание проблемы

Представим, что в компанию устраиваются два новичка на одинаковую зарплату. В коде подобную ситуацию можно выразить следующим образом:

var compensation = new Compensation() { Gross = 100, Currency = "USD" };
var employee1 = new Employee() { Id = 1, Compensation = compensation };
var employee2 = new Employee() { Id = 2, Compensation = compensation };

Потом появилось требование изменить зарплату только первому сотруднику:

employee1.Compensation.Gross = 200;

Строка выше приведет к побочному эффекту. Два объекта Employee ссылаются на один и тот же объект Compensation. Повышение зарплаты первому сотруднику автоматически повышает зарплату второму, что явно не является поведением, которого ожидают клиенты класса Employee. Одним из выходов из данной ситуации является создание отдельного объекта Compensation для каждого сотрудника:

var employee1 = new Employee() { Id = 1, Compensation = new Compensation() { Gross = 100, Currency = "USD" } };
var employee2 = new Employee() { Id = 2, Compensation = new Compensation() { Gross = 100, Currency = "USD" } };

Теперь изменение зарплаты первого не повлияет на второго и наоборот, но ничего не мешает кому-то в следующий раз наделить нескольких Employee единственным объектом Compensation. И дело не в чьем-то злом умысле, просто клиенту класса Employee неоткуда знать, что объекты Compensation должны обязательно быть разными, даже если содержат в себе идентичные значения. Наши классы спроектированы таким образом, что их очень легко использовать неправильно. Начать использовать класс Employee и Compensation правильным образом без изучения деталей их реализации невозможно.

Решение №1: Каждому Employee отдельный Compensation

Первым решением будет гарантировать наличие отдельного объекта Compensation для каждого Employee. Для реализации такого решения, необходимо создавать объект Compensation каждый раз при создании объекта Employee.

public class Employee
{
    public int Id { getset; }
    public Compensation Compensation { get; } = new Compensation(100, "USD");
}

Отсутствие setter’а в свойстве Compensation гарантирует, что никто не сможет обнулить это свойство, а инициализация Compensation в момент создания Employee в свою очередь гарантирует, что сотрудник точно не останется без зарплаты.

В таком случае клиентам класса Employee больше нет необходимости самостоятельно создавать объект Compensation. Теперь они легко могут создавать сотрудников и при необходимости устанавливать зарплату следующим образом:

var employee1 = new Employee();
var employee2 = new Employee();
employee1.Compensation.Gross = 200;

Проблема побочного эффекта решена, так как каждый Employee имеет свой собственный объект Compensation, изменение которого не повлияет на остальных.

Но рассмотренный подход в некоторых случаях может обладать одним недостатком. Представим, что вознаграждение 100 USD получают 90% из 10000 сотрудников компании, для которой разрабатывается наша программа. Следовательно, мы имеем 9000 идентичных объектов Compensation, хотя можем обойтись и одним единственным, что хорошо сэкономит используемую оперативную память. Вернуться к первому примеру нельзя, так как опять получим побочные эффекты. Решением будет создание неизменяемого объекта.

Решение №2: Превратить Compensation в неизменяемый объект

Надежным способов избавиться от побочных эффектов является использование неизменяемых объектов, для создания которых необходимо выполнить два шага:

  • Удалить setter’ы всех свойств класса
  • Инициализировать все свойства класса в конструкторе

После выполнения вышеперечисленных шагов, класс Compensation будет выглядеть так:

public class Compensation
{
    public Compensation(decimal gross, string currency)
    {
        Gross = gross;
        Currency = currency;
    }

    public decimal Gross { get; }
    public string Currency { get; }
}

Отсутствие setter’ов, начиная с С# 6.0 превращает свойство в read-only. Теперь подобный код

employee1.Compensation.Gross = 200;

приведет к ошибке на этапе компиляции. Единственным способом поменять зарплату первого сотрудника будет создать новый объект Compensation c новыми данными, что никак не повлияет на второго.

Теперь мы можем спокойно проинициализировать тысячи объектов Employee единственным объектом Compensation со значением 100 USD, что сэкономит используемую приложением память и не приведет к побочным эффектам при попытке изменить вознаграждение некоторого сотрудника.

var compensation = new Compensation() { Gross = 100, Currency = "USD" };
var employee1 = new Employee() { Id = 1, Compensation = compensation };
var employee2 = new Employee() { Id = 2, Compensation = compensation };
//...

empl
Изменяем зарплату первому сотруднику:

employee1.Compensation = new Compensation(200, "USD");

empl2

Заключение

Если некоторый объект редко подвергается изменениям и используется в сотнях или тысячах других классов, то решение #2 выглядит более приемлемым, чем #1. Если объект будет меняться часто, то во избежание возможных проблем с производительностью использование #1 более оптимально, чем #2. Конкретно для нашего примера именно #2 подходит больше, так как большинство сотрудников получают одинаковую зарплату, которая не будет меняться тысячи раз в день.

То или иное решение необходимо взвешенно выбирать руководствуясь требованиями предметной области, спецификой разрабатываемого проекта и просто здравым смыслом.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google photo

Для комментария используется ваша учётная запись Google. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s