[Высокая производительность, Программирование, Проектирование и рефакторинг] Почему так важна иммутабельность (перевод)

Автор Сообщение
news_bot ®

Стаж: 6 лет 4 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
16-Сен-2020 13:30

Привет, Хабр!
Сегодня мы хотим затронуть тему иммутабельности и примериться, заслуживает ли эта проблема более серьезного рассмотрения.
Иммутабельные объекты – неизмеримо мощный феномен в программировании. Иммутабельность помогает избежать всевозможных проблем с конкурентностью и не допустить кучу разнообразных багов, но понять иммутабельные конструкции бывает непросто. Давайте рассмотрим, что они из себя представляют, и как ими пользоваться.
Во-первых, взгляните на простой объект:
class Person {
    public String name;
    public Person(
        String name
    ) {
        this.name = name;
    }
}

Как видите, объект Person в своем конструкторе принимает один параметр, а затем ставит его в публичную переменную name. Соответственно, мы можем делать такие вещи:
Person p = new Person("John");
p.name = "Jane";

Просто, правда? В любой момент читать или изменять данные как нам угодно. Но с этим способом есть пара проблем. Первая и важнейшая из них – мы используем в нашем классе переменную name, и таким образом, бесповоротно вводим внутреннее хранилище класса в состав публичного API. Иными словами, мы никак не сможем изменить способ хранения имени внутри класса, если только не перепишем значительной части нашего приложения.
В некоторых языках (например, в C#) предоставляется возможность вставлять функцию-геттер, чтобы обходить эту проблему, но в большинстве объектно-ориентированных языков приходится действовать явно:
class Person {
    private String name;
    public Person(
        String name
    ) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

Пока все хорошо. Если бы вы теперь захотели изменить внутреннее хранилище имени, скажем, на имя и фамилию, то могли бы сделать так:
class Person {
    private String firstName;
    private String lastName;
    public Person(
        String firstName,
        String lastName
    ) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    public String getName() {
        return firstName + " " + lastName;
    }
}

Если не углубляться в серьезнейшие проблемы, сопряженные с таким представлением имен, то очевидно, что внешне API getName() не изменился.
Что же насчет установки имен? Что нужно добавить, чтобы не только получать имя, но и устанавливать его вот так?
class Person {
    private String name;
    //...
    public void setName(String name) {
        this.name = name;
    }
    //...
}

На первый взгляд выглядит отлично, ведь теперь мы снова можем менять имя. Но в таком способе изменения данных есть фундаментальный изъян. У него две стороны: философская и практическая.
Для начала рассмотрим философскую проблему. Объект Person предназначен для представления человека. Действительно, фамилия у человека может меняться, но функцию для этой цели лучше было бы назвать changeName, поскольку такое название подразумевает, что мы меняем фамилию все того же человека. Также она должна включать бизнес-логику для изменения фамилии человека, а не просто действовать как сеттер. Название setName подводит к вполне логичному выводу, что мы можем в добровольно-принудительном порядке изменить имя, сохраненное в объекте person, и нам за это ничего не будет.
Вторая причина связана с практикой: изменяемое состояние (сохраненные данные, которые могут меняться) чревато возникновением багов. Возьмем этот объект Person и определим интерфейс PersonStorage:
interface PersonStorage {
    public void store(Person person);
    public Person getByName(String name);
}

Обратите внимание: этот PersonStorage не указывает, где именно хранится объект: в памяти, на диске или в базе данных. Интерфейс также не требует от реализации создавать копию хранимого в ней объекта. Поэтому может возникнуть интересный баг:
Person p = new Person("John");
myPersonStorage.store(p);
p.setName("Jane");
myPersonStorage.store(p);

Сколько персон сейчас содержится в хранилище person? Одна или две? Кроме того, если сейчас применить метод getByName, то какую из персон он вернет?
Как видите, здесь возможны два варианта: либо PersonStorage скопирует объект Person, и в таком случае будут сохранены две записи Person, либо не станет этого делать, и сохранит лишь ссылку на переданный объект; во втором случае будет сохранен всего один объект с именем “Jane”. Реализация второго варианта может выглядеть так:
class InMemoryPersonStorage implements PersonStorage {
    private Set<Person> persons = new HashSet<>();
    public void store(Person person) {
        this.persons.add(person);
    }
}

Хуже того, сохраненные данные можно изменить, даже не вызывая функцию store. Поскольку в хранилище находится только ссылка на оригинал объекта, при изменении имени также изменится и сохраненная версия:
Person p = new Person("John");
myPersonStorage.store(p);
p.setName("Jane");

Итак, в сущности, баги закрадываются в нашу программу именно потому, что мы имеем дело с изменяемым состоянием. Можно не сомневаться, что эту проблему удастся обойти, если явно прописать работу по созданию копии в хранилище, но есть и гораздо более простой способ: работа с неизменяемыми объектами. Рассмотрим пример:
class Person {
    private String name;
    public Person(
        String name
    ) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public Person withName(String name) {
        return new Person(name);
    }
}

Как видите, вместо метода setName теперь используется метод withName, создающий новую копию объекта Person. Если всякий раз создавать новую копию, то мы обходимся без изменяемого состояния и без соответствующих проблем. Конечно, это приводит к некоторым издержкам, но современные компиляторы могут с ними справляться и, если у вас возникнут проблемы с производительностью, то их можно будет исправить позже.
Помните:
Преждевременная оптимизация – корень всех зол (Дональд Кнут)

Можно возразить, что уровень долговременного хранения, где содержится ссылка на действующий объект – это поломанный уровень долговременного хранения, но такой сценарий реалистичен. Неисправный код действительно существует, и иммутабельность – ценный инструмент, помогающий не допускать таких поломок.
В более сложных сценариях, когда объекты передаются сквозь несколько уровней приложения, баги легко наводняют код, и иммутабельность не допускает возникновения багов, связанных с состоянием. К примерам такого рода относится, например, кэширование в оперативной памяти или внеочередные вызовы функций.
Как иммутабельность помогает при параллельной обработке
Еще одна важная сфера, где нам пригодится иммутабельность – это параллельная обработка. Точнее, многопоточность. В многопоточных приложениях параллельно выполняется сразу несколько линий кода, которые, при этом, обращаются к одной и той же области памяти. Рассмотрим очень простой листинг:
if (p.getName().equals("John")) {
    p.setName(p.getName() + "Doe");
}

Сам по себе этот код не содержит багов, но при параллельном запуске он начинает работать с вытеснением, и может возникнуть беспорядок. Посмотрите, как выглядит вышеприведенный фрагмент кода с комментарием:
if (p.getName().equals("John")) {
    // здесь другой поток изменяет имя, которое более не равно John
    p.setName(p.getName() + "Doe");
}

Это состояние гонки. Первый поток проверяет, равно ли имя “John”, но затем второй поток изменяет это имя. Первый поток при этом продолжает работу, по-прежнему полагая, что имя равно John.
Разумеется, можно было бы применить блокировку, чтобы гарантировать, что в любой момент времени в критичную часть кода будет входить только один поток, однако, здесь может возникнуть узкое место. Однако, если объекты иммутабельны, то такой сценарий сложиться не может, так как в p всегда сохранен один и тот же объект. Если другой поток хочет повлиять на изменение, то создает новую копию, которой не будет в первом потоке.
Итоги
В принципе, я бы посоветовал всегда следить за тем, чтобы изменяемое состояние в вашем приложении встречалось в минимальном объеме. Если же вы и будете к нему прибегать, плотно ограничивайте его качественно спроектированными API, не позволяйте ему протечь в другие области приложения. Чем меньше у вас фрагментов кода, в которых содержится состояние, тем маловероятнее, что проклюнутся ошибки, связанные с состоянием.
Разумеется, большинство задач из области программирования не решаемы, если вообще не прибегать к состоянию. Но, если считать все структуры данных по умолчанию иммутабельными, то в коде будет возникать гораздо меньше случайных багов. Если вы действительно будете вынуждены ввести в код изменяемость – то придется делать это осторожно и продумывать последствия, а не начинять ею весь код.
===========
Источник:
habr.com
===========

===========
Автор оригинала: Janos Pasztor
===========
Похожие новости: Теги для поиска: #_vysokaja_proizvoditelnost (Высокая производительность), #_programmirovanie (Программирование), #_proektirovanie_i_refaktoring (Проектирование и рефакторинг), #_immutability, #_oop (ООП), #_programmirovanie (программирование), #_chistyj_kod (чистый код), [url=https://torrents-local.xyz/search.php?nm=%23_blog_kompanii_izdatelskij_dom_«piter»&to=0&allw=0&o=1&s=0&f%5B%5D=820&f%5B%5D=959&f%5B%5D=958&f%5B%5D=872&f%5B%5D=967&f%5B%5D=954&f%5B%5D=885&f%5B%5D=882&f%5B%5D=863&f%5B%5D=881&f%5B%5D=860&f%5B%5D=884&f%5B%5D=865&f%5B%5D=873&f%5B%5D=861&f%5B%5D=864&f%5B%5D=883&f%5B%5D=957&f%5B%5D=859&f%5B%5D=966&f%5B%5D=956&f%5B%5D=955]#_blog_kompanii_izdatelskij_dom_«piter» (
Блог компании Издательский дом «Питер»
)[/url], #_vysokaja_proizvoditelnost (
Высокая производительность
)
, #_programmirovanie (
Программирование
)
, #_proektirovanie_i_refaktoring (
Проектирование и рефакторинг
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 06-Июл 16:30
Часовой пояс: UTC + 5