[Высокая производительность, Программирование, Проектирование и рефакторинг] Почему так важна иммутабельность (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Привет, Хабр!
Сегодня мы хотим затронуть тему иммутабельности и примериться, заслуживает ли эта проблема более серьезного рассмотрения.
Иммутабельные объекты – неизмеримо мощный феномен в программировании. Иммутабельность помогает избежать всевозможных проблем с конкурентностью и не допустить кучу разнообразных багов, но понять иммутабельные конструкции бывает непросто. Давайте рассмотрим, что они из себя представляют, и как ими пользоваться.
Во-первых, взгляните на простой объект:
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
===========Похожие новости:
- [Программирование, Управление разработкой, Управление персоналом, Карьера в IT-индустрии] Как начать подгорать, но не выгореть в проекте, где «было срочно нужно»
- [Разработка веб-сайтов, API] Как мы использовали GraphQL в разработке на примере интернет-каталога
- [Информационная безопасность, Программирование] Безопасность через неясность недооценивается (перевод)
- [IT-эмиграция, Изучение языков, Программирование] Как российский разработчик уехал в Австралию. История Юрия Галустова
- [Конференции, Проектирование и рефакторинг, Сотовая связь] Open DDD Meetup 22/09
- [Ненормальное программирование, Киберпанк, Биотехнологии, Биографии гиков] Эта музыка будет вечной или генетическое программирование
- [Java, Scala, Анализ и проектирование систем, Высокая производительность] Как построить надежное приложение на базе Event sourcing?
- [Профессиональная литература] Издательство Питер. Осенняя распродажа
- [JavaScript, Программирование, Разработка веб-сайтов] JavaScript: парочка задач на знание рекурсии
- [Системы обмена сообщениями, Python, Программирование, Анализ и проектирование систем] Система отложенного исполнения на RabbitMQ
Теги для поиска: #_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 (
Проектирование и рефакторинг
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 11:55
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Привет, Хабр! Сегодня мы хотим затронуть тему иммутабельности и примериться, заслуживает ли эта проблема более серьезного рассмотрения. Иммутабельные объекты – неизмеримо мощный феномен в программировании. Иммутабельность помогает избежать всевозможных проблем с конкурентностью и не допустить кучу разнообразных багов, но понять иммутабельные конструкции бывает непросто. Давайте рассмотрим, что они из себя представляют, и как ими пользоваться. Во-первых, взгляните на простой объект: 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 ===========Похожие новости:
Блог компании Издательский дом «Питер» )[/url], #_vysokaja_proizvoditelnost ( Высокая производительность ), #_programmirovanie ( Программирование ), #_proektirovanie_i_refaktoring ( Проектирование и рефакторинг ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 11:55
Часовой пояс: UTC + 5