How an Immutable Object Can Have State and Behavior?

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

Я часто слышу это возражение против неизменяемых объектов: “Да, они полезны, когда состояние не меняется. Однако в нашем случае мы имеем дело с часто изменяющимися объектами. Мы просто не можем позволить себе создавать новый документ каждый раз, когда нам нужно только изменить его заголовок”. Вот где я не согласен: заголовок объекта - это не состояние документа, если вам нужно часто его менять. Вместо этого, это поведение документа. Документ может и должен быть неизменяемым, если это хороший объект, даже когда его заголовок часто меняется. Позвольте мне объяснить, как это работает.

В основе каждого объекта лежат три элемента: идентичность, состояние и поведение. Идентичность отличает наш документ от других объектов, состояние - это то, что документ знает о себе (также известное как “инкапсулированное знание”), а поведение - это то, что документ может сделать для нас по запросу. Например, этот документ является изменяемым.

Давайте попробуем использовать этот изменяемый объект:

Здесь мы создаем два объекта и затем изменяем их инкапсулированные состояния. Очевидно, что first.equals(second) вернет false, потому что у этих двух объектов разные идентичности, даже если они инкапсулируют одно и то же состояние.

Метод toString() предоставляет поведение документа - документ может преобразовать себя в строку.

Для изменения заголовка документа мы просто вызываем его метод setTitle() еще раз:

Простыми словами, мы можем многократно использовать объект, изменяя его внутреннее состояние. Это быстро и удобно, не так ли? Быстро, да. Удобно, не совсем. Продолжайте чтение.

Как я упоминал ранее, неизменяемость является одним из достоинств хорошего объекта и очень важным. Хороший объект является неизменяемым, и хорошее программное обеспечение содержит только неизменяемые объекты. Основное отличие между неизменяемыми и изменяемыми объектами заключается в том, что неизменяемый объект не имеет идентичности, и его состояние никогда не изменяется. Вот неизменяемый вариант того же документа:

Этот документ является неизменяемым, и его состояние (id и title) является его идентификатором. Давайте посмотрим, как мы можем использовать этот неизменяемый класс (кстати, я использую аннотацию @Immutable из jcabi-aspects).

Мы уже не можем изменять документ. Когда нам необходимо изменить заголовок, мы должны создать новый документ.

Каждый раз, когда мы хотим изменить его инкапсулированное состояние, мы также должны изменить его идентичность, потому что идентичности нет. Состояние - это идентичность. Посмотрите на код метода equals(), который сравнивает документы по их идентификаторам и заголовкам. Теперь идентичность документа - это его идентификатор и заголовок!

Теперь я перехожу к вопросу, с которого мы начали: как насчет производительности и удобства? Мы не хотим изменять весь документ каждый раз, когда нам нужно изменить его заголовок. Если документ достаточно большой, это будет огромная обязанность. Более того, если неизменяемый объект инкапсулирует другие неизменяемые объекты, мы должны изменить всю иерархию даже при изменении только одной строки в одном из них.

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

Концептуально говоря, данный документ действует как прокси реального документа, у которого заголовок хранится где-то - например, в файле. Это то, что хороший объект должен делать - быть прокси реальной сущности. Документ предоставляет две функции: чтение заголовка и сохранение заголовка. Вот как будет выглядеть его интерфейс:

title() reads the title of the document and returns it as a String, and title(String) saves it back into the document. Imagine a real paper document with a title. You ask an object to read that title from the paper or to erase an existing one and write new text over it. This paper is a “copy” utilized in these methods.

Теперь мы можем часто вносить изменения в неизменяемый документ, и документ остается тем же самым. Он не перестает быть неизменяемым, поскольку его состояние (id) не изменяется. Это тот же самый документ, даже если мы меняем его заголовок, потому что заголовок не является состоянием документа. Это что-то в реальном мире, за пределами документа. Документ - это всего лишь прокси между нами и этим “чем-то”. Чтение и запись заголовка - это поведение документа, а не его состояние.

Единственный вопрос, на который у нас все еще нет ответа, - что такое “копия” и что произойдет, если нам нужно сохранить название документа в памяти?

Давайте рассмотрим это с точки зрения “объектного мышления”. У нас есть объект document, который должен представлять сущность из реального мира в объектно-ориентированном мире. Если такая сущность - это файл, мы легко можем реализовать методы title(). Если такая сущность - это объект Amazon S3, мы также легко реализуем методы чтения и записи названия, сохраняя объект неизменным. Если такая сущность - это веб-страница, у нас нет проблем с реализацией методов чтения или записи названия, сохраняя объект неизменным. У нас нет проблем, пока существует реальный документ в мире и у него есть своя собственная идентичность. Наши методы чтения и записи названия будут взаимодействовать с этим реальным документом и извлекать или обновлять его название.

Проблемы возникают, когда такая сущность не существует в реальном мире. В этом случае нам нужно создать изменяемое свойство объекта под названием title, читать его с помощью title(), и изменять с помощью title(String). Но объект неизменяем, поэтому мы не можем иметь изменяемое свойство в нем - по определению! Что мы делаем?

Как это может быть, что наш объект не представляет сущность реального мира? Помните, что реальный мир - это все, что находится вокруг живой среды объекта. Возможно ли, что объект не представляет никого и действует сам по себе? Нет, это невозможно. Каждый объект является представителем сущности реального мира. Так кого он представляет, если мы хотим сохранить title внутри него и у нас нет никакого файла или HTTP-страницы за объектом?

Он представляет компьютерную память.

Заголовок неизменного документа №50 “Как приготовить гриль-сэндвич” хранится в памяти, занимая 23 байта пространства. Документ должен знать, где хранятся эти байты, и он должен иметь возможность прочитать их и заменить на что-то другое. Эти 23 байта - это сущность реального мира, которую представляет объект. Байты не имеют никакого отношения к состоянию объекта. Они являются изменяемой сущностью реального мира, подобной файлу, HTTP-странице или объекту Amazon S3.

К сожалению, Java (и многие другие современные языки) не позволяют прямой доступ к компьютерной памяти. Вот как мы бы разработали наш класс, если бы такой прямой доступ был возможен:

Класс Memory будет реализован нативно в JDK, и все остальные классы будут неизменяемыми. Класс Memory будет иметь прямой доступ к куче памяти и будет отвечать за операции malloc и free на уровне операционной системы. Наличие такого класса позволит сделать все классы Java неизменяемыми, включая StringBuffer, ByteArrayOutputStream и т. д.

Класс Memory явно подчеркнет миссию объекта в программном обеспечении, которой является анимация данных. Объект не хранит данные; он анимирует их. Данные существуют где-то и являются анемичными, статическими, неподвижными и т. д. Данные являются мертвыми, в то время как объект жив. Роль объекта состоит в том, чтобы оживить кусок данных, анимировать его, но не стать самим куском данных. Объекту нужно некоторое знание, чтобы получить доступ к этому мертвому куску данных. Объекту может понадобиться уникальный ключ базы данных, HTTP-адрес, имя файла или адрес памяти, чтобы найти данные и оживить их. Но объект никогда не должен думать о себе как о данных.

К сожалению, у нас нет такого класса, представляющего память, в Java, Ruby, JavaScript, Python, PHP и многих других языках высокого уровня. Кажется, разработчики языков не поняли идею живых объектов против мертвых данных, что печально. Нам приходится смешивать данные с состоянием объектов, используя те же языковые конструкции: переменные объектов и свойства. Может быть, когда-нибудь у нас будет такой класс Memory в Java и других языках, но пока у нас есть несколько вариантов.

Использовать C++. В C++ и подобных низкоуровневых языках можно иметь прямой доступ к памяти и работать с данными в памяти так же, как мы работаем с данными в файле или через HTTP. В C++ мы можем создать этот класс Memory и использовать его точно так же, как мы объяснили выше.

Использовать массивы. В Java массив является структурой данных с уникальным свойством — его можно изменять при объявлении как final. Вы можете использовать массив байтов как изменяемую структуру данных внутри неизменяемого объекта. Это заменительное решение, которое концептуально напоминает класс Memory, но является более примитивным.

Избегайте данных в памяти. Старайтесь избегать данных в памяти насколько возможно. В некоторых областях это легко сделать, например, в веб-приложениях, обработке файлов, адаптерах ввода-вывода и т. д. Однако в других областях это намного сложнее. Например, в играх, алгоритмах обработки данных и графическом интерфейсе большая часть объектов анимируется данными в памяти, потому что память - это единственный ресурс, который у них есть. В этом случае без класса Memory вы оказываетесь с изменяемыми объектами :( Нет обходного пути.

Подводя итог, не забывайте, что объект является аниматором данных. Он использует свои инкапсулированные знания, чтобы получить доступ к данным. Независимо от того, где хранятся данные - в файле, через HTTP или в памяти - концептуально они очень отличаются от состояния объекта, даже если они могут выглядеть очень похожими.

Хороший объект является неизменяемым аниматором изменяемых данных. Несмотря на то, что он неизменяемый, а данные изменяемые, он живой, а данные мертвы в рамках среды жизни объекта.

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-12-05 at 21:24

sixnines availability badge   GitHub stars