ДОП против инкапсуляции данных.
Чем более гибкой является система, тем легче ее адаптировать к меняющимся
требованиям.
Разделение между кодом и данными в системах ДОП делает их более гибкими, чем традиционные системы ООП.
Когда код отделен от данных, у нас есть свобода работать над кодом и данными
изолированно.
Мы представляем данные как объекты данных.
Мы обнаруживаем объекты данных нашей системы и сортируем их по группам
высокого уровня либо в виде вложенного списка, либо в виде ментальной карты.
Систему ДОП легче понять, чем традиционную систему ООП, потому что система разделена на две части: объекты данных и модули кода.
В ДОП модуль кода представляет собой совокупность функций без состояния.
Системы ДОП являются гибкими. Довольно часто они адаптируются к изме-няющимся требованиям, не меняя дизайн системы.
В традиционном ООП состояние объекта является неявным аргументом для
методов объекта.
Функции без состояния получают данные, которыми они манипулируют, в качестве явного аргумента.
Высокоуровневые модули системы ДОП соответствуют объектам данных высокого уровня.
Единственным видом связи между модулями кода является отношение использования.
Единственными видами связи между объектами данных являются связь и ком-позиционное отношение.
Обсуждение полиморфизма в ДОП см. в главе 13.
Основные манипуляции с данными
Медитация и программирование
В ЭТОЙ ГЛАВЕ РАССМАТРИВАЮТСЯ
Представление записей с помощью строковых карт для повышения
гибкости.
Манипулирование данными с помощью универсальных функций.
Доступ к каждой части информации по ее информационному пути.
Свободное получение сериализации JSON.
После того как в предыдущей главе мы узнали, почему и как нужно отделять код от
данных, давайте поговорим о самих данных. В отличие от традиционного ООП, где
проектирование системы, как правило, предполагает жесткую иерархию классов, ДОП предписывает представлять нашу модель данных в виде гибкой комбинации
карт и массивов (или списков), где мы можем получить доступ к каждой части
информации по информационному пути. Эта глава представляет собой глубокое
погружение во второй принцип ДОП.
ПРИНЦИП № 2. Представляйте объекты данных с помощью общих структур данных.
Мы повышаем гибкость системы, когда представляем записи в виде строковых
отображений, а не в виде объектов, созданных из классов. Это освобождает данные
от жесткости системы, основанной на классах. Данные становятся первоклассным
гражданином, использующим универсальные функции для добавления, удаления
или переименования полей.
ПРИМЕЧАНИЕ. Мы называем карты, содержащие строки в качестве ключей, строковыми
картами.
Зависимость между кодом, который манипулирует данными, и самими данными
является слабой зависимостью. Коду нужно знать только ключи определенных полей в записи, которой он хочет манипулировать. Коду даже не нужно знать обо
70
Часть 1. Гибкость
всех ключах в записи, только о тех, которые имеют к ней отношение. В этой главе
мы будем иметь дело только с запросом данных. Управление изменениями состояния системы мы обсудим в следующей главе.
3.1. Разработка модели данных
Во время обеда в Simple Тео и Джо не говорят о программировании. Вместо этого
они начинают узнавать друг друга на личном уровне. Тео обнаруживает, что Джо
женат на Кей, которая только что открыла свою практику творческой терапии после многих лет изучения различных областей, связанных с благополучием. Нерайя, их 14-летний сын, увлечен дронами, в то время как Аурелия, их 12-летняя дочь, играет на поперечной флейте.
Джо говорит Тео, что он практикует медитацию в течение 10 лет. Медитация, по
его словам, научила его, как избавиться от постоянного погружения в «штормовые
мысли» (особенно негативные мысли, которые могут быть источником больших
страданий), чтобы достичь более прямых отношений с реальностью. Чем больше он
учится воспринимать реальность такой, какая она есть, тем спокойнее его разум.
Когда он впервые начал практиковать медитацию, иногда это было трудно и даже
странно, но, проявляя настойчивость, он с каждым годом улучшал свое самочувст-вие.
Когда они возвращаются в офис, Джо говорит Тео, что его следующий шаг в их
путешествии по ДОП будет связан с моделями данных. Это включает в себя представление данных.
Джо: Когда мы разрабатываем информационную часть нашей системы, мы можем
делать это изолированно.
Тео: Что вы подразумеваете под изоляцией?
Джо: Я имею в виду, что вам не нужно беспокоиться о коде, только о данных.
Тео: О, точно. Я помню, вы рассказывали мне, как это делает систему ДОП проще, чем ООП. Разделение задач — это принцип проектирования, к которому я привык в ООП.
Джо: Именно.
Тео: И когда мы думаем о данных, единственные отношения, о которых мы должны думать, — это ассоциация и композиция.
Джо: Верно.
Тео: Будет ли дизайн модели данных существенно отличаться от модели данных, которую я привык разрабатывать как ООП-разработчик?
Джо: Не так уж и существенно.
Тео: Хорошо. Позвольте мне посмотреть, смогу ли я нарисовать диаграмму объектов данных в стиле ДОП.
Тео бросает взгляд на ментальную карту данных, которую он нарисовал ранее
утром. Затем он рисует диаграмму на рис. 3.1.



Глава 3. Основные манипуляции с данными
71
Он уточняет детали полей каждого объекта данных и вид отношений между объектами. На рис. 3.2 показан результат этой переопределенной диаграммы объектов
данных.
Рис. 3.1. Ментальная карта данных системы управления библиотекой
Рис. 3.2. Модель данных системы управления библиотекой
72
Часть 1. Гибкость
Джо: Следующий шаг — более четко описать отношения между объектами.
Тео: Что вы имеете в виду?
Джо: Например, на вашей диаграмме объектов Book и Author связаны ассоциатив-ным отношением «многие ко многим». Как это отношение будет представлено
в вашей программе?
Тео: В объекте Book будет коллекция идентификаторов автора, а в объекте Author будет коллекция идентификаторов книги.
Джо: Звучит хорошо. И каким будет идентификатор книги?
Тео: Книжный ISBN.
ПРИМЕЧАНИЕ. Международный стандартный номер книги (ISBN) — это цифровой идентификатор коммерческой книги, который должен быть уникальным.
Джо: И где вы будете хранить индекс, который позволит вам получить книгу по ее
ISBN?
Тео: В Catalog, потому что каталог содержит индекс bookByISBN.
Джо: А как насчет идентификатора автора?
Тео: Идентификатор автора — это имя автора в нижнем регистре и с тире вместо
пробелов (при условии, что у нас нет двух авторов с одинаковым именем).
Джо: И я предполагаю, что у вас также есть индекс автора в Catalog?
Тео: Именно так.
Джо: Отлично. Вы были на 100 % точны в отношении связи между Book и Author.
Я попрошу вас сделать то же самое с другими отношениями системы.
Для Тео это довольно легко сделать, т. к. он делал это много раз в качестве разработчика ООП. На рис. 3.3 представлена подробная диаграмма объектов системы
Тео.
ПРИМЕЧАНИЕ. Под позиционной коллекцией мы подразумеваем коллекцию, в которой
элементы расположены по порядку (например, список или массив). Под индексом мы подразумеваем коллекцию, элементы которой доступны через ключ (например, хеш-карту или словарь).
Объект Catalog содержит два индекса:
booksByIsbn — ключи — это книжный ISBN, а значения — это объекты Book. Его
тип отмечен как {Book}.
authorsById — ключи являются идентификаторами авторов, а значения являются
объектами Author. Его тип отмечен как {Author}.
Внутри объекта Book у нас есть authors, которая представляет собой позиционную
коллекцию идентификаторов авторов типа [String]. Внутри объекта Author у нас
есть books, которая представляет собой набор идентификаторов книг типа [String].


Глава 3. Основные манипуляции с данными
73
Рис. 3.3. Модель отношений управления библиотекой. Пунктирные линии
(например, между Book и Author) обозначают косвенные отношения,
[String] обозначает позиционный набор строк, а {Book} обозначает индекс Books ПРИМЕЧАНИЕ. Для обозначения коллекций и типов индексов позиционная коллекция
строк обозначается как [String]. Указатель книг отмечен как {Book}. В контексте модели данных ключи индекса всегда являются строками.
Существует пунктирная линия между Book и Author, что означает, что связь между
Book и Author является косвенной. Чтобы получить доступ к коллекции объектов
Author из объекта Book, мы будем использовать индекс authorById, определенный
в объекте Catalog.
Джо: Мне нравится ваша диаграмма объектов данных.
Тео: Благодарю.
Джо: Можете ли вы сказать мне, какие три вида агрегаций данных представлены на
вашей диаграмме (и фактически на любой диаграмме объектов данных)?
Тео: Давайте посмотрим... у нас есть позиционные коллекции, такие как authors в Book. В каталоге у нас есть такие индексы, как booksByIsbn. Я не могу найти
третий.

74
Часть 1. Гибкость
Джо: Третий вид агрегации данных — это то, что мы до сих пор называли «объектом» (например, Library, Catalog, Book и т. д.). А общий термин для объекта
в информатике — запись.
ПРИМЕЧАНИЕ. Запись — это структура данных, которая группирует связанные элементы
данных. Это набор полей, возможно, разных типов данных.
Тео: Правильно ли говорить, что диаграмма объектов данных состоит только из
записей, позиционных коллекций и индексов?
Джо: Это правильно. Можете ли вы сделать аналогичное заявление об отношениях
между объектами?
Тео: Отношения на диаграмме объектов данных представляют собой либо композицию (сплошная линия с полным ромбом), либо ассоциацию (пунктирная линия с пустым ромбом). Оба типа отношений могут быть либо «один к одному», либо «один ко многим», либо «многие ко многим».
Джо: Прекрасно!
СОВЕТ. Диаграмма объектов данных состоит из записей, значениями которых являются
либо примитивы, позиционные коллекции, либо индексы. Связь между записями является
либо композицией, либо ассоциацией.
3.2. Представление записей в виде карт
До сих пор мы иллюстрировали преимущества, которые получаем от разделения
кода и данных на высоком системном уровне. Существует разделение проблем между кодом и данными, и каждая часть имеет четкие ограничения:
Код состоит из статических функций, которые получают данные в качестве
явного аргумента.
Объекты данных моделируются как записи, а отношения между записями представлены позиционными коллекциями и индексами.
Теперь возникает вопрос о представлении данных. ДОП не может сказать ничего
особенного о коллекциях и индексах. Тем не менее существует твердое мнение
о представлении записей: записи должны быть представлены общими структурами
данных, такими как карты.
Это относится как к языкам ООП, так и к языкам ФП. В динамически типизированных языках, таких как JavaScript, Python и Ruby, представление данных кажется
естественным. В то время как в статически типизированных языках, таких как Java и C#, оно немного более громоздко.
Тео: Мне действительно любопытно узнать, как мы представляем позиционные
коллекции, индексы и записи в ДОП.
Джо: Давайте начнем с позиционных коллекций. ДОП не может сказать ничего
особенного о представлении коллекций. Они могут быть связанными списками, массивами, векторами, наборами или другими коллекциями, наиболее подходя-щими для конкретного случая использования.

Глава 3. Основные манипуляции с данными
75
Тео: Как и в ООП.
Джо: Правильно! На данный момент, чтобы упростить задачу, мы будем использовать массивы для представления позиционных коллекций.
Тео: А как насчет индексов?
Джо: Индексы представлены в виде гомогенных строковых карт.
Тео: Что вы подразумеваете под гомогенной картой?
Джо: Я имею в виду, что все значения карты имеют один и тот же вид. Например, в индексе Book все значения — Book, а в индексе Author все значения — Author и т. д.
Тео: И снова как в ООП!
ПРИМЕЧАНИЕ. Гомогенная карта — это карта, на которой все значения имеют один и тот
же тип. Гетерогенная карта — это карта, где значения относятся к разным типам.
Джо: А вот теперь большой сюрприз: в ДОП записи представлены в виде карт, точнее, гетерогенных строковых карт.
Джо подходит к белой доске и начинает рисовать. Закончив, он показывает Тео
диаграмму на рис. 3.4.
Рис. 3.4. Строительные блоки представления данных
Тео некоторое время молчит. Он потрясен, услышав, что объекты данных системы
могут быть представлены в виде общей структуры данных, где имена полей и типы
значений не указаны в классе. Затем Тео спрашивает Джо:
Тео: Каковы преимущества такого вздора?
Джо: Гибкость и универсальность.
Тео: Не могли бы вы объяснить, пожалуйста?
Джо: Я объясню через мгновение, но перед этим я хотел бы показать вам, как
выглядит экземпляр записи в системе ДОП.
Тео: Ладно.
76
Часть 1. Гибкость
Джо: Давайте возьмем в качестве примера книгу «Хранители» (англ. «Watchmen») Алана Мура и Дэйва Гиббонса, это мой любимый комикс. Этот шедевр был
опубликован в 1987 году. Я собираюсь предположить, что в физической библиотеке есть две копии этой книги, идентификатор которой — nyc-central-lib, и что
одна из двух копий в настоящее время отсутствует. Вот как я бы представил
запись Book для «Хранителей» в ДОП.
Джо подходит ближе к ноутбуку Тео. Он открывает текстовый редактор (не IDE!) и
вводит запись Book для Тео.
Листинг 3.1. Экземпляр записи Book, представленный в виде карты
{
"isbn": "978-1779501127",
"title": "Watchmen",
"publicationYear": 1987,
"authors": ["alan-moore", "dave-gibbons"],
"bookItems": [
{
"id": "book-item-1",
"libId": "nyc-central-lib",
"isLent": true
},
{
"id": "book-item-2",
"libId": "nyc-central-lib",
"isLent": false
}
]
}
Тео смотрит на экран ноутбука. У него есть вопрос.
Тео: Как я должен создать экземпляр записи Book для «Хранителей» программно?
Джо: Это зависит от возможностей, которые предлагает ваш язык программирования для создания экземпляров карт. С динамическими языками, такими как
JavaScript, Ruby или Python, это просто, потому что мы можем использовать
литералы для карт и массивов. Позвольте мне показать вам, как это делается.
Джо записывает код JavaScript, который создает экземпляр записи Book, которая
представляется в виде карты в JavaScript. Он показывает код Тео.
Листинг 3.2. Запись Book, представленная в виде карты в JavaScript var watchmenBook = {
"isbn": "978-1779501127",
"title": "Watchmen",
Глава 3. Основные манипуляции с данными
77
"publicationYear": 1987,
"authors": ["alan-moore", "dave-gibbons"],
"bookItems": [
{
"id": "book-item-1",
"libId": "nyc-central-lib",
"isLent": true
},
{
"id": "book-item-2",
"libId": "nyc-central-lib",
"isLent": false
}
]
}
Тео: А если на Java??
Джо: Это немного более утомительно, но все же выполнимо с помощью неизменяемых методов Map и List static factory.
ПРИМЕЧАНИЕ. Смотрите раздел «Создание неизменяемых списков, наборов и карт» на
http://mng.bz/voGm для получения дополнительной информации об этой библиотеке Java core.
Джо вводит Java-код для создания экземпляра записи Book, представленной в виде
карты. Он показывает Тео Java-код.
Листинг 3.3. Запись Book, представленная в виде карты на Java
Map watchmen = Map.of(
"isbn", "978-1779501127",
"title", "Watchmen",
"publicationYear", 1987,
"authors", List.of("alan-moore", "dave-gibbons"),
"bookItems", List.of(
Map.of(
"id", "book-item-1",
"libId", "nyc-central-lib",
"isLent", true
),
Map.of (
"id", "book-item-2",
"libId", "nyc-central-lib",
"isLent", false
)
)
);

78
Часть 1. Гибкость
СОВЕТ. В ДОП мы представляем запись в виде гетерогенного отображения строк.
Тео: Я бы определенно предпочел создать запись Book, используя класс Book и класс
BookItem.
Листинг 3.4. Запись Book как экземпляра класса Book в JavaScript
class Book {
isbn;
title;
publicationYear;
authors;
bookItems;
constructor(isbn, title, publicationYear, authors, bookItems)
{
this.isbn = isbn;
this.title = title;
this.publicationYear = publicationYear;
this.authors = authors;
this.bookItems = bookItems;
}
}
class BookItem {
id;
libId;
isLent;
constructor(id, libId, isLent) {
this.id = id;
this.libId = libId;
this.isLent = isLent;
}
}
var watchmenBook = new Book("978-1779501127",
"Watchmen",
1987,
["alan-moore", "dave-gibbons"],
[new BookItem("book-item-1", "nyc-central-lib", true), new BookItem("book-item-2", "nyc-central-lib", false)]); Джо: Тео, почему вы предпочитаете классы картам для представления записей?
Тео: Это делает форму данных записи частью моей программы. В результате IDE
может автоматически заполнять имена полей, и ошибки обнаруживаются во
время компиляции.



Глава 3. Основные манипуляции с данными
79
Джо: Справедливо. Могу ли я показать вам некоторые недостатки этого подхода?
Тео: Конечно.
Джо: Представьте, что вы хотите отобразить информацию о книге в контексте
результатов поиска. В этом случае вместо идентификаторов авторов вы хотите
отобразить имена авторов, и вам не нужна информация об элементе книги. Как
бы вы с этим справились?
Тео: Я бы создал класс BookInSearchResults без члена bookItems и с членом
authorNames вместо члена authorIds класса Book. Кроме того, мне нужно было бы
написать конструктор копирования, который получает объект Book.
Джо: В классическом ООП тот факт, что данные создаются только с помощью
классов, обеспечивает безопасность. Но эта безопасность достигается за счет
гибкости.
СОВЕТ. В модели данных существует компромисс между гибкостью и безопасностью.
Тео: Тогда это может быть иным?
Джо: В подходе ДОП, где записи представлены в виде карт, нам не нужно создавать класс для каждого варианта данных. Мы можем свободно добавлять, удалять и переименовывать поля записей динамически. Наша модель данных является гибкой.
Тео: Это интересно!
СОВЕТ. В ДОП модель данных является гибкой. Мы можем свободно добавлять, удалять и переименовывать поля записи динамически во время выполнения.
Джо: Теперь позвольте мне поговорить об универсальности. Как бы вы сериализо-вали содержимое объекта Book в JSON?
СОВЕТ. В ДОП записи обрабатываются с помощью универсальных функций.
Тео: О нет! Я помню, что во время работы над прототипом Klafim мне приснился
кошмар о сериализации JSON, когда я разрабатывал первую версию системы
управления библиотекой.
Джо: В ДОП сериализация записи в JSON очень проста.
Тео: Требуется ли использование отражения для того, чтобы просматривать поля
записи, как это делает библиотека Java Gson?
ПРИМЕЧАНИЕ. Перейдите по ссылке https://github.com/google/gson для получения дополнительной информации о Gson.
Джо: Вовсе нет! Помните, что в ДОП запись — это не более чем данные. Мы можем написать универсальную функцию сериализации JSON, которая работает
с любой записью. Это может быть Book, Author, BookItem или что-либо еще.
Тео: Потрясающе!


80
Часть 1. Гибкость
СОВЕТ. В ДОП вы получаете сериализацию JSON совершенно свободно.
Джо: На самом деле, как я вам сейчас покажу, многие манипуляции с данными
можно выполнять с помощью универсальных функций.
Тео: Являются ли универсальные функции частью языка?
Джо: Это зависит от функций и от языка. Например, JavaScript предоставляет стан-дартную функцию сериализации JSON под названием JSON.stringify, но не для
пропуска нескольких ключей или для переименования ключей.
Тео: Досадно.
Джо: Не совсем; существуют сторонние библиотеки, которые предоставляют средства для манипулирования данными. Популярной библиотекой для манипулирования данными в экосистеме JavaScript является Lodash.
ПРИМЕЧАНИЕ. Перейдите по ссылке https://lodash.com /, чтобы узнать больше о Lodash.
Тео: А как насчет других языков?
Джо: Lodash был перенесен на Java, C#, Python и Ruby. Позвольте мне дать несколько сайтов для вас.
Джо добавляет эти сайты в закладки для Тео:
https://javalibs.com/artifact/com.github.javadev/underscore-lodash для Java; https://www.nuget.org/packages/lodash/ для C#;
https://github.com/dgilland/pydash для Python;
https://rudash-website.now.sh/ для Ruby.
ПРИМЕЧАНИЕ. В книге Lodash используется для демонстрации того, как манипулировать
данными с помощью универсальных функций, но в Lodash нет ничего особенного. Точно такой же подход может быть реализован с помощью других библиотек обработки данных или
пользовательского кода.
Тео: Здорово!
Джо: На самом деле, Lodash и ее богатый набор функций обработки данных могут
быть перенесены на любой язык. Вот почему так выгодно представлять записи
в виде карт.
СОВЕТ. ДОП идет на компромисс с безопасностью данных, чтобы получить гибкость и
универсальность.
У доски Джо быстро набрасывает компромиссы (см. табл. 3.1).
Таблица 3.1. Компромисс между безопасностью, гибкостью и универсальностью
Функции ООП
ДОП
Безопасность Высокая
Низкая
Гибкость Низкая Высокая
Универсальность Низкая
Высокая
Глава 3. Основные манипуляции с данными
81
3.3. Манипулирование данными
с помощью универсальных функций
Джо: Теперь позвольте мне показать вам, как манипулировать данными в ДОП
с помощью универсальных функций.
Тео: Да, мне очень любопытно посмотреть, как вы будете реализовывать функциональность поиска в системе управления библиотекой.
Джо: Хорошо. Во-первых, давайте создадим экземпляр записи Catalog для данных
каталога библиотеки, где у нас есть единственная книга «Хранители».
Листинг 3.5. Запись Catalog
var catalogData = {
"booksByIsbn": {
"978-1779501127": {
"isbn": "978-1779501127",
"title": "Watchmen",
"publicationYear": 1987,
"authorIds": ["alan-moore", "dave-gibbons"],
"bookItems": [
{
"id": "book-item-1",
"libId": "nyc-central-lib",
"isLent": true
},
{
"id": "book-item-2",
"libId": "nyc-central-lib",
"isLent": false
}
]
}
},
"authorsById": {
"alan-moore": {
"name": "Alan Moore",
"bookIsbns": ["978-1779501127"]
},
"dave-gibbons": {
"name": "Dave Gibbons",
"bookIsbns": ["978-1779501127"]
}
}
}
Тео: Я вижу два индекса, о которых мы говорили, booksByIsbn и authorsById. Как вы
отличаете запись от индекса в ДОП?


82
Часть 1. Гибкость
Джо: На диаграмме данных существует четкое различие между записями и индексами. Но в нашем коде и то и другое является обычными данными.
Тео: Я думаю, именно поэтому этот подход называется дата-ориентированным
программированием.
Джо: Видите, как просто визуализировать любую часть системных данных внутри
программы? Причина в том, что данные представлены как данные!
СОВЕТ. В ДОП данные представляются в виде данных.
Тео: Это звучит как лапалиссада1.
Джо: О, неужели это так? Я бы не был так уверен! В ООП данные обычно представлены объектами, что усложняет визуализацию данных внутри программы.
СОВЕТ. В ДОП мы можем визуализировать любую часть системных данных.
Тео: Как бы вы извлекли название конкретной книги из данных каталога?
Джо: Отличный вопрос! Фактически в системе ДОП каждая часть информации
имеет информационный путь, из которого мы можем извлечь информацию.
Тео: Информационный путь?
Джо: Например, информационный путь к названию книги «Хранители» в каталоге — ["booksByIsbn", "978-1779501127", "title"].
Тео: А, я понимаю. Является ли информационный путь чем-то вроде пути к файлу, но имена в информационном пути соответствуют вложенным данным?
Джо: Вы совершенно правы. И как только у нас будет путь к фрагменту информации, мы сможем извлечь эту информацию с помощью функции _.get от Lodash.
Джо набирает несколько символов на ноутбуке Тео. Тео поражен тем, как мало ко-да требуется, чтобы получить название книги.
Листинг 3.6. Извлечение названия книги из ее информационного пути
_.get(catalogData, ["booksByIsbn", "978-1779501127", "title"])
// → "Watchmen"
Тео: Как лаконично. Интересно, насколько сложно было бы реализовать такую
функцию, как _.get, самому.
После нескольких минут проб и ошибок Тео смог создать свою реализацию. Он
показывает Джо код.
Листинг 3.7. Пользовательская реализация get
function get(m, path) {
var res = m;
1 Лапалиссада — это очевидная истина, трюизм или тавтология, которая производит комический эффект.
Глава 3. Основные манипуляции с данными
83
for(var i = 0; i < path.length; i++) {❶
var key = path[i];
res = res[key];
}
return res;
}
❶ Мы могли бы использовать forEach вместо цикла for.
После тестирования реализации get, предложенной Тео, Джо хвалит Тео. Он благодарен, что Тео так быстро схватывает суть.
Листинг 3.8. Тестирование пользовательской реализации get
get(catalogData, ["booksByIsbn", "978-1779501127", "title"]);
// → "Watchmen"
Джо: Молодец!
Тео: Интересно, работает ли такая функция, как _.get, хорошо на статически типизированном языке, таком как Java?
Джо: Это зависит от того, нужно ли вам только передавать значение по кругу или
обращаться к значению конкретно.
Тео: Я не улавливаю сути.
Джо: Представьте, что как только вы получаете название книги, вы хотите преобразовать строку в строку верхнего регистра. Вам нужно выполнить статическое
приведение к String, верно? Здесь позвольте мне показать вам пример, который
преобразует значение поля в строку, затем мы можем манипулировать им как
строкой.
Листинг 3.9. Преобразование значения поля в строку
((String)watchmen.get("title")).toUpperCase()
Тео: В этом есть смысл. Значения карты имеют разные типы, поэтому компилятор
объявляет ее как Map<String,Object>. Информация о типе поля теряется.
Джо: Это немного раздражает, но довольно часто наш код просто передает данные
по кругу. В этом случае нам не придется иметь дело со статическим приведением. Более того, в таком языке, как C#, при использовании типа dynamic данных
можно избежать приведения типов2.
2 См. http://mng.bz/4jo5 — документацию по C# о встроенной ссылке на динамические типы.
См. приложение A для получения подробной информации о динамических полях и приведении типов в C#.



84
Часть 1. Гибкость
СОВЕТ. В языках со статической типизацией нам иногда требуется статически приво-дить значения полей.
Тео: А как насчет производительности?
Джо: В большинстве языков программирования карты довольно эффективны. Доступ к полю на карте происходит немного медленнее, чем доступ к члену класса.
Обычно это не имеет значения.
СОВЕТ. Нет существенного снижения производительности при доступе к полю на карте
вместо того, чтобы быть членом класса.
Тео: Давайте вернемся к этой идее информационного пути. Это работает и в ООП.
Я мог бы получить доступ к названию книги «Хранители» с помощью
catalogData.booksByIsbn["978- 1779501127"].title. Я бы использовал члены класса
для полей записи и строки для ключей индекса.
Джо: Однако есть фундаментальная разница. Когда записи представлены в виде
карт, информация может быть извлечена по ее информационному пути с помощью универсальной функции, такой как _.get. Но когда записи представлены
в виде объектов, вам нужно написать определенный код для каждого типа
информационного пути.
Тео: Что вы подразумеваете под конкретным кодом? Что конкретно в
catalogData.books- ByIsbn["978-1779501127"].title?
Джо: В статически типизированном языке, таком как Java, вам нужно было бы импортировать определения классов для Catalog и Book.
Тео: И на динамически типизированном языке, таком как JavaScript?..
Джо: Даже в JavaScript, когда вы представляете записи с объектами, созданными из
классов, вы не можете легко написать функцию, которая получает путь в качестве аргумента и отображает информацию, соответствующую этому пути. Вам
пришлось бы написать определенный код для каждого типа пути. Вы получили
бы доступ к членам класса с помощью точечных обозначений, а к полям карты — с помощью скобочных обозначений.
Тео: Вы бы сказали, что в ДОП информационный путь — это первоклассный гражданин?
Джо: Абсолютно! Информационный путь может быть сохранен в переменной и
передан в качестве аргумента функции.
СОВЕТ. В ДОП вы можете извлекать каждую часть информации с помощью пути и универсальной функции.
Джо подходит к белой доске. Он рисует диаграмму, подобную приведенной на
рис. 3.5, на которой данные каталога представлены в виде дерева.
Джо: Видишь ли, Тео, каждая часть информации доступна по пути, состоящему из
строк и целых чисел. Например, путь к первой книге Алана Мура — ["catalog",
"authorsById", "alan-moore", "bookIsbns", 0].


Глава 3. Основные манипуляции с данными
85
Рис. 3.5. Данные каталога в виде дерева
3.4. Вычисление результатов поиска
Тео: Интересно. Я начинаю чувствовать силу выражения ДОП!
Джо: Подождите, это только начало. Позвольте мне показать вам, как просто написать код, который извлекает информацию о книге и отображает ее в результатах
поиска. Можете ли вы точно сказать мне, какая информация должна отображаться в результатах поиска?
Тео: Поиск информации о книге должен возвращать isbn, title и authorNames.
Джо: И как будет выглядеть запись BookInfo для «Хранителей»?
Тео быстро вводит код на своем ноутбуке. Затем он показывает его Джо.
Листинг 3.10. Запись BookInfo для Watchmen в контексте результата поиска
{
"title": "Watchmen",
"isbn": "978-1779501127",
"authorNames": [
"Alan Moore",
"Dave Gibbons",
]
}
86
Часть 1. Гибкость
Джо: Теперь я покажу вам шаг за шагом, как написать функцию, которая возвращает результаты поиска, соответствующие заголовку в формате JSON. Я буду
использовать общие функции манипулирования данными из Lodash.
Тео: Я готов!
Джо: Начнем с функции authorNames, которая вычисляет имена авторов записи Book, просматривая индекс authorById. Не могли бы вы сказать мне, какой информационный путь для имени автора, чей идентификатор — authorId?
Тео: Это ["authorsById", authorId, "name"].
Джо: Теперь позвольте мне показать вам, как получить имена нескольких авторов, используя _.map.
Джо вводит код для сопоставления идентификаторов авторов с именами авторов.
Тео с беспечным видом заглядывает через плечо Джо.
Листинг 3.11. Сопоставление идентификаторов авторов с именами авторов
_.map(["alan-moore", "dave-gibbons"],
function(authorId) {
return _.get(catalogData, ["authorsById", authorId,
"name"]);
});
// → ["Alan Moore", "Dave Gibbons"]
Тео: Что это за функция _.map? Это пахнет функциональным программированием!
Вы сказали, что мне не придется изучать ФП, чтобы реализовать ДОП!
Джо: Нет необходимости изучать функциональное программирование, чтобы использовать _.map, который представляет собой функцию, преобразующую значения коллекции. Вы можете реализовать это с помощью простого цикла for.
Тео проводит пару минут перед своим компьютером, выясняя, как реализовать
_.map. Теперь у него есть это!
Листинг 3.12. Пользовательская реализация map
function map(coll, f) {
var res = [];
for(var i = 0; i < coll.length; i++) {❶
res[i] = f(coll[i]);
}
return res;
}
❶ Мы могли бы использовать forEach вместо цикла for.
После тестирования реализации карты Тео Джо показывает Тео тест. Джо снова
делает комплимент Тео.
Глава 3. Основные манипуляции с данными
87
Листинг 3.13. Тестирование пользовательской реализации map
map(["alan-moore", "dave-gibbons"],
function(authorId) {
return _.get(catalogData, ["authorsById", authorId,
"name"]);
});
// → [ "Alan Moore", "Dave Gibbons"]
Джо: Отлично сработано!
Тео: Ты был прав! Это было нетрудно.
Джо: Теперь давайте реализуем authorNames, используя _.map.
Тео требуется несколько минут, чтобы придумать реализацию authorNames. Закончив, он поворачивает свой ноутбук к Джо.
Листинг 3.14. Вычисление имен авторов книги
function authorNames(catalogData, book) {
var authorIds = _.get(book, "authorIds");
var names = _.map(authorIds, function(authorId) {
return _.get(catalogData, ["authorsById", authorId, "name"]);
});
return names;
}
Тео: Нам также нужна функция bookInfo, которая преобразует запись книги в запись
BookInfo. Позвольте мне показать вам код для этого.
Листинг 3.15. Преобразование записи книги в запись BookInfo
function bookInfo(catalogData, book) {
var bookInfo = {
"title": _.get(book, "title"),
"isbn": _.get(book, "isbn"),
"authorNames": authorNames(catalogData, book)
};
return bookInfo; ❶
}
❶ Нет необходимости создавать класс для bookInfo.
Тео: Глядя на код, я вижу, что запись BookInfo содержит три поля: title, isbn и
authorNames. Есть ли способ получить эту информацию, не заглядывая в код?
Джо: Вы можете либо добавить его в диаграмму объектов данных, либо записать
в документацию функции bookInfo, либо и то и другое.
88
Часть 1. Гибкость
Тео: Я должен привыкнуть к идее, что в ДОП информация о поле записи не является частью программы.
Джо: Действительно, это не является частью программы, но это дает нам большую
гибкость.
Тео: Есть ли какой-нибудь способ для меня получить все и сразу?
Джо: Да, и когда-нибудь я покажу вам, как сделать информацию о поле записи
частью программы ДОП (см. главы 7 и 12).
Тео: Звучит интригующе!
Джо: Теперь, когда у нас есть все части на месте, мы можем написать нашу функцию searchBooksByTitle, которая возвращает информацию о книгах, соответствующих запросу. Сначала мы находим записи Book, которые соответствуют запросу с помощью _.filter, а затем преобразуем каждую запись Book в запись
BookInfo с помощью _.map и bookInfo.
Листинг 3.16. Поиск книг, соответствующих запросу
function searchBooksByTitle(catalogData, query) {
var allBooks = _.values(_.get(catalogData, "booksByIsbn")); var matchingBooks = _.filter(allBooks, function(book) {
return _.get(book, "title").includes(query); ❶
});
var bookInfos = _.map(matchingBooks, function(book) {
return bookInfo(catalogData, book);
});
return bookInfos;
}
❶ Функция includes JavaScript проверяет, содержит ли строка строку в качестве подстроки.
Тео: Вы снова используете функции Lodash без каких-либо объяснений!
Джо: Прости мне это. Я настолько привык к базовым функциям манипулирования
данными, что рассматриваю их как часть языка. Какие функции являются новы-ми для вас?
Тео: _.values и _.filter.
Джо: Что ж, _.values возвращает коллекцию, состоящую из значений карты, а
_.filter возвращает коллекцию, состоящую из значений, удовлетворяющих
предикату.
Тео: _.values кажутся тривиальными. Позвольте мне попробовать реализовать
_.filter.
Реализация _.filter занимает немного больше времени. В конце концов Тео удается сделать это правильно, и теперь он может ее протестировать.
Глава 3. Основные манипуляции с данными
89
Листинг 3.17. Пользовательская реализация filter
function filter(coll, f) {
var res = [];
for(var i = 0; i < coll.length; i++) {❶
if(f(coll[i])) {
res.push(coll[i]);
}
}
return res;
}
❶ Мы могли бы использовать forEach вместо цикла for.
Листинг 3.18. Тестирование пользовательской реализации filter
filter(["Watchmen", "Batman"], function (title) {
return title.includes("Watch");
});
// → ["Watchmen"]
Тео: Для меня немного странно, что для доступа к названию записи книги мне
нужно написать _.get(book, "title"). Я бы ожидал, что это будет book.title в точечной записи или book ["title"] в скобочной записи.
Джо: Помните, что book — это запись, которая не представлена как объект. Это
карта. Действительно, в JavaScript вы можете написать _.get(book, "title"), book.title или book["title"]. Но я предпочитаю использовать Lodash функцию
_.get. На некоторых языках обозначения в виде точек и скобок могут не работать на картах.
Тео: Быть независимым от языка имеет свою цену!
Джо: Хорошо, но не хотели бы вы протестировать searchBooksByTitle?
Тео: Абсолютно! Позвольте мне вызвать searchBooksByTitle для поиска книг, название которых содержит строку Watch.
Листинг 3.19. Тестирование searchBooksByTitle
searchBooksByTitle(catalogData, "Wat");
//[
// {
// "authorNames": [
// "Alan Moore",
// "Dave Gibbons"
// ],
// "isbn": "978-1779501127",
90
Часть 1. Гибкость
// "title": "Watchmen"
// }
//]
Тео: Кажется, это работает! Мы закончили с реализацией поиска?
Джо: Почти. Функция searchBooksByTitle, которую мы написали, будет частью модуля Catalog, и она возвращает коллекцию записей. Мы должны написать функцию, которая является частью модуля Library и которая возвращает строку
JSON.
Тео: Ранее вы говорили мне, что сериализация JSON в ДОП была простой.
Джо: Это правда. Код для searchBooksByTitleJSON извлекает запись Catalog, передает
ее в searchBooksByTitle и преобразует результаты в JSON с помощью
JSON.stringify. Это часть JavaScript. Вот, позвольте мне показать.
Листинг 3.20. Реализация поиска книг в библиотеке в формате JSON
function searchBooksByTitleJSON(libraryData, query) {
var results = searchBooksByTitle(_.get(libraryData, "catalog"), query); var resultsJSON = JSON.stringify(results);
return resultsJSON;
}
Джо: Чтобы протестировать наш код, нам нужно создать запись Library, содержащую нашу запись Catalog. Не могли бы вы сделать это для меня, пожалуйста?
Тео: Должна ли запись Library содержать все поля библиотеки (name, address и
UserManagement)?
Джо: В этом нет необходимости. На данный момент нам нужно только поле
catalog, затем тест для поиска книг.
Листинг 3.21. Запись Library
var libraryData = {
"catalog": {
"booksByIsbn": {
"978-1779501127": {
"isbn": "978-1779501127",
"title": "Watchmen",
"publicationYear": 1987,
"authorIds": ["alan-moore",
"dave-gibbons"],
"bookItems": [
{
"id": "book-item-1",
"libId": "nyc-central-lib",
"isLent": true
},
Глава 3. Основные манипуляции с данными
91
{
"id": "book-item-2",
"libId": "nyc-central-lib",
"isLent": false
}
]
}
},
"authorsById": {
"alan-moore": {
"name": "Alan Moore",
"bookIsbns": ["978-1779501127"]
},
"dave-gibbons": {
"name": "Dave Gibbons",
"bookIsbns": ["978-1779501127"]
}
}
}
};
Листинг 3.22. Тест для поиска книг в библиотеке в формате JSON
searchBooksByTitleJSON(libraryData, "Wat");
Тео: Как мы собираемся объединить четыре функции, которые мы написали ранее?
Джо: Функции authorNames, bookInfo и searchBooksByTitle входят в модуль Catalog, а searchBooksByTitleJSON — в модуль Library.
Листинг 3.23. Вычисление результатов поиска для Library и Catalog
class Catalog {
static authorNames(catalogData, book) {
var authorIds = _.get(book, "authorIds");
var names = _.map(authorIds, function(authorId) {
return _.get(catalogData, ["authorsById", authorId, "name"]);
});
return names;
}
static bookInfo(catalogData, book) {
var bookInfo = {
"title": _.get(book, "title"),
"isbn": _.get(book, "isbn"),
"authorNames": Catalog.authorNames(catalogData, book)
};❶
return bookInfo;
}

92
Часть 1. Гибкость
static searchBooksByTitle(catalogData, query) {
var allBooks = _.get(catalogData, "booksByIsbn");
var matchingBooks = _.filter(allBooks,
function(book) {❷
return _.get(book, "title").includes(query);
});
var bookInfos = _.map(matchingBooks,
function(book) {
return Catalog.bookInfo(catalogData, book);
});
return bookInfos;
}
}
class Library {
static searchBooksByTitleJSON(libraryData, query)
var catalogData = _.get(libraryData, "catalog");
var results = Catalog.searchBooksByTitle(catalogData, query);
var resultsJSON = JSON.stringify(results); ❸
return resultsJSON;
}
}
❶ Нет необходимости создавать класс для bookInfo.
❷ Когда _.filter передается карте, он перебирает значения карты.
❸ Преобразует данные в JSON (часть JavaScript).
После тестирования окончательного кода в листинге 3.24 Тео снова просматривает
исходный код из листинга 3.23. Через несколько секунд он чувствует, что у него
снова начинается Ага!-момент.
Листинг 3.24. Результаты поиска в формате JSON
Library.searchBooksByTitleJSON(libraryData, "Watchmen");
// → "[{\"title\":\"Watchmen\",\"isbn\":\"978-1779501127\",
// → \"authorNames\":[\"Alan Moore\",\"Dave Gibbons\"]}]"
Тео: Важно не то, что код лаконичен, а то, что код не содержит абстракций. Это
просто манипуляция данными!
Джо отвечает улыбкой, которая говорит: «Ты понял, мой друг!»
Джо: Это напоминает мне о том, что мой первый учитель медитации сказал мне
10 лет назад: медитация помогает уму воспринимать реальность такой, какая она
есть, без абстракций, созданных нашими мыслями.
СОВЕТ. В ДОП многие части нашей кодовой базы, как правило, касаются только манипулирования данными без каких-либо абстракций.
Глава 3. Основные манипуляции с данными
93
3.5. Обработка записей различных типов
Мы видели, как ДОП позволяет нам относиться к записям как к первоклассным
гражданам, которыми можно гибко манипулировать с помощью универсальных
функций. Но если запись — это не более чем совокупность полей, как мы узнаем, каков тип записи? У ДОП есть удивительный ответ на этот вопрос.
Тео: У меня есть вопрос. Если запись — это не более чем карта, как вы узнаете тип
записи?
Джо: Это отличный вопрос с неожиданным ответом.
Тео: Удивите меня.
Джо: В большинстве случаев нет необходимости знать тип записи.
Тео: Что?! Как же так?
Джо: Я имею в виду, что самое важное — это значения полей. Например, взгляните
на исходный код Catalog.authorNames. Он работает с записью Book, но единственное, что имеет значение, — это значение поля authorIds.
Сомневаясь, Тео смотрит на исходный код Catalog.authorNames. Это то, что видит
Тео.
Листинг 3.25. Вычисление имен авторов книги
function authorNames(catalogData, book) {
var authorIds = _.get(book, "authorIds");
var names = _.map(authorIds, function(authorId) {
return _.get(catalogData, ["authorsById", authorId, "name"]);
});
return names;
}
Тео: Как насчет различия между различными типами пользователей, такими как
Member и Librarian? Я имею в виду, что у них обоих есть email и
encrypttedPassword. Как вы узнаете, представляет ли запись Member или Librarian?
Джо: Просто. Вы проверяете, найдена ли запись в индексе librariansByEmail или
MembersByEmail в индексе Catalog.
Тео: Не могли бы вы быть более конкретными?
Джо: Конечно! Позвольте мне написать, как могли бы выглядеть данные управления пользователями нашей крошечной библиотеки, предполагая, что у нас есть
один библиотекарь и один читатель. Чтобы упростить задачу, я шифрую пароли
с помощью наивной кодировки base-64 для записи UserManagement.
Листинг 3.26. Запись UserManagement
var userManagementData = {
"librariansByEmail": {

94
Часть 1. Гибкость
"[email protected]" : {
"email": "[email protected]",
"encryptedPassword": "bXlwYXNzd29yZA=="❶
}
},
"membersByEmail": {
"[email protected]": {
"email": "[email protected]",
"encryptedPassword": "c2VjcmV0",❷
"isBlocked": false,
"bookLendings": [
{
"bookItemId": "book-item-1",
"bookIsbn": "978-1779501127",
"lendingDate": "2020-04-23"
}
]
}
}
}
❶ Кодировка base-64 «mypassword».
❷ Кодировка base-64 «секрет».
СОВЕТ. В большинстве случаев нет необходимости знать тип записи.
Тео: Этим утром вы сказали мне, что покажете мне код для функции
UserManagement.IsLibrary сегодня днем.
Джо: Итак, уже полдень, и я собираюсь выполнить свое обещание.
Джо реализует isLibrarian. После небольшой паузы он затем выдает тест на
isLibrarian.
Листинг 3.27. Проверка того, является ли пользователь библиотекарем
function isLibrarian(userManagement, email) {
return _.has(_.get(userManagement, "librariansByEmail"), email);
}
Листинг 3.28. Тестирование isLibrarian
isLibrarian(userManagementData, "[email protected]");
// → true
Тео: Я предполагаю, что _.has — это функция, которая проверяет, существует ли
ключ на карте. Верно?
Джо: Так.
Глава 3. Основные манипуляции с данными
95
Тео: Хорошо. Вы просто проверяете, содержит ли карта librariansByEmail поле
email.
Джо: Ага.
Тео: Будете ли вы использовать тот же шаблон, чтобы проверить, является ли читатель Супер-читателем или VIP-читателем?
Джо: Конечно. У нас могли бы быть индексы SuperMembersByEmail и VIPMembersByEmail.
Но есть способ получше.
Тео: Какой?
Джо: Когда участник является VIP-читателем, мы добавляем в его запись поле
isVIP со значением true. Чтобы проверить, является ли читатель VIP-читателем, мы проверяем, установлено ли для поля isVIP значение true в записи читателя.
Вот как я бы закодировал isVIPMember.
Листинг 3.29. Проверка того, является ли читатель VIP-читателем
function isVIPMember(userManagement, email) {
return _.get(userManagement, ["membersByEmail", email, "isVIP"]) == true;
}
Тео: Я вижу, что вы обращаетесь к полю isVIP через его информационный путь
["membersByEmail", email, "isVIP"].
Джо: Да, я думаю, что это делает код кристально понятным.
Тео: Я согласен. Думаю, мы можем сделать то же самое для isSuperMember и установить для isSuperfield значение true, когда читатель является Супер-читателем?
Джо: Да, именно так.
Листинг 3.30. Код модуля UserManagement
class UserManagement {
isLibrarian(userManagement, email) {
return _.has(_.get(userManagement, "librariansByEmail"), email);
}
isVIPMember(userManagement, email) {
return _.get(userManagement,
["membersByEmail", email, "isVIP"]) == true;
}
isSuperMember(userManagement, email) {
return _.get(userManagement,
["membersByEmail", email, "isSuper"]) == true;
}
}

96
Часть 1. Гибкость
Тео пару секунд смотрит на код модуля UserManagement. Вдруг ему в голову приходит идея.
Тео: Почему бы не иметь поле типа в записи читателя, значение которого будет
либо VIP, либо Super?
Джо: Я предполагаю, что, согласно требованиям продукта, читатель может быть
как VIP, так и Super.
Тео: Хм... тогда поле types может быть коллекцией, содержащей VIP или Super или
и то и другое.
Джо: В некоторых ситуациях полезно иметь поле types, но я считаю, что проще
иметь логическое поле для каждой функции, поддерживаемой записью.
Тео: Есть ли название для таких полей, как isVIP и isSuper?
Джо: Я называю их функциональными полями.
СОВЕТ. Вместо сохранения информации о типе записи используйте поле функции (например, isVIP).
Тео: Можем ли мы использовать функциональные поля, чтобы различать библиотекарей и читателей?
Джо: Вы имеете в виду наличие полей isLibrarian и isMember?
Тео: Да, и иметь общий тип записи User как для библиотекарей, так и для читателей.
Джо: Можно, но я думаю, что проще иметь разные типы записей для библиотекарей и читателей: Librarian для библиотекарей и Member для читателей.
Тео: Почему?
Джо: Потому что существует четкое различие между библиотекарями и читателями
с точки зрения данных. Например, читатели могут брать книги напрокат, а библиотекари — нет.
Тео: Я согласен. Теперь нам нужно упомянуть два поля функций Member на нашей
диаграмме сущностей.
При этом Тео добавляет эти поля к своей диаграмме на доске. Закончив, он показывает Джо свои дополнения (рис. 3.6).
Джо: Вам нравится модель данных, которую мы разработали вместе?
Тео: Я нахожу ее довольно простой и понятной.
Джо: Это главная цель ДОП.
Тео: Кроме того, я приятно удивлен тем, насколько легко адаптироваться к меняющимся требованиям с точки зрения как кода, так и модели данных.
Джо: Я полагаю, вы также рады избавиться от сложных диаграмм иерархии классов.
Тео: Абсолютно! Кроме того, я думаю, что обнаружил интересную связь между
ДОП и медитацией.


Глава 3. Основные манипуляции с данными
97
Рис. 3.6. Модель данных управления библиотекой с полями характеристик Member, isVIP и isSuper Джо: Правда?
Тео: Когда мы ужинали в Simple, вы сказали мне, что медитация помогает вам воспринимать реальность такой, какая она есть, без фильтра ваших мыслей.
Джо: Это так.
Тео: Из того, чему вы научили меня сегодня, я понимаю, что в ДОП нам рекомендуется рассматривать данные как данные без фильтра наших классов.
Джо: Умно! Я никогда не замечал этой связи между этими двумя дисциплинами, которые так важны для меня. Я думаю, вы хотели бы продолжить свое путешествие в царство ДОП.
Тео: Определенно. Давай встретимся завтра снова.
Джо: К сожалению, завтра я везу свою семью на пляж, чтобы отпраздновать две-надцатый день рождения моей старшей дочери Аурелии.
Тео: Мои поздравления!
Джо: Мы могли бы встретиться снова в следующий понедельник, если ты не против.
Тео: С удовольствием!
98
Часть 1. Гибкость
Итоги
Принцип ДОП № 2 заключается в представлении объектов данных с помощью
общих структур данных.
Мы называем карты, содержащие строки в качестве ключей, строковыми картами.
Представление данных как данных означает представление записей с помощью
строковых карт.
Под позиционной коллекцией мы подразумеваем коллекцию, в которой элементы
расположены по порядку (например, список или массив).
Позиционная коллекция строк (Strings) помечается как [String].
Под индексом мы подразумеваем коллекцию, элементы которой доступны через
ключ (например, хеш-карту или словарь).
Индекс книг отмечен как {Book}.
В контексте модели данных ключи индекса всегда являются строками.
Запись — это структура данных, которая группирует связанные элементы данных. Это набор полей, возможно, разных типов данных.
Однородная карта — это карта, на которой все значения имеют один и тот же
тип.
Гетерогенная карта — это карта, где значения относятся к разным типам.
В ДОП мы представляем запись в виде гетерогенной строковой карты.
Диаграмма объектов данных состоит из записей, значениями которых являются
либо примитивы, позиционные коллекции, либо индексы.
Связь между записями на диаграмме сущностей данных является либо композицией, либо ассоциацией.
Информационная часть системы ДОП является гибкой, и каждая часть информации доступна по своему информационному пути.
В модели данных существует компромисс между гибкостью и безопасностью.
ДОП идет на компромисс с безопасностью данных, чтобы получить гибкость и
универсальность.
В ДОП модель данных является гибкой. Мы можем свободно добавлять, удалять
и переименовывать поля записи динамически во время выполнения.
Мы манипулируем данными с помощью универсальных функций.
Универсальные функции предоставляются либо самим языком, либо сторонними библиотеками, такими как Lodash.
Сериализация JSON реализована в терминах универсальной функции.
С одной стороны, мы потеряли безопасность доступа к полям записи через элементы, определенные во время компиляции. С другой стороны, мы освободили
Глава 3. Основные манипуляции с данными
99
данные от ограничений классов и объектов. Данные представлены в виде данных!
Слабая зависимость между кодом и данными облегчает адаптацию к изменяющимся требованиям.
Когда данные представлены в виде данных, легко визуализировать системные
данные.
Обычно нам не нужно сохранять информацию о типе записи.
Мы можем визуализировать любую часть системных данных.
В языках со статической типизацией нам иногда требуется статически приво-дить значения полей.
Вместо сохранения информации о типе записи мы используем поле объекта.
Нет существенного снижения производительности при доступе к полю на карте
вместо члена класса.
В ДОП вы можете извлекать каждую часть информации с помощью информационного пути и универсальной функции.
В ДОП многие части нашей кодовой базы, как правило, касаются только манипулирования данными без каких-либо абстракций.
Функции Lodash, представленные в этой главе
Функция Описание
get(map, path)
Получает значение map в path
has(map, path)
Проверяет, есть ли на карте поле в path
merge(mapA, mapB)
Создает карту, полученную в результате рекурсивных слияний между
mapA и mapB
values(map)
Создает массив значений map
filter(coll, pred)
Выполняет итерацию по элементам coll, возвращая массив всех
элементов, для которых pred возвращает true
map(coll, f)
Создает массив значений, запуская каждый элемент в coll через f
100
Часть 1. Гибкость
Управление состоянием
Путешествие во времени
В ЭТОЙ ГЛАВЕ РАССМАТРИВАЮТСЯ
Мультиверсионный подход к управлению состоянием.
Вычислительный этап изменения.
Фиксационный этап изменения.
Ведение истории предыдущих версий состояния.
Мы уже увидели, как ДОП обрабатывает запросы с помощью универсальных
функций, которые обращаются к системным данным, представленным в виде хеш-карты. В этой главе мы проиллюстрируем, как ДОП имеет дело с изменениями (запросами, которые меняют состояние системы). Вместо того чтобы обновлять суще-ствующее состояние, мы будем сохранять несколько версий системных данных.
В определенный момент состояние системы относится к определенной версии
системных данных. Эта глава представляет собой глубокое погружение в третий
принцип ДОП.
ПРИНЦИП № 3. Данные неизменяемы.
Для поддержания нескольких версий системных данных требуется, чтобы данные
были неизменяемыми. Это подтверждает свою эффективность как с точки зрения
вычислений, так и с точки зрения памяти при использовании приема под названием
структурное совместное использование. В этом приеме части данных, которые
являются общими для двух версий, используются совместно вместо того, чтобы
копироваться. В ДОП изменение подразделяется на два отдельных этапа.
На этапе вычисления мы находим следующую версию системных данных.
На этапе фиксации мы перемещаем состояние системы вперед, чтобы оно обра-щалось к версии системных данных, найденной на этапе вычисления.
Это отличие этапа вычисления от этапа фиксации позволяет нам свести к минимуму ту часть нашей системы, которая сохраняет состояние. Код только на этапе фик-
102
Часть 1. Гибкость
сации зависит от состояния, в то время как код на этапе вычисления изменения не
имеет состояния и состоит из универсальных функций, аналогичных коду для запроса. Реализация фиксационного этапа является общей для всех изменений. Как
следствие, на этапе фиксации у нас есть возможность гарантировать, что состояние
всегда обращается к валидной версии системных данных.
Еще одним преимуществом такого подхода к управлению состоянием является то, что мы можем отслеживать историю предыдущих версий системных данных. Восстановить систему в предыдущее состояние (если это необходимо) будет очень
просто. В табл. 4.1 показаны эти два этапа.
Таблица 4.1. Два этапа изменения
Этап Действие
Состояние
Реализация
Вычисление
Вычисляется следующая версия систем-
Без состояния
Частная
ных данных
Фиксация
Состояние системы перемещается вперед С сохранением
Общая
состояния
В этой главе мы предполагаем, что в нашей системе изменения не происходят конкурентно. Управление конкурентностью мы рассмотрим в следующей главе.
4.1. Несколько версий системных данных
Когда Джо приходит в офис в понедельник, он говорит Тео, что ему нужно раз-мяться, прежде чем начинать работу с умом. Тео и Джо идут гулять по кварталу, и тема разговора переходит к системам управления версиями. Они обсуждают, что
Git отслеживает всю историю фиксаций и что восстановить код в предыдущее состояние можно легко и быстро. Тео делится, что способность Git «путешествовать
во времени» напоминает ему один из его любимых фильмов, «Назад в будущее», а
Джо вспоминает, что месяц назад он смотрел трилогию «Назад в будущее» со своим 14-летним сыном Нерайей.
Завершив прогулку, они возвращаются в офис Тео. Тео и Джо угощаются из кофемашины на кухне, прежде чем приступить к сегодняшнему уроку.
Джо: Итак, мы уже видели, как в ДОП можно управлять запросами, которые извле-кают информацию из системы. Теперь я покажу вам, как можно управлять изменениями. Под изменением я имею в виду операцию, которая меняет состояние
системы.
ПРИМЕЧАНИЕ. Изменение — это операция, которая меняет состояние системы.
Тео: Есть ли в ДОП фундаментальная разница между запросами и изменениями?
В конце концов, все состояние системы представлено в виде хеш-карты. Я смог
бы легко написать код, который изменяет часть хеш-карты, и он будет похож на
код, который извлекает информацию из хеш-карты.



Глава 4. Управление состоянием
103
Джо: Вы могли бы изменить данные на месте, но тогда было бы сложно гарантировать, что код изменения не приведет систему к невалидной дате. Вы также поте-ряете возможность отслеживать предыдущие версии состояния системы.
Тео: Понятно. Итак, как мы поступаем с изменениями в ДОП?
Джо: Мы используем подход с несколькими версиями состояния, аналогичный
тому, что делает система управления версиями, такая как Git; мы управляем
различными версиями системных данных. В определенный момент состояние
системы относится к версии системных данных. После внесения изменения мы
перемещаем ссылку вперед.
Тео: Я что-то не пойму: является ли состояние системы изменяемым или неизменяемым?
Джо: Данные неизменяемы, но ссылка на состояние изменяема.
СОВЕТ. Данные неизменяемы, но ссылка на состояние изменяема.
Заметив замешательство на лице Тео, Джо быстро рисует на доске схему. Затем он
показывает Тео рис. 4.1, надеясь, что ему все станет ясно.
Рис. 4.1. После внесения изменения B состояние системы относится к версии данных V12.
После внесения изменения C состояние системы ссылается на данные V13
Тео: Означает ли это, что перед запуском кода изменения мы создаем копию системных данных?
Джо: Нет, это было бы неэффективно, т. к. нам пришлось бы сделать глубокую копию данных.
Тео: Как же тогда это работает?

104
Часть 1. Гибкость
Джо: Это работает с помощью приема под названием «структурное совместное использование», при котором большая часть данных между последующими версиями состояния используется совместно, а не копируется. В результате использования этого приема эффективно создаются новые версии системных данных
как с точки зрения памяти, так и с точки зрения вычислений.
Тео: Я заинтригован.
СОВЕТ. При помощи структурного совместного использования можно эффективно (с точки зрения памяти и вычислений) создавать новые версии данных.
Джо: Сейчас я подробно объясню, как работает структурное использование.
Тео еще раз смотрит на диаграмму с рис. 4.1, которая иллюстрирует, как состояние
системы относится к версии системных данных. Внезапно у него возникает вопрос.
Тео: А предыдущие версии системных данных сохраняются?
Джо: В простом приложении предыдущие версии автоматически удаляются программой сборки мусора. Но в некоторых случаях мы сохраняем исторические
ссылки на предыдущие версии данных.
Тео: В каких именно случаях?
Джо: Например, если мы хотим, чтобы в нашей системе, как в Git, поддерживались
путешествия во времени, мы можем легко вернуть систему к предыдущей версии состояния.
Тео: Теперь я понял, что вы имеете в виду, говоря, что данные неизменяемы, но
ссылка на состояние изменяема!
4.2. Структурное совместное использование
Как упоминалось в предыдущем разделе, структурное совместное использование
позволяет эффективно создавать новые версии неизменяемых данных. В ДОП мы
используем структурное совместное использование на вычислительном этапе изменения для того, чтобы найти следующее состояние системы на основе текущего
состояния. Внутри вычислительного этапа нам не нужно иметь дело с управлением
состоянием; это откладывается до этапа фиксации. Как следствие, код, участвующий в вычислительном этапе изменения, не имеет состояния и так же прост, как
код запроса.
Тео: Я действительно заинтригован этим более эффективным способом создания
новых версий данных. Как он работает?
Джо: Давайте возьмем простой пример из нашей библиотечной системы. Представьте, что вы хотите изменить значение поля в книге из каталога; например, год издания комикса «Хранители». Можете ли вы сообщить мне информационный путь для года издания «Хранителей»?
Тео бросает быстрый взгляд на данные каталога на рис. 4.2. Затем он отвечает на
вопрос Джо.


Глава 4. Управление состоянием
105
Рис. 4.2. Визуализация данных из каталога.
Узлы в информационном пути к году публикации «Хранителей» отмечены пунктирной рамкой
Тео: Информационный путь для года публикации «Хранителей»:
["catalog", "booksByIsbn", "978-1779501127", "publicationYear"].
Джо: Теперь позвольте мне показать, как можно использовать неизменяемую
функцию _.set, которую также предоставляет библиотека Lodash.
Тео: Подождите! Почему вы говорите, что эта функция неизменяемая? В документации Lodash для _.set на их веб-сайте написано, что она изменяет объект.
Джо: Вы правы, просто по умолчанию функции Lodash не являются неизменяемыми. Чтобы применить неизменяемую версию функций, нужно использовать
модуль Lodash FP, как описано в руководстве по Lodash FP.
ПРИМЕЧАНИЕ. По ссылке https://lodash.com/docs/4.17.15#set можно просмотреть документацию Lodash для функции _.set, а на https://github.com/lodash/lodash/wiki/FP-Guide вы найдете руководство по Lodash FP.
Тео: Имеют ли неизменяемые функции ту же сигнатуру, что и изменяемые функции?
Джо: По умолчанию порядок аргументов в неизменяемых функциях перемешивается. Руководство Lodash FP объясняет, как решить эту проблему. При использовании следующего фрагмента кода сигнатура неизменяемых функций будет
точно такой же, как и у изменяемых.

106
Часть 1. Гибкость
Листинг 4.1. Настройка Lodash таким образом,
чтобы неизменяемые и изменяемые функции имели одинаковую сигнатуру
_ = fp.convert({
"cap": false,
"curry": false,
"fixed": false,
"immutable": true,
"rearg": false
});
СОВЕТ. Чтобы использовать неизменяемые функции Lodash, примените модуль Lodash FP и настройте его таким образом, чтобы сигнатура неизменяемых функций была такой же, как в документации на сайте Lodash.
Тео: В общем и целом все еще можно полагаться на документацию Lodash при использовании неизменяемых версий функций.
Джо: За исключением отрывка в документации, в котором говорится, что функция
изменяет объект.
Тео: Конечно!
Джо: Теперь я покажу вам, как написать код, который создает версию библиотечных данных с неизменяемой функцией _.set.
Пальцы Джо порхают по клавиатуре Тео. Затем Тео просматривает получившийся
код, создающий версию библиотечных данных, в которой год издания «Хранителей» установлен на 1986 год.
Листинг 4.2. Использование _.set в качестве неизменяемой функции
var nextLibraryData = _.set(libraryData,
["catalog", "booksByIsbn",
"978-1779501127", "publicationYear"],
1986);
ПРИМЕЧАНИЕ. Функция считается неизменяемой, когда вместо изменения данных она
создает новую версию данных без изменения тех данных, которые она получает.
Тео: Вы уже говорили, что структурное совместное использование позволяет неизменяемым функциям быть эффективными с точки зрения памяти и вычислений.
Скажите, а что именно делает их эффективными?
Джо: С удовольствием, но перед этим вы должны ответить на ряд вопросов. Готовы?
Тео: Ну да...
Джо: На какую часть библиотечных данных влияет обновление года издания «Хранителей»: на UserManagement или на Catalog?


Глава 4. Управление состоянием
107
Тео: Только на Catalog.
Джо: На какую часть Catalog?
Тео: Только на индекс booksByIsbn.
Джо: На какую часть индекса booksByIsbn?
Тео: Только на запись Book, содержащую информацию о «Хранителях».
Джо: На какую часть записи Book?
Тео: Только на поле publicationYear.
Джо: Прекрасно! Теперь предположим, что текущая версия библиотечных данных
выглядит следующим образом.
Джо подходит к доске и рисует диаграмму. Результат показан на рис. 4.3.
Рис. 4.3. Высокоуровневая визуализация текущей версии Library
Тео: Пока все в порядке...
Джо: Далее, позвольте мне показать вам, что делает неизменяемая функция, когда
вы используете ее для создания новой версии Library, в которой год издания
«Хранителей» установлен на 1986 вместо 1987.
Джо обновляет свою диаграмму (рис. 4.4).
Тео: А не могли бы вы объяснить?



108
Часть 1. Гибкость
Рис. 4.4. Совместное структурное использование обеспечивает эффективный способ создания
новой версии данных. Next Library рекурсивно состоит из узлов, которые используют части Library, которые являются общими для двух версий
Джо: Неизменяемая функция создает новую хеш-карту Library, которая рекурсивно
использует части текущей версии Library, которые являются общими для двух
версий, вместо их глубокого копирования.
Тео: Для меня это немного абстрактно.
Джо: Следующая версия Library использует ту же хеш-карту UserManagament, что и
старая версия. Catalog внутри следующей Library использует тот же индекс
authorsById, что и текущий Catalog. Запись Book для «Хранителей» в следующей
версии Catalog использует все поля текущей записи Book, за исключением поля
publicationYear.
Тео: Итак, на самом деле большая часть данных является общей для двух версий.
Верно?
Джо: Абсолютно! Вот почему этот прием называется структурным совместным
использованием.
СОВЕТ. Структурное совместное использование обеспечивает эффективный способ
(как с точки зрения памяти, так и с точки зрения вычислений) создания новой версии данных
путем рекурсивного совместного использования частей, которым не нужно изменяться.
Тео: Очень круто!
Джо: И в самом деле. Теперь давайте посмотрим, как написать изменение для добавления читателя библиотеки, используя неизменяемые функции.

Глава 4. Управление состоянием
109
Джо снова подходит к доске. На рис. 4.5 показана диаграмма, которую Джо рисует, чтобы проиллюстрировать, как выглядит структурное совместное использование
при добавлении нового читателя библиотеки.
Рис. 4.5. Добавление читателя библиотеки при помощи структурного совместного использования.
Большая часть данных является общей для двух версий
Тео: Потрясающе! Хеш-карты Catalog и librarians копировать не обязательно!
Джо: Теперь с точки зрения кода мы должны написать функцию Library.addMember, которая делегирует задачу UserManagement.addMember.
Тео: Предполагаю, это будет похоже на код, который мы писали ранее для реализации поискового запроса на книги, где Library.searchBooksByTitleJSON делегирует задачу Catalog.searchBooksByTitle.
Джо: Будет похоже в том смысле, что все функции статичны и получают данные, которыми они манипулируют, в качестве аргумента. Но есть два отличия.
Во-первых, изменение может завершиться неудачей, например, если добавляе-мый элемент уже существует. Во-вторых, код для Library.addMember немного
более сложный, чем код для Library.searchBooksByTitleJSON, потому что мы
должны создать новую версию Library, которая ссылается на новую версию
UserManagement. Давайте я покажу вам пример.
Листинг 4.3. Код для изменения, добавляющего читателя библиотеки
UserManagement.addMember = function(userManagement, member) {
var email = _.get(member, "email");
var infoPath = ["membersByEmail", email];
if(_.has(userManagement, infoPath)) { ❶
throw "Member already exists.";
}
var nextUserManagement = _.set(
userManagement, ❷
110
Часть 1. Гибкость
infoPath,
member);
return nextUserManagement;
};
Library.addMember = function(library, member) {
var currentUserManagement = _.get(library, "userManagement"); var nextUserManagement = UserManagement.addMember(
currentUserManagement,
member);
var nextLibrary = _.set(library,
"userManagement",
nextUserManagement); ❸
return nextLibrary;
};
❶ Проверяет, имеется ли уже существующий читатель с таким адресом электронной почты.
❷ Создает новую версию UserManagement, которая включает в себя нового читателя.
❸ Создает новую версию библиотеки, содержащую новую версию UserManagement.
Тео: Для меня немного странно, что неизменяемые функции возвращают обнов-ленную версию данных вместо того, чтобы изменять их на месте.
Джо: Для меня это тоже было странно, когда я впервые столкнулся с неизменяемыми данными в Clojure семь лет назад.
Тео: Сколько времени вам потребовалось, чтобы привыкнуть к этому?
Джо: Пара недель.
4.3. Реализация
структурного совместного использования
Когда Джо уходит, возле кофемашины Тео встречает Дейва. Дейв выглядит озада-ченным.
Дейв: Что за парень только что вышел из офиса?
Тео: Это Джо. Мой наставник по ДОП.
Дейв: А что такое ДОП?
Тео: ДОП — это программирование, ориентированное на данные.
Дейв: Никогда раньше не слышал такого термина.
Тео: Это довольно мощная парадигма программирования, но программисты о ней
пока что мало знают. Насколько я понял, ДОП значительно упрощает программирование.
Дейв: А ты можешь привести мне пример?
Тео: Я только что узнал о структурном совместном использовании и о том, как оно
позволяет эффективно создавать новые версии данных без копирования.
Дейв: Как это работает?


Глава 4. Управление состоянием
111
Тео ведет Дейва в свой офис и показывает ему схему, оставленную Джо на доске
(рис. 4.6). В течение нескольких минут Тео объясняет Дейву, что именно происходит на схеме, и в конце концов Дейв вник в суть.
Дейв: Как реализуется структурное совместное использование?
Тео: Я не знаю. Я использовал функцию _.set из Lodash.
Дейв: Звучит как интересный вызов.
Тео: Принимай его, если хочешь. А я слишком устал, чтобы заниматься этими ре-курсивными алгоритмическими штучками.
Рис. 4.6. Структурное совместное использование в действии
На следующий день Тео задерживается у офисной кабинки Дейва, прежде чем отправиться к себе в офис. Дейв с гордостью показывает Тео свою реализацию структурного совместного использования. Тео поражает то, что это всего лишь 11 строк
кода JavaScript!
Листинг 4.4. Реализация структурного совместного использования
function setImmutable(map, path, v) {
var modifiedNode = v;
var k = path[0];
var restOfPath = path.slice(1);
if (restOfPath.length > 0) {
modifiedNode = setImmutable(map[k], restOfPath, v);
}
112
Часть 1. Гибкость
var res = Object.assign({}, map); ❶
res[k] = modifiedNode;
return res;
}
❶ Поверхностно копирует карту в JavaScript.
Тео: Дейв, да это же гениально!
Дейв (улыбаясь): Да ладно тебе.
Тео: Так, мне нужно идти. Я опаздываю на встречу с Джо! Он, наверное, уже сидит
в моем кабинете и ногти грызет от нетерпения.
4.4. Безопасность данных
Джо готов начать дневной урок. Но сперва Тео задает ему вопрос по вчерашнему
материалу.
Тео: Мне кое-что непонятно относительно этого самого структурного совместного
использования. Что произойдет, если мы напишем код, который изменяет часть
данных, которая является общей для двух версий данных? Повлияет ли это изменение на обе версии?
Джо: Не могли бы вы, пожалуйста, написать фрагмент кода, который иллюстрирует ваш вопрос?
Тео начинает печатать на своем ноутбуке. Он придумывает такой код, который
проиллюстрирует изменение части данных, совместно используемых двумя версиями.
Листинг 4.5. Изменение данных, совместно используемых двумя версиями
var books = {
"978-1779501127": {
"isbn": "978-1779501127",
"title": "Watchmen",
"publicationYear": 1987,
"authorIds": ["alan-moore",
"dave-gibbons"]
}
};
var nextBooks = _.set(books, ["978-1779501127", "publicationYear"], 1986) console.log("Before:", nextBooks["978-1779501127"]["authorIds"][1]); books["978-1779501127"]["authorIds"][1] = "dave-chester-gibbons"; console.log("After:", nextBooks["978-1779501127"]["authorIds"][1]);
// → Before: dave-gibbons
// → After: dave-chester-gibbons

Глава 4. Управление состоянием
113
Тео: Мой вопрос: каково значение isBlocked в updatedMember?
Джо: Ответ заключается в том, что изменение данных с помощью нативного уста-новщика хеш-карты запрещено. Все манипуляции с данными должны выполняться с помощью неизменяемых функций.
ПРИМЕЧАНИЕ. Все манипуляции с данными должны выполняться с помощью неизменяемых функций. Запрещено использовать нативный установщик хеш-карты.
Тео: Когда вы говорите «запрещено», вы имеете в виду, что разработчик должен
сам убедиться, что этого не произойдет. Верно?
Джо: Именно.
Тео: Есть ли способ защитить систему от ошибки разработчика?
Джо: Да, есть способ обеспечить неизменяемость данных на уровне структуры
данных. Это называется персистентными структурами данных.
Тео: Являются ли персистентные структуры данных также эффективными с точки
зрения памяти и вычислений?
Джо: На самом деле то, как данные организованы внутри персистентных структур
данных, делает их даже более эффективными, чем неизменяемые функции.
СОВЕТ. Персистентные структуры данных неизменяемы на уровне данных. Их невозможно изменить даже по ошибке.
Тео: Существуют ли библиотеки, предоставляющие персистентные структуры данных?
Джо: Определенно. У меня на компьютере как раз имеется список этих библиотек.
Джо, человек вполне организованный (для программиста), быстро находит этот
список. Он показывает его Тео:
Immutable.js в JavaScript на https://immutable-js.com/;
Paguro в Java на https://github.com/GlenKPeterson/Paguro;
Immutable Collections в C# на http://mng.bz/y4Ke;
Pyrsistent в Python на https://github.com/tobgu/pyrsistent;
Hamster in Ruby на https://github.com/hamstergem/hamster.
Тео: Почему бы не использовать персистентные структуры данных вместо неизменяемых функций?
Джо: Недостаток персистентных структур данных заключается в том, что они не
являются нативными. Это означает, что для работы с ними требуется преобразование из нативного в персистентное и из персистентного в нативное.
Тео: Какой подход вы бы порекомендовали?
Джо: Если хотите побаловаться, начинайте с неизменяемых функций. Но для рабо-чего приложения я бы рекомендовал использовать персистентные структуры
данных.

114
Часть 1. Гибкость
Тео: Жаль, что нативные структуры данных не являются персистентными!
Джо: Это одна из причин, по которой я люблю Clojure: нативные структуры данных в этом языке неизменяемы!
4.5. Фиксационный этап изменения
Итак, мы рассмотрели реализацию вычислительного этапа изменения. Этап вычисления не имеет состояния в том смысле, что в ходе него не вносится никаких изменений в систему. Теперь давайте посмотрим, как обновить состояние системы
внутри этапа фиксации.
Тео еще раз смотрит на код для Library.addMember. Его кое-что беспокоит: эта функция возвращает новое состояние библиотеки, содержащее дополнительного читателя, но это не влияет на текущее состояние библиотеки.
Листинг 4.6. Этап фиксации перемещает состояние системы вперед
Library.addMember = function(library, member) {
var currentUserManagement = _.get(library, "userManagement"); var nextUserManagement = UserManagement.addMember(
currentUserManagement,
member);
var nextLibrary = _.set(library, "userManagement", nextUserManagement); return nextLibrary;
};
Тео: Как я вижу, Library.addMember не изменяет состояние библиотеки. Как же обновляется состояние библиотеки?
Джо: Это очень хороший вопрос. Library.addMember имеет дело только с вычислением данных и не имеет состояния. Состояние обновляется на этапе фиксации путем перемещения вперед версии состояния, на которую ссылается состояние
системы.
Тео: Что вы имеете в виду?
Джо: Вот что происходит, когда мы добавляем в систему читателя. На этапе
вычисления создается версия состояния, состоящая из двух читателей. Перед
этапом фиксации состояние системы ссылается на версию состояния с одним
элементом. На этапе фиксации происходит перемещение состояния системы
вперед так, чтобы оно ссылалось на версию состояния с двумя элементами.
СОВЕТ. На этапе фиксации происходит перемещение состояния системы вперед к версии состояния, возвращенной этапом вычисления.
Джо рисует на доске еще одну иллюстрацию (рис. 4.7). Он надеется, что это поможет прояснить любые недоразумения, которые могут возникнуть у Тео.

Глава 4. Управление состоянием
115
Рис. 4.7. Этап фиксации перемещает состояние системы вперед
Тео: А как это реализуется?
Джо: Код состоит из двух классов: System, одноэлементного класса с сохранением
состояния, который реализует изменения, и SystemState, одноэлементного класса
с сохранением состояния, который управляет состоянием системы.
Тео: Для меня это звучит как классическое ООП.
Джо: Верно, и то, что эта часть системы сохраняет состояние, тоже напоминает
ООП.
Тео: Рад видеть, что вы все еще видите какую-никакую пользу в ООП.
Джо: Медитации научили меня тому, что каждая часть нашей вселенной играет
свою роль.
Тео: Неплохо! Не могли бы вы показать мне какой-нибудь код?
Джо: Конечно.
Джо на мгновение задумывается, прежде чем начать печатать. Он хочет показать
класс System и его реализацию изменения addMember.
Листинг 4.7. Класс System
class System {
addMember(member) {
var previous = SystemState.get();
var next = Library.addMember(previous, member);
SystemState.commit(previous, next); ❶
}
}
❶ Класс SystemState разобран в листинге 4.8.
Тео: Как выглядит SystemState?
Джо: У меня было предчувствие, что вы об этом спросите. Вот код для класса
SystemState, который является классом с сохранением состояния!

116
Часть 1. Гибкость
Листинг 4.8. Класс SystemState
class SystemState {
systemState;
get() {
return this.systemState;
}
commit(previous, next) {
this.systemState = next;
}
}
Тео: Я не понимаю, в чем смысл SystemState. Это простой класс с геттером и функцией фиксации, верно?
Джо: Через мгновение мы обогатим код метода SystemState.commit так, чтобы он
обеспечивал валидацию данных и отслеживание истории. А прямо сейчас важно
отметить, что код этапа вычисления не имеет состояния и отделен от кода этапа
фиксации, который сохраняет состояние.
СОВЕТ. Этап вычисления не имеет состояния. Этап фиксации сохраняет состояние.
4.6. Обеспечение
целостности состояния системы
Тео: Меня все еще беспокоит то, что функции манипулируют неизменяемыми данными на этапе вычисления. Как нам сохранить целостность данных?
Джо: Что вы имеете в виду?
Тео: В ООП данными манипулируют только методы, принадлежащие к тому же
классу, что и сами данные. Это предотвращает повреждение внутреннего состояния класса другими классами.
Джо: Не могли бы вы привести мне пример невалидного состояния библиотеки?
Тео: Представьте, что код изменения добавляет запись в список взятых читателем
книг, не помечая эту книгу как взятую для чтения в каталоге. Тогда системные
данные будут повреждены.
Джо: В ДОП мы спокойно можем обеспечивать целостность данных на уровне всей
системы вместо того, чтобы распределять валидацию между многими классами.
Тео: Как это работает?
Джо: Тот факт, что код для этапа фиксации является общим для всех изменений, позволяет нам валидировать системные данные в одном централизованном месте. В начале этапа фиксации выполняется шаг, который проверяет, является ли
версия состояния системы, подлежащая фиксации, валидной. Если данные невалидны, фиксация отклоняется. Давайте я вам покажу.


Глава 4. Управление состоянием
117
Листинг 4.9. Валидация данных на этапе фиксации
SystemState.commit = function(previous, next) {
if(!SystemValidity.validate(previous, next)) { // not implemented for now throw "The system data to be committed is not valid!";
};
this.systemData = next;
};
Тео: Это похоже на хук фиксации в Git.
Джо: Мне нравится ваша аналогия!
Тео: Почему вы передаете предыдущее состояние из previous и следующее состояние из next в SystemValidity.validate?
Джо: Потому что это позволяет SystemValidity.validate оптимизировать валидацию
с точки зрения вычислений. Например, мы можем валидировать только те данные, которые изменились.
СОВЕТ. В ДОП мы валидируем системные данные в целом. Валидация данных отделена от манипулирования данными.
Тео: Как выглядит код SystemValidity.validate?
Джо: Когда-нибудь я покажу вам, как определить схему данных и проверить, соответствует ли фрагмент данных схеме.
ПРИМЕЧАНИЕ. Смотрите главы 7 и 12, чтобы увидеть, как именно Джо определяет схему
данных.
4.7. Восстановление предыдущих состояний
Еще одно преимущество мультиверсионного подхода с неизменяемыми данными, которыми манипулируют с помощью структурного совместного использования, заключается в том, что мы можем отслеживать историю всех версий данных, не
перегружая память нашей программы. Это позволяет нам, например, легко восстановить систему до более раннего состояния.
Тео: Вы упоминали, что можно легко восстановить систему до предыдущего состояния. Не могли бы вы показать мне, как это делается?
Джо: С радостью, но перед этим я хочу убедиться, что вы понимаете, почему отслеживание всех версий данных эффективно с точки зрения памяти.
Тео: Я думаю, это связано с тем фактом, что неизменяемые функции используют
структурное совместное использование и большая часть данных между последующими версиями состояния является общей.
СОВЕТ. Структурное совместное использование позволяет сохранять множество версий
состояния системы без чрезмерного использования памяти.


118
Часть 1. Гибкость
Джо: Превосходно! Теперь я покажу вам, как просто можно отменить изменение.
Чтобы реализовать механизм отмены, наш класс SystemState должен иметь две
ссылки на системные данные: systemData ссылается на текущее состояние системы, а previousSystemData ссылается на предыдущее состояние системы.
Тео: В этом есть смысл.
Джо: На этапе фиксации мы обновляем как previousSystemData, так и systemData.
Тео: Что именно нужно, чтобы реализовать механизм отмены?
Джо: Отмена достигается за счет того, что systemData ссылается на ту же версию
системных данных, что и previousSystemData.
Тео: Не могли бы вы показать это на примере?
Джо: Чтобы упростить задачу, я присвою каждой версии состояния системы свой
номер. Нумерация будет начинаться с V0, и каждый раз, когда фиксируется изменение, версия приращивается: V1, V2, V3 и т. д.
Тео: ОК.
Джо: Предположим, что в настоящее время наше системное состояние — номер V12 (рис. 4.8). В объекте SystemState systemData ссылается на V12, а
previousSystemData ссылается на V11.
Рис. 4.8. Когда состояние системы пронумеровано как V12, systemData ссылается на V12, а previousSystemData ссылается на V11
Тео: Пока все ясно...
Джо: А теперь, когда фиксируется изменение (например, добавляется элемент), обе
ссылки перемещаются вперед: systemData ссылается на V13, а previousSystemData ссылается на V12.
Джо стирает с доски, чтобы освободить место для другой диаграммы (рис. 4.9). Когда он заканчивает новый рисунок, он показывает его Тео.
Рис. 4.9. Когда фиксируется изменение, systemData ссылается на V13, а previousSystemData ссылается на V12

Глава 4. Управление состоянием
119
Тео: Предполагаю, что, когда мы отменяем изменение, обе ссылки перемещаются
назад.
Джо: Теоретически да, но на практике необходимо поддерживать стек всех ссылок
на состояние. На данный момент, чтобы упростить ситуацию, мы сохраним
только ссылку на предыдущую версию. Как следствие, когда мы отменяем изменение, обе ссылки указывают на V12. Сейчас я нарисую еще одну диаграмму, которая показывает это состояние (рис. 4.10).
Рис. 4.10. Когда изменение отменяется, как systemData,
так и previousSystemData ссылаются на версию 12
Тео: Не могли бы вы показать, как реализовать этот механизм отмены?
Джо: На самом деле для этого требуется всего пара изменений в классе SystemState.
Обратите внимание на изменения в функции commit. Внутри
systemDataBeforeUpdate мы сохраняем ссылку на текущее состояние системы. Ес-ли валидация и разрешение конфликта завершаются успешно, мы обновляем как
previousSystemData, так и systemData.
Листинг 4.10. Класс SystemState с возможностью отмены
class SystemState {
systemData;
previousSystemData;
get() {
return this.systemData;
}
commit(previous, next) {
var systemDataBeforeUpdate = this.systemData;
if(!Consistency.validate(previous, next)) {
throw "The system data to be committed is not valid!";
}
this.systemData = next;
this.previousSystemData = systemDataBeforeUpdate;
}
undoLastMutation() {
this.systemData = this.previousSystemData;
}
}
120
Часть 1. Гибкость
Тео: Как я понял, реализация System.undoLastMutation — это всего лишь вопрос
того, чтобы systemData ссылался на то же значение, что и previousSystemData.
Джо: Как я уже упоминал, если нам нужно провести несколько отмен, код будет
немного сложнее, но идею вы поняли.
Тео: Кажется, понял. Фильм «Назад в будущее» относится к области научной фан-тастики, а вот в ДОП путешествия во времени реальны.
Итоги
Принцип ДОП № 3 гласит, что данные неизменяемы.
Изменение — это операция, которая изменяет состояние системы.
При мультиверсионном подходе к управлению состоянием мутации подразде-ляются на этап вычисления и этап фиксации.
Все манипуляции с данными должны выполняться с помощью неизменяемых
функций. Запрещено использовать нативный установщик хеш-карты.
Структурное совместное использование позволяет эффективно создавать новые
версии данных (с точки зрения памяти и вычислений), где данные, которые
являются общими для двух версий, используются совместно, а не копируются.
При применении структурного совместного использования создается новая версия данных путем рекурсивного совместного использования частей, которым не
нужно изменяться.
Изменение происходит в два этапа: вычисление и фиксация.
Функция считается неизменяемой, когда вместо изменения данных она создает
новую версию данных без изменения тех, которые она получает.
На этапе вычисления данные обрабатываются с помощью неизменяемых функций, которые используют структурное совместное использование.
Этап вычисления не имеет состояния.
На этапе фиксации обновляется состояние системы.
На этапе фиксации происходит перемещение состояния системы вперед к версии состояния, возвращаемой фазой вычисления.
Данные неизменяемы, но ссылка на состояние изменяется.
Этап фиксации сохраняет состояние.
Системные данные валидируются в целом. Валидация данных отделена от манипуляции данными.
Тот факт, что код для этапа фиксации является общим для всех изменений, позволяет валидировать состояние системы в одном централизованном месте, прежде чем обновится состояние системы.
Глава 4. Управление состоянием
121
Хранение истории версий системных данных экономит память благодаря
структурному совместному использованию.
Восстановить систему до одного из ее предыдущих состояний легко благодаря
четкому разделению между этапом вычисления и этапом фиксации.
Для того чтобы использовать неизменяемые функции библиотеки Lodash, необходимо использовать модуль Lodash FP (https://github.com/lodash/lodash/wiki/
FP-Guide).
Функции Lodash, представленные в этой главе
Функция Описание
set(map, path, value)
Создает карту с теми же полями, что и map, с добавлением поля
<path, value>
122
Часть 1. Гибкость
Основы контроля конкурентности
Семейные конфликты
В ЭТОЙ ГЛАВЕ РАССМАТРИВАЮТСЯ
Управление конкурентными изменениями с помощью оптимистич-
ной стратегии управления конкурентностью без блокировок.
Поддержка высокой пропускной способности операций чтения
и записи.
Согласование конкурентных изменений.
Изменения, необходимые для управления конкурентностью в системе, происходят
только на этапе фиксации. Они включают в себя алгоритм согласования, который
является универсальным в том смысле, что его можно использовать в любой системе, где данные представлены в виде неизменяемой хеш-карты. Реализация алгоритма согласования эффективна, поскольку последующие версии состояния системы
создаются с помощью структурного совместного использования.
В предыдущей главе мы проиллюстрировали мультиверсионный подход к управлению состоянием, при котором изменение состояния подразделяется на два отдельных этапа. Первый — этап вычисления, который касается только вычислений. Второй — этап фиксации, на котором ссылка на состояние продвигается вперед.
Обычно в системе во время эксплуатации такие изменения происходят конкурентно. Просто взять и продвинуть состояние вперед, как мы это делали в предыдущей
главе, неуместно. В настоящей главе мы узнаем, как обрабатывать конкурентные
изменения.
В ДОП, поскольку только код этапа фиксации отслеживает состояние, мы можем
использовать оптимистичную стратегию контроля конкурентности, которая не
включает в себя механизмы блокировки. Как следствие, пропускная способность
операций чтения и записи высока. Изменения в коде довольно значительны, поскольку мы должны реализовать алгоритм, который согласовывает конкурентные
изменения. Но эти изменения влияют только на этап фиксации. Код для этапа
вычисления остается тем же, что и в предыдущей главе.

124
Часть 1. Гибкость
ПРИМЕЧАНИЕ. Чтобы понять эту главу, потребуется больше усилий. Блок-схема алгоритма согласования непроста, а реализация включает в себя сложную рекурсию.
5.1. Оптимистичный контроль конкурентности
Этим утром, прежде чем приступить к работе, Тео приглашает Джо в фитнес-зал
в офисе, и во время бега на степ-тренажере двое мужчин снова говорят о своей
личной жизни. Джо рассказывает о ссоре, которая произошла у него вчера вечером
с Кей, которая думает, что он уделяет больше внимания своей работе, чем своей
семье. Тео рассказывает о болезненном конфликте, который у него был с Джейн, его женой, по поводу управления домашним бюджетом. Они были на приеме у семейного психотерапевта, специалиста по имаготерапии. Имаготерапия позволила
им превратить свой конфликт в возможность расти и исцеляться.
Джо навострил уши, когда услышал слово «конфликт», потому что сегодняшний
урок будет посвящен разрешению конфликтов и конкурентных изменений. Однако
это конфликты другого рода... После душа и полезного завтрака Тео и Джо присту-пают к работе.
Джо: Вчера я показал вам, как управлять состоянием с помощью неизменяемых
данных, предполагая, что изменения не происходят конкурентно. Сегодня я покажу вам, как в ДОП контролировать конкурентность.
Тео: Мне любопытно узнать, какие механизмы блокировки вы используете в ДОП
для синхронизации конкурентных изменений.
Джо: На самом деле мы не используем механизмы блокировки!
Тео: А почему?
Джо: Блокировки вредят производительности, и если вы не будете осторожны, в системе произойдет взаимоблокировка.
Тео: Итак, как вы справляетесь с возможными конфликтами между конкурентными
изменениями в ДОП?
Джо: В ДОП мы используем стратегию без блокировок, называемую оптимистичным контролем конкурентности. Это стратегия, которая позволяет таким базам
данных, как Elasticsearch, быть высокомасштабируемыми.
ПРИМЕЧАНИЕ. Посетите https://www.elastic.co/elasticsearch/, чтобы узнать больше об
Elasticsearch.
Тео: Вы сейчас похожи на моего семейного психотерапевта, когда она говорит
о своей оптимистичной стратегии разрешения конфликтов без гнева.
Джо: Оптимистичный контроль конкурентности и ДОП хорошо работают вместе.
Как вы сейчас увидите, оптимистичный контроль конкурентности очень эффективен, когда системные данные неизменяемы.
СОВЕТ. Оптимистичный контроль конкурентности с неизменяемыми данными очень
эффективен.



Глава 5. Основы контроля конкурентности
125
Тео: Как это работает?
Джо: Оптимистичный контроль конкурентности происходит, когда мы позволяем
при изменениях «просить прощения вместо разрешения».
СОВЕТ. Оптимистичный контроль конкурентности происходит, когда мы позволяем при
изменениях просить прощения вместо разрешения.
Тео: Что вы имеете в виду?
Джо: Этап вычисления выполняет вычисления так, как если бы это было единственное запущенное изменение. Этап фиксации отвечает за согласование конкурентных изменений, когда они не конфликтуют, или за прерывание изменения.
СОВЕТ. Этап вычисления выполняет вычисления так, как если бы кроме них ничего
не менялось. Этап фиксации отвечает за согласование конкурентных изменений, когда они
не конфликтуют, или за прерывание изменения.
Тео: Похоже, что это будет сложно реализовать.
Джо: Работа с государством никогда не бывает простой. Но хорошая новость заключается в том, что код для логики согласования на этапе фиксации является
универсальным.
Тео: Означает ли это, что один и тот же код для этапа фиксации может быть использован в любой системе ДОП?
Джо: Определенно. Код, реализующий этап фиксации, ничего не предполагает
о деталях системы, за исключением того, что системные данные представлены
в виде неизменяемой карты.
СОВЕТ. Реализация этапа фиксации в рамках оптимистичного контроля конкурентности
является универсальной. Ее можно использовать в любой системе, где данные представлены неизменяемой хеш-картой.
Тео: Это потрясающе!
Джо: Еще одна интересная вещь заключается в том, что обработка конкурентности
не требует каких-либо изменений в коде на этапе вычисления. С точки зрения
этапа вычисления следующая версия системных данных вычисляется изолированно, как если бы никакие другие изменения не выполнялись конкурентно.
Джо встает, чтобы проиллюстрировать свою мысль на доске. Пока Тео рассматривает диаграмму на рис. 5.1, Джо обобщает информацию, приведенную в табл. 5.1.
Таблица 5.1. Две фазы изменения при оптимистичном управлении параллелизмом
Этап Действие
Состояние
Реализация
Вычисление Вычисляется
следующее
состоя-
Без состояния
Частная
ние в изоляции
Фиксация
Согласовывается и обновляется
С сохранением состояния
Общая
состояние системы

126
Часть 1. Гибкость
Рис. 5.1. Блок-схема оптимистичного контроля конкурентности
5.2. Согласование
между конкурентными изменениями
Тео: Не могли бы вы привести примеры конфликтующих конкурентных изменений?
Джо: Конечно. Одним из примеров является попытка двух читателей библиотеки
позаимствовать один и тот же экземпляр книги. Другим примером может по-служить ситуация, когда два библиотекаря обновляют год издания одной и той
же книги.
Тео: Вы упомянули, что код для логики согласования на этапе фиксации является
универсальным. Что именно вы подразумеваете под логикой согласования?
Джо: Это очень похоже на то, что происходит в Git, когда мы производим слияние
ветки обратно с основной веткой.
Тео: Мне нравится, когда основная ветка остается прежней.
Джо: Да, приятно, когда слияние не имеет конфликтов и может выполняться автоматически. Вы помните, как Git обрабатывает слияние в этом случае?
Тео: Git выполняет быструю перемотку вперед; система обновляет основную ветку, чтобы она была такой же, как ветка слияния.
Джо: Верно! И что происходит, когда вы обнаруживаете, что тем временем другой
разработчик зафиксировал свой код в основной ветке?
Тео: Тогда Git выполняет трехстороннее слияние, пытаясь объединить все изменения из двух веток слияния с основной веткой.



Глава 5. Основы контроля конкурентности
127
Джо: Всегда ли все проходит гладко?
Тео: Обычно — да, но два разработчика могли изменить одну и ту же строку в одном
файле. Тогда придется разрешать конфликт вручную. Терпеть этого не могу!
СОВЕТ. Во время эксплуатации в системе несколько изменений выполняются конкурентно. Прежде чем обновлять состояние, нужно согласовать конфликты между возможными
конкурентными изменениями.
Джо: В ДОП алгоритм согласования на этапе фиксации очень похож на слияние
в Git, за исключением того, что вместо ручного разрешения конфликтов мы пре-рываем изменение. Существуют три возможности согласования возможных
конкурентных изменений: быстрая перемотка вперед, трехстороннее слияние и
прерывание.
Джо снова подходит к доске. Он рисует две диаграммы, показанные на рис. 5.2
и 5.3.
Рис. 5.2. Блок-схема процесса согласования
Рис. 5.3. Когда начинается этап фиксации, существуют три версии состояния системы
Тео: Не могли бы вы объяснить более подробно?
Джо: Когда начинается этап фиксации изменения, у нас есть три версии состояния
системы: previous, которая является версией, на которой этап вычисления осно-вывал свои вычисления; current, которая является текущей версией во время
этапа фиксации; и next, которая является версией, возвращенной этапом вычисления.


128
Часть 1. Гибкость
Тео: Почему current отличается от previous?
Джо: Это случается, когда другие изменения происходят одновременно с нашим
изменением.
Тео: Понятно.
Джо: Если мы находимся в ситуации, где текущее состояние совпадает с предыдущим состоянием, это означает, что никакие изменения не выполняются конкурентно. Поэтому, как и в Git, мы можем безопасно выполнить быструю перемотку вперед и обновить состояние системы при помощи следующей версии.
Тео: А что если состояние не осталось прежним?
Джо: Тогда это означает, что изменения выполнялись конкурентно. Мы должны
проверять наличие конфликтов способом, аналогичным трехстороннему слия-нию, используемому Git. Разница в том, что вместо сравнения строк мы сравниваем поля системной хеш-карты.
Тео: Не могли бы вы это объяснить?
Джо: Мы вычисляем разницу между previous и next, а также между previous и
current. Если у двух разниц нет общих полей, то нет и конфликта между изменениями, которые выполнялись одновременно. Мы можем безопасно применить
изменения от previous к next версии current.
Джо делает свое объяснение наглядным с помощью другой диаграммы на доске.
Затем он показывает рис. 5.4 Тео.
Рис. 5.4. При трехстороннем слиянии мы вычисляем разницу
между версиями previous и next и применяем ее к версии current
Тео: А что если возникнет конфликт?
Джо: Тогда мы прервем мутацию.
Тео: Прерывать запрос пользователя вроде как не полагается.
Джо: На самом деле в системе, ориентированной на пользователя, конфликтующие
конкурентные изменения довольно редки. Вот почему можно прервать действие
и позволить пользователю снова запустить изменение. Здесь позвольте мне набросать таблицу, чтобы показать вам различия между Git и ДОП (табл. 5.2).
Тео: Здорово! Это очень полезно, но в случаях, когда два изменения обновляют
одно и то же поле одного и того же объекта, я думаю, что следует сообщить
пользователю, что запрос не может быть обработан.
СОВЕТ. В системе, ориентированной на пользователя, конфликтующие конкурентные
изменения встречаются довольно редко.
Глава 5. Основы контроля конкурентности
129
Таблица 5.2. Аналогия между Git и дата-ориентированным программированием
Дата-ориентированное программирование
Git
Конкурентные изменения
Разные ветки
Версия системных данных
Операция фиксации
Состояние Ссылка
Этап вычисления
Ветвление
Валидация
Хук предварительной фиксации
Согласование Слияние
Быстрая перемотка вперед
Быстрая перемотка вперед
Трехстороннее слияние
Трехстороннее слияние
Прерывание
Разрешение конфликта вручную
Хеш-карта Дерево
(папка)
Листовой узел
Блоб (файл)
Поле данных
Строка кода
5.3. Сокращение коллекций
Джо: Готовы ли вы бросить вызов своему разуму с помощью реализации алгоритма
нахождения разницы?
Тео: Давайте сделаем небольшой перерыв на кофе, если вы не возражаете. Тогда я
буду готов взяться за что угодно.
Насладившись большой кружкой горячего кофе и парой сдобных печений, Тео и
Джо возвращаются к работе. Обсуждение алгоритма нахождения разницы продолжается.
Джо: В реализации алгоритма нахождения разницы мы будем сокращать коллекции.
Тео: Я слышал о сокращении коллекций в разговоре о FP, но я не помню подроб-ностей. Не могли бы вы напомнить мне, как это работает?
Джо: Представьте, что вы хотите вычислить сумму элементов в коллекции чисел.
При использовании метода _.reduce из Lodash это будет выглядеть так.
Листинг 5.1. Суммирование чисел с помощью _.reduce
_.reduce([1, 2, 3], function(res, elem) {
return res + elem;
}, 0);
// → 6

130
Часть 1. Гибкость
Тео: Я не понимаю.
Джо подходит к доске и набрасывает описание метода _.reduce. Тео терпеливо
ждет, пока Джо отложит маркер, прежде чем взглянуть на получившееся.
МЕТОД _.REDUCE
Метод _.reduce принимает три аргумента:
coll — коллекция элементов;
f — функция, которая принимает два аргумента;
initVal — некоторое значение.
Логическая схема:
1. Инициализировать currentRes с initVal.
2. Для каждого элемента х коллекции coll обновить currentRes при помощи f (currentRes, х).
3. Вернуть currentRes.
Тео: Вы не будете возражать, если я вручную расширю логическую схему вашего
кода для _.reduce?
Джо: Я думаю, это отличная идея!
Тео: В нашем случае значение initVal равно 0. Это означает, что первым вызовом f будет f(0, 1). Затем у нас будет f(f(0, 1), 2) и, наконец, f(f(f(0, 1), 2), 3).
Джо: Мне нравится это ручное расширение, Тео! Давайте его визуализируем.
Теперь к доске подходит Тео и рисует диаграмму. На рис. 5.5 показано, как она
выглядит.
Рис. 5.5. Визуализация
метода _.reduce
Тео: Теперь все намного яснее. Я думаю, что после реализации моей собственной
пользовательской версии _.reduce ясность достигнет ста процентов.
Работа над реализацией reduce() занимает у Тео гораздо меньше времени, чем он
ожидал. В мгновение ока он показывает Джо код.
Глава 5. Основы контроля конкурентности
131