Разработчики хорошо знают опасности изменяемого состояния в программе. Значение изменяется из-под вас, нарушая выполнение вычислений. Два разных места на экране, якобы основанные на одной и той же информации, не соответствуют друг другу. Приложение аварийно завершает работу, потому что значение не было изменено в нужный момент.

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

Ценности и сущности

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

Этот взгляд объединяет равенство с изменчивостью, но, на мой взгляд, другие возможности не нужны. Неизменяемый объект с отдельным идентификатором может быть просто значением, включающим уникальный идентификатор. А изменяемый объект с равенством на основе состояний, по моему опыту, просто вызывает проблемы.

Такие языки, как C # и Swift, имеют некоторое разделение на уровне языка с разделением типов на структуры (значения) и классы (сущности). Но они не идут до конца, поскольку оба также включают переопределяемое равенство для своих типов сущностей, что позволяет определять равенство, не основанное на идентичности. Мне кажется, что это пережиток языков, в которых все является сущностью, и ему нет места в надлежащем языке, разделяющем ценности-сущности. Подход Clojure идентичность имеет состояние намного лучше, но мы не всегда можем программировать на Clojure.

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

Состояние

Теперь, когда огромное чудовище изменчивого состояния было внедрено в то, что я называю сущностями, давайте посмотрим, как им управлять. Я разделил состояние объекта на две формы: внутреннюю и внешнюю. Не имеет большого значения, как осуществляется управление внутренним состоянием, поскольку оно является локальным для объекта и может содержаться в достаточно небольшой области, чтобы его обычно можно было понять.

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

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

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

Зависимости

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

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

Чтобы сделать это правильно, язык программирования или среда должны поддерживать некоторую форму привязки данных. Производное состояние определяется привязкой ко всем его зависимостям с чистой функцией для его создания. Таким образом, система гарантирует, что все состояния во всем приложении всегда согласованы. Теоретически это также можно сделать с помощью шаблона Observer, но поскольку шаблон Observer отделяет определение состояния от обновления, на практике может быть слишком легко забыть какое-то необходимое обновление производного состояния, когда отношения становятся сложными.

Архитектура Redux демонстрирует подобное мышление. Изменения состояния выполняются с помощью чистых функций, которые принимают существующее состояние и выполненное действие по изменению состояния. Redux, по сути, имеет только одну сущность, хранилище, что упрощает управление производным состоянием. В сочетании с чем-то вроде React, где пользовательский интерфейс - это чистая функция состояния приложения, сложность управления состоянием значительно снижается.

На платформах, которые поощряют стиль с несколькими сущностями, управление производным состоянием на самом деле не очень хорошо поддерживается в самих языках или их библиотеках. Связывание данных обычно обрабатывается посредством событий модификации, которые не работают, скажем, в сценарии ромбовидной зависимости: состояние объекта Top зависит от состояний объектов Middle1 и Middle2, оба из которых зависят от состояния объекта Bottom. Правильный порядок изменения состояний: Bottom - ›(Middle1, Middle2) -› Top, за которым следует любая электронная таблица, но система, основанная на событиях модификации, сделает Bottom - ›Middle1 -› Top, где новое состояние Top вычисляется на основе нового состояния Middle1, но старого состояния Middle2, что приводит к несогласованности.

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