Книга: Дата-ориентированное программирование: Пер. с англ.
Назад: Итоги
На главную: Предисловие

var data = new AuthorData("Isaac", "Asimov");

NameCalculation.fullName(data) === "Isaac Asimov"

// → true

СОВЕТ. Писать тесты легче, когда код отделен от данных.

406

Приложение А

Преимущество № 3: системы, как правило, менее сложны

Третье преимущество применения принципа № 1 к нашим программам заключается

в том, что системы, как правило, получаются менее сложными. Это преимущество

является самым глубоким, а также наименее простым для понимания.

Та сложность, о которой я говорю, относится к типу сложности, затрудняющему

понимание систем, как определено в статье Бена Мозли и Питера Маркса «Из ямы

со смолой» («Out of the Tar Pit», http://mng.bz/enzq). Этот тип не имеет ничего

общего со сложностью ресурсов, потребляемых программой. А также, упоминая

слово «простота», я имею в виду «отсутствие сложности» (другими словами, легкость понимания).

ПРИМЕЧАНИЕ. «Сложное» в контексте этой книги означает «трудное для понимания».

Имейте в виду, что сложность и простота (как и трудность и легкость) — понятия

не абсолютные, а относительные. Сложность двух систем можно сравнить, чтобы

определить, является ли система A более сложной (или более простой), чем система B. Когда код и данные хранятся отдельно друг от друга, систему, как правило, легче понять по двум причинам:

область действия объекта данных или объекта кода меньше, чем область действия объекта, объединяющего код и данные. Таким образом, отдельные объекты

легче понять;

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

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

на рис. А.2 представляет собой сложную систему; сложную в том смысле, что ее

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

Наиболее сложным объектом системы на рис. А.2 является Librarian, который связан с другими объектами посредством шести отношений. Некоторые отношения

являются отношениями данных (ассоциация и композиция), а другие являются отношениями кода (наследование и зависимость). Но при таком дизайне объект

Librarian смешивает код с данными, и, следовательно, он должен быть вовлечен

как в отношения данных, так и в отношения кода. Если каждый объект этой системы разделить на объект кода и объект данных без внесения в систему каких-либо

дополнительных модификаций, в результате (рис. А.3) получатся две несвязанные

части:

левая часть, состоящая только из объектов данных и отношений данных — ассоциации и композиции;

правая часть, состоящая только из объектов кода и кодовых отношений — зависимости и наследования.

Принципы дата-ориентированного программирования

407

Рис. A.2. Обзор диаграммы классов для Системы управления библиотекой

Рис. A.3. Диаграмма классов,

в которой каждый класс разделен на объекты кода и объекты данных

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

по отдельности.

СОВЕТ. Система, состоящая из разъединенных частей, менее сложна, чем система, представляющая собой единое целое.

408

Приложение А

Можно, конечно, возразить, что сложность исходной системы, где код и данные

смешаны, обусловлена плохим дизайном и что опытный разработчик ООП разработал бы более простую систему, используя хитроумные шаблоны проектирования.

Это верно, но в известном смысле не имеет значения. Суть принципа № 1 заключается в том, что система, состоящая из объектов, которые не объединяют в себе код

и данные, как правило, проще, чем система, состоящая из объектов, которые со-вмещают код и данные.

Я не раз говорил, что простота — это трудно. Согласно первому принципу ДОП, простоты легче достичь, разделив код и данные.

СОВЕТ. Простоты легче достичь, когда код отделен от данных.

A.1.3. Издержки принципа № 1

В этом разделе рассматриваются издержки реализации принципа № 1. Мы платим

тройную цену за то, чтобы извлечь выгоду из отделения кода от данных.

Отсутствует контроль над тем, какой код может получить доступ к каким данным.

Отсутствует упаковка.

Системы состоят из большего количества объектов.

Издержка № 1: отсутствие контроля над тем,

какой код к каким данным может получить доступ

Когда код и данные смешаны, легко понять, какие фрагменты кода могут получить

доступ к каким видам данных. Например, в ООП данные инкапсулированы в объекте, что гарантирует доступ к данным только с помощью методов этого объекта.

В ДОП данные существуют сами по себе. Можно сказать, что данные прозрачны, и, как следствие, к ним можно получить доступ с помощью любого фрагмента кода.

При рефакторинге формы каких-либо данных нужно знать о каждом месте, из которого код обращается к этим данным. Более того, без применения принципа № 3

(обеспечение неизменяемости данных), который мы обсудим позже, получение

доступа к данным с помощью любого фрагмента кода по определению небезопас-но. В этом случае трудно гарантировать валидность данных.

СОВЕТ. За безопасность данных отвечает другой принцип (принцип № 3), который

обеспечивает неизменяемость данных.

Издержка № 2: отсутствие пакетирования

Одно из преимуществ смешивания кода и данных заключается в том, что готовый

объект является также пакетом, содержащим как код (через методы), так и данные

(через элементы класса). Как следствие, легко обнаружить, как манипулировать

данными: нужно посмотреть на методы класса.

Принципы дата-ориентированного программирования

409

В ДОП код, который манипулирует данными, может находиться где угодно. Например, createAuthorData может находиться в одном файле, а fullName — в другом.

Поэтому разработчикам труднее обнаружить, что функция fullName доступна.

В некоторых ситуациях это может привести к напрасной трате времени и ненужно-му дублированию кода.

Издержка № 3:

системы состоят из большего количества объектов

Сейчас мы займемся простой арифметикой. Представьте себе систему, состоящую

из N классов, которые объединяют в себе код и данные. Когда мы разделяем систему на объекты кода и объекты данных, мы получаем систему, состоящую из

2N объектов. Однако этот расчет неверен, потому что обычно, когда мы разделяем

код и данные, иерархия классов имеет тенденцию упрощаться, поскольку нам требуется меньше наследования классов и композиции. Следовательно, количество

классов в получившейся системе, вероятно, будет где-то между N и 2N.

С одной стороны, при соблюдении принципа № 1 объекты системы становятся

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

благодаря принципу № 2, который позволяет представлять данные с помощью

обобщенных структур данных.

СОВЕТ. При соблюдении принципа № 1 системы состоят из более простых объектов, но

этих объектов больше.

A.1.4. Основная суть принципа № 1

В ДОП требуется отделять код от данных. В языках ООП код агрегируется в статических методах, а данные — в классах без методов. В языках ФП нужно избегать

сокрытия данных в лексической области действия функций.

Отделение кода от данных имеет свою цену. Сокращается возможность контроля

над тем, какие фрагменты кода получают доступ к данным, а системы могут содержать большее количество объектов. Но заплатить эту цену имеет смысл. Если

мы будем придерживаться этого принципа, наш код можно будет повторно использовать в различных контекстах непосредственным способом и тестировать изолированно. Более того, система, состоящая из отдельных объектов для кода и данных, как правило, более понятна.

ПРИНЦИП ДОП № 1: ОТДЕЛЯЙТЕ КОД ОТ ДАННЫХ

Следуя этому принципу, мы отделяем код от данных таким образом, чтобы код заключался в функциях, поведение которых не зависит от данных, инкапсулированных в контексте

функций. Диаграмма на рис. А.4 дает наглядное представление об этом.

Преимущества:

Возможность повторного использования кода в различных контекстах.

Возможность тестирования кода изолированно.

Системы, как правило, получаются менее сложными.

410

Приложение А

Рис. A.4. Принцип ДОП № 1: Отделяйте код от данных

Издержки реализации принципа № 1:

Отсутствие контроля над тем, какой код обращается к тем или иным данным.

Отсутствие пакетирования.

Большее количество объектов.

A.2. Принцип № 2: представляйте данные

с помощью обобщенных структур

Если соблюсти принцип № 1, код будет отделен от данных. ДОП не предписывает, какие программные конструкты следует использовать для организации кода, но

определяет то, как должны быть представлены данные. В рамках этой темы и существует принцип № 2.

Наиболее распространенными обобщенными структурами данных являются карты

(они же словари) и массивы (или списки). Можно использовать и другие обобщенные структуры данных (например, множества, деревья и очереди). Принцип № 2 не

имеет отношения к изменяемости или неизменяемости данных. В рамках этой темы

существует принцип № 3.

ПРИНЦИП № 2: представляйте данные приложения с помощью обобщенных структур данных.

A.2.1. Иллюстрация к принципу № 2

В ДОП мы представляем данные с помощью обобщенных структур данных (таких

как карты и массивы) вместо того, чтобы создавать экземпляры данных с помощью

определенных классов. На самом деле, большинство объектов данных, которые появляются в стандартном приложении, могут быть представлены с помощью карт и

массивов (или списков). Но существуют другие обобщенные структуры данных

(например, множества, списки, очереди и т. д.), которые могут потребоваться

в некоторых случаях. Давайте посмотрим на тот же простой пример, который мы

использовали для иллюстрации принципа № 1 (данные, представляющие автора

книги).

Автор — это объект данных с полями firstName, lastName и количеством написанных им книг books. Принцип № 2 нарушается, когда мы используем определенный

класс для представления автора, как показано в следующем листинге.

Принципы дата-ориентированного программирования

411

Листинг A.12. Нарушение принципа № 2 в ООП

class AuthorData {

constructor(firstName, lastName, books) {

this.firstName = firstName;

this.lastName = lastName;

this.books = books;

}

}

Принцип № 2 соблюдается при использовании карты (словаря или ассоциативного

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

Листинг A.13. Следование принципу № 2 в ООП

function createAuthorData(firstName, lastName, books) {

var data = new Map;

data.firstName = firstName;

data.lastName = lastName;

data.books = books;

return data;

}

В таком языке, как JavaScript, мы также можем создать экземпляр карты с помощью литерала данных, а это более удобно. В следующем листинге показан такой

пример.

Листинг A.14. Следование принципу № 2 с литералами карты

function createAuthorData(firstName, lastName, books) {

return {

firstName: firstName,

lastName: lastName,

books: books

};

}

A.2.2. Преимущества принципа № 2

Использование обобщенных структур данных для представления данных имеет

множество преимуществ. В следующих подразделах мы более подробно рассмотрим эти преимущества:

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

гибкая модель данных.

412

Приложение А

Применение функций, которые не ограничены

определенным случаем использования

Используя обобщенные структуры для представления данных, мы можем манипулировать данными с помощью широкого выбора функций, которые нативно доступны для этих структур в выбранном нами языке программирования. Кроме того, сторонние библиотеки предоставляют еще больше таких функций. Например, JavaScript нативно предоставляет некоторые базовые функции для карт и массивов, а сторонние библиотеки, такие как Lodash (https://lodash.com/), расширяют функциональность с помощью дополнительного набора функций. У Алана Перлиса есть

известная цитата, которая резюмирует это преимущество: «Лучше сто функций, работающих с одной структурой данных, чем десять функций, работающих с десятью структурами данных» (Перлис А. Эпиграммы о программировании, 1982).

Когда сведения об авторе книги представлены в виде карты, они могут быть сериа-лизованы в JSON с помощью функции JSON.stringify(), которая является частью

JavaScript. В следующем листинге приведен пример этого.

Листинг A.15. [#serialize-klipse-js],reftext="A.1"

var data = createAuthorData("Isaac", "Asimov", 500); JSON.stringify(data);

// → "{\"firstName\":\"Isaac\",\"lastName\":\"Asimov\",\"books\":500}"

Сериализация данных об авторе без учета количества книг может быть выполнена

с помощью функции _.pick() из библиотеки Lodash. В следующем листинге

_.pick() используется для создания объекта с подмножеством ключей.

Листинг A.16. Манипулирование данными с помощью обобщенных функций

var data = createAuthorData("Isaac", "Asimov", 500); var dataWithoutBooks = _.pick(data, ["firstName", "lastName"]); JSON.stringify(dataWithoutBooks);

// → "{\"firstName\":\"Isaac\",\"lastName\":\"Asimov\"}"

СОВЕТ. Когда мы придерживаемся принципа № 2, нам открывается широкий выбор

функциональных возможностей для манипулирования данными.

Гибкая модель данных

При использовании обобщенных структур модель данных является гибкой, и данные не принуждаются к определенной форме. Данные могут быть созданы без

какой-либо заранее заданной формы, и их форма может быть изменена по желанию.

В классическом ООП, когда не соблюдается принцип № 2, экземпляр каждого

фрагмента данных создается при помощи класса и должен иметь жесткую форму.

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

Принципы дата-ориентированного программирования

413

Возьмем, к примеру, AuthorData, класс, который представляет объект для автора

книги, состоящий из трех полей: firstName, lastName и books. Предположим, что вы

хотите добавить поле с названием fullName, содержащее полное имя автора. Если

мы не будем придерживаться принципа № 2, нам придется определить новый класс

AuthorDataWithFullName. Однако с использованием обобщенных структур данных

можно добавлять (или удалять) поля карты на лету, как показано в следующем листинге.

Листинг А.17. Добавление поля на лету

var data = createAuthorData("Isaac", "Asimov", 500); data.fullName = "Isaac Asimov";

СОВЕТ. Работать с гибкой моделью данных особенно полезно в приложениях, где форма данных имеет тенденцию быть динамичной (например, веб-приложения и веб-сервисы).

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

в реальных приложениях. Далее рассмотрим издержки соблюдения принципа № 2.

A.2.3. Издержки принципа № 2

Как и в случае с любым принципом программирования, использование этого принципа сопряжено с некоторыми компромиссами. Вот цена, которую мы платим за

представление данных с помощью обобщенных структур:

наблюдается небольшое снижение производительности;

не требуется схема данных;

не требуется проверка данных на валидность во время компиляции;

в некоторых языках со статической типизацией требуется приведение типов.

Издержка № 1: снижение производительности

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

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

стоит.

СОВЕТ. Извлечение и сохранение значения, связанного с произвольным ключом, на

карте происходит немного медленнее, чем в случае с элементом класса.

414

Приложение А

Издержка № 2: отсутствие схемы данных

Когда экземпляр данных создается из класса, информация о форме данных содер-жится в определении класса. Каждый фрагмент данных имеет связанную форму

данных. Наличие схемы данных на уровне класса полезно для разработчиков и для

сред IDE по следующим причинам:

разработчики могут легко обнаружить ожидаемую форму данных;

среды IDE предоставляют такие функции, как автозаполнение названия поля.

Когда данные представлены с помощью обобщенных структур, схема данных не

является частью такого представления. Как следствие, некоторые фрагменты данных могут иметь связанную схему данных, а другие фрагменты — нет (см. принцип № 4).

СОВЕТ. Когда для хранения данных используются обобщенные структуры, форма данных не является частью представления данных.

Издержка № 3: отсутствует проверка данных на валидность

во время компиляции

Еще раз взгляните на функцию fullName в следующем листинге, который изначаль-но был создан для изучения принципа № 1. Эта функция получает данные, которыми она манипулирует, в качестве аргумента.

Листинг A.18. Декларация функции fullName

function fullName(data) {

return data.firstName + " " + data.lastName;

}

Когда данные, которые передаются fullName, не соответствуют ожидаемой этой

функцией форме, во время выполнения возникает ошибка. При использовании

обобщенных структур данных неправильный ввод названия поля, хранящего имя

автора (например, fistName вместо firstName), не приводит к ошибке во время компиляции или исключению. В этом случае поле firstName будет таинственным образом пропущено при выводе результата. В следующем листинге показано это не-ожиданное поведение.

Листинг A.19. Неожиданное поведение с невалидными данными

fullName({fistName: "Issac", lastName: "Asimov"});

// → "undefined Asimov"

Когда мы создаем экземпляры данных с помощью классов с жесткой формой данных, этот тип ошибки обнаруживается во время компиляции. Этот недостаток компенсируется применением принципа № 4, который занимается валидацией данных.

Принципы дата-ориентированного программирования

415

СОВЕТ. Когда данные представлены с помощью обобщенных структур, ошибки, связанные с формой данных, обнаруживаются только во время выполнения.

Издержка № 4:

необходимость явного приведения типов

В некоторых языках со статической типизацией требуется явное приведение типов.

В этом подразделе рассматривается явное приведение типов в Java и динамические

поля в C#.

В статически типизированном языке, таком как Java, данные об авторе могут быть

представлены в виде карты, ключи которой имеют тип string, а значения имеют

типы Object. Например, в Java данные об авторе представлены картой Map<String, Object>, как показано в следующем листинге.

Листинг A.20. Данные об авторе в виде строковой карты в Java

var asimov = new HashMap<String, Object>();

asimov.put("firstName", "Isaac");

asimov.put("lastName", "Asimov");

asimov.put("books", 500);

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

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

Листинг A.21. Приведение типа требуется при доступе к полю в Java

class AuthorRating {

static boolean isProlific (Map<String, Object> data) {

return (int)data.get("books") > 100;

}

}

Некоторые библиотеки JSON-сериализации для Java, такие как Gson (https://

github.com/google/gson), поддерживают сериализацию карт типа Map<String, Object>, не требуя от пользователя выполнения какого-либо приведения типа. Все

волшебство происходит за кулисами!

C# поддерживает динамический тип данных dynamic (см. по ссылке http://mng.bz/

voqJ), который позволяет проверять тип во время выполнения. При использовании

этой возможности данные об авторе представляются в виде словаря, в котором

ключи имеют тип string, а значения — тип dynamic. В следующем листинге показано такое представление.

416

Приложение А

Листинг A.22. Данные об авторе в виде динамической строковой карты в C#

var asimov = new Dictionary<string, dynamic>();

asimov["name"] = "Isaac Asimov";

asimov["books"] = 500;

Вопрос о точном типе значений поля разрешается во время выполнения. При доступе к полю приведение типа не требуется. Например, проверяя, является ли автор

плодовитым, к полю books можно получить доступ, как будто оно было деклариро-вано как целое число (смотрите следующий листинг).

Листинг A.23. Приведение типа не требуется

при доступе к динамическим полям в C#

class AuthorRating {

public static bool isProlific (Dictionary<String, dynamic>

data) {

return data["books"] > 100;

}

}

A.2.4. Основная суть принципа № 2

В ДОП для представления данных используются обобщенные структуры. Это может привести к (небольшому) снижению производительности и вызвать необходимость вручную документировать форму данных, поскольку компилятор не может

проверить ее статически. Соблюдение этого принципа позволяет манипулировать

данными с помощью широкого выбора обобщенных функций (предоставляемых

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

При таком положении дел данные могут быть либо изменяемыми, либо неизменяемыми. Следующий принцип (принцип № 3) иллюстрирует важность неизменяемости.

ПРИНЦИП ДОП № 2:

ПРЕДСТАВЛЯЙТЕ ДАННЫЕ С ПОМОЩЬЮ ОБОБЩЕННЫХ СТРУКТУР ДАННЫХ

Чтобы исполнить этот принцип, мы представляем данные приложения с помощью обобщенных структур в основном карт и массивов (а также списков). Диаграмма на рис. А.5

дает наглядное представление об этом принципе.

Рис. A.5. Принцип ДОП № 2: Представляйте данные с помощью общих структур данных

Принципы дата-ориентированного программирования

417

Преимущества:

Использование обобщенных функций, которые не ограничены определенным случаем

использования.

Гибкая модель данных.

Издержки реализации этого принципа:

Небольшое снижение производительности.

Схема данных не требуется.

Не требуется проверка валидности данных во время компиляции.

В некоторых языках со статической типизацией требуется явное приведение типов.

A.3. Принцип № 3: данные неизменяемы

Как же управлять изменениями в данных, когда данные отделены от кода и представлены с помощью обобщенных структур? ДОП придерживается очень строгих

взглядов на этот вопрос. Изменение данных не допускается! В ДОП изменения

в данных выполняются путем создания новых версий данных. Ссылка на переменную может быть изменена так, чтобы она указывала на новую версию данных, но

значение самих данных никогда не должно изменяться.

ПРИНЦИП № 3: данные неизменяемы.

A.3.1. Иллюстрация к принципу № 3

Представьте себе число 42. Что происходит с этим числом, когда вы добавляете

к нему 1? Превращается ли оно в 43? Нет, 42 всегда будет 42! А теперь поместите

число 42 внутрь объекта: {num: 42}. Что происходит с объектом, когда вы добавляете 1 к 42? Превращается ли он в 43? А вот это зависит от языка программирования.

В Clojure, языке программирования, который поддерживает неизменяемость

данных, значение поля num остается равным 42 всегда и несмотря ни на что.

Во многих языках программирования значение поля num становится 43.

Например, в JavaScript изменение поля в словаре, на которую ссылаются две переменные, влияет на обе переменные. Следующий листинг это демонстрирует.

Листинг А.24. Изменение данных, на которые ссылаются две переменные, влияет на обе переменные

var myData = {num: 42};

var yourData = myData;

yourData.num = yourData.num + 1;

console.log(myData.num);

// → 43

Теперь значение myData.num равно 43. Однако, согласно ДОП, данные никогда не

должны меняться! Вместо изменения данных они создаются заново. Простой до

418

Приложение А

безобразия (и неэффективный) способ создания новой версии данных — это клони-рование данных перед их модификацией. Например, в листинге A.25 есть функция, которая изменяет значение поля внутри объекта путем клонирования объекта с помощью функции Object.assign, нативно предоставляемой JavaScript. Когда функция

changeValue вызывается со значением myData, само myData не затрагивается; myData

.num остается равным 42. В этом вся суть неизменяемости данных!

Листинг А.25. Неизменяемость данных при помощи клонирования

function changeValue(obj, k, v) {

var res = Object.assign({}, obj);

res[k] = v;

return res;

}

var myData = {num: 42};

var yourData = changeValue(myData, "num", myData.num + 1); console.log(myData.num);

// → 43

Для эффективного обеспечения неизменяемости с точки зрения как вычислений, так и памяти требуется сторонняя библиотека, например, Immutable.js (https://

immutable-js.com/), которая обеспечивает эффективную реализацию персистентных структур данных (также известных как неизменяемые структуры данных).

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

При задействовании Immutable.js не используются нативные карты и массивы

JavaScript, а вместо этого создаются экземпляры неизменяемых карт и неизменяемых списков с помощью Immutable.Map и Immutable.List. Доступ к элементу карты

осуществляется с помощью метода get. Новая версия карты создается при модифи-кации поля с помощью метода set.

В листинге A.26 показано, как можно эффективно создавать неизменяемые данные

и манипулировать ими с помощью сторонней библиотеки. В выходных данных

значение yourData.get("num") равно 43, но значение myData.get("num") остается 42.

Листинг А.26. Создание неизменяемых данных и манипулирование ими

var myData = Immutable.Map({num: 42})

var yourData = myData.set("num", 43);

console.log(yourData.get("num"));

// → 43

console.log(myData.get("num"));

// → 42

СОВЕТ. Когда данные неизменяемы, вместо изменения данных создается их новая версия.

Принципы дата-ориентированного программирования

419

A.3.2. Преимущества принципа № 3

Когда программы ограничены в возможности изменения данных, это выгодно во

многих отношениях. В следующих подразделах подробно описываются такие преимущества:

надежность доступа к данным для всех;

предсказуемость поведения кода;

быстрая проверка равенства;

безопасность конкурентности без затрат.

Преимущество № 1: надежный доступ к данным для всех

Согласно принципу № 1 (отделение кода от данных) доступ к данным является

прозрачным. Любой функции разрешен доступ к любому фрагменту данных. Если

неизменяемость данных не гарантирована, нужно вести себя крайне осторожно при

передаче данных функции в качестве аргумента. Нам придется либо точно убедиться, что функция не мутирует данные, либо клонировать данные до того, как они

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

СОВЕТ. Когда данные неизменяемы, их можно надежно передавать любой функции, потому что данные точно никогда не поменяются.

Преимущество № 2: предсказуемое поведение кода

В качестве иллюстрации того, что подразумевается под предсказуемостью, вот

пример непредсказуемого фрагмента кода, который не придерживается принципа

неизменяемости данных. Взгляните на фрагмент асинхронного кода JavaScript в следующем листинге. Когда данные изменяемы, поведение асинхронного кода

непредсказуемо.

Листинг А.27. Поведение непредсказуемого асинхронного кода,

когда данные изменяемы

var myData = {num: 42};

setTimeout(function (data){

console.log(data.num);

}, 1000, myData);

myData.num = 0;

Значение data.num внутри обратного вызова со временем ожидания не является

предсказуемым. Оно зависит от того, были ли данные модифицированы другим

фрагментом кода в течение 1000 миллисекунд времени ожидания. Однако неизменяемость данных гарантирует, что данные всегда останутся прежними, а значение

data.num всегда будет равным 42 внутри обратного вызова.

420

Приложение А

СОВЕТ. Когда данные неизменяемы, поведение кода, который манипулирует данными, предсказуемо.

Преимущество № 3: быстрая проверка равенства

При использовании фреймворков пользовательского интерфейса (англ. user interface, UI), таких как React.js, необходимы частые проверки того, какая часть

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

В приложении React, которое не придерживается принципа неизменяемости данных, необходимо проверять каждую (вложенную) часть данных UI. Однако в приложении React, которое придерживается принципа неизменяемости данных, можно

оптимизировать сравнение данных для случая, когда данные не модифицируются.

И в самом деле, когда адрес объекта тот же, можно гарантировать, что данные не

изменились.

Сравнение адресов объектов выполняется намного быстрее, чем сравнение всех

полей. В части 1 нашей книги быстрые проверки на равенство используются для

согласования конкурентных изменений в высокомасштабируемой готовой к работе

системе.

СОВЕТ. Неизменяемые данные позволяют быстро проверять на равенство путем сравнения данных по ссылке.

Преимущество № 4: безопасность конкурентности без затрат

В многопоточной среде механизмы безопасности конкурентности (например, мьютексы) часто используются для предотвращения изменения данных в потоке A во

время доступа к ним в потоке B. В дополнение к небольшому снижению производительности, которое они вызывают, механизмы безопасности конкурентности

налагают дополнительную умственную нагрузку, значительно затрудняя написание

и чтение кода.

СОВЕТ. Следование принципу неизменяемости данных устраняет необходимость в ме-ханизме контроля конкурентности. Ваши данные никогда не меняются!

A.3.3. Издержки принципа № 3

Как и в случае с предыдущими принципами, применение принципа № 3 имеет свою

цену. В подразделах ниже рассматриваются следующие издержки:

снижение производительности;

потребность в библиотеке для персистентных структур данных.

Принципы дата-ориентированного программирования

421

Издержка № 1: снижается производительность

Как мы упоминали ранее, варианты реализации персистентных структур данных

существуют в большинстве языков программирования. Но даже самая эффективная

реализация происходит немного медленнее, чем изменение данных на месте.

В большинстве приложений снижение производительности и дополнительное по-требление памяти, связанные с использованием персистентных структур данных, незначительны. Но забывать об этом не стоит.

Издержка № 2: требуется библиотека

для персистентных структур данных

В таком языке как Clojure нативные структуры данных сами по себе неизменяемы.

Однако в большинстве языков программирования, чтобы соблюсти принцип неизменяемости данных, потребуется сторонняя библиотека, обеспечивающая реализацию персистентных структур данных.

Когда эти структуры данных не являются нативными для используемого языка, будет трудно (а порой и невозможно) обеспечить повсеместное использование неизменяемых данных. Кроме того, при интеграции сторонних библиотек (например, библиотеки диаграмм) персистентные структуры данных должны быть преобразо-ваны в эквивалентные нативные структуры данных.

A.3.4. Основная суть принципа № 3

ДОП рассматривает данные как значение, которое никогда не меняется. Если придерживаться этого принципа, код будет предсказуем даже в многопоточной среде, а

проверка равенства может быть выполнена быстро. Однако вам потребуется значительно перепрограммировать себя для работы с этим принципом, а в большинстве

языков программирования для обеспечения эффективной реализации персистентных структур данных необходима сторонняя библиотека.

ПРИНЦИП ДОП № 3: ДАННЫЕ НЕИЗМЕНЯЕМЫ

Когда мы придерживаемся этого принципа, мы представляем данные в виде неизменяемых структур. Диаграмма на рис. А.6 дает наглядное представление об этом.

Рис. A.6. Принцип ДОП № 3: Данные неизменяемы

Преимущества:

Надежный доступ к данным для всех.

Предсказуемое поведение кода.

422

Приложение А

Быстрые проверки на равенство.

Безопасность конкурентности без затрат.

Издержки реализации принципа № 3:

Снижение производительности.

Необходимость в библиотеке персистентных структур данных.

A.4. Принцип № 4: отделяйте схему данных

от представления данных

Данные отделены от кода и представлены с помощью обобщенных и неизменяемых

структур, и далее возникает вопрос о том, как выражается форма данных. В ДОП

ожидаемая форма выражается в виде схемы данных, которая хранится отдельно от

самих данных. Главное преимущество принципа № 4 заключается в том, что он

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

какие — нет.

ПРИНЦИП № 4: отделяйте схему данных от представления данных.

A.4.1. Иллюстрация к принципу № 4

Предлагаю поразмыслить о том, как обработать запрос на добавление нового автора книг в систему. Чтобы упростить задачу, представьте, что такой запрос содержит

только основную информацию об авторе: его имя и фамилию и в качестве опции

количество написанных им книг. Из принципа № 2 (данные представлены с помощью обобщенных структур) следует, что в ДОП данные запроса представляются

в виде строковой карты, и ожидается, что карта будет содержать три поля: firstName — это строка;

lastName — тоже строка;

books — число (в качестве опции).

В ДОП ожидаемая форма данных — это данные, которые хранятся отдельно от

данных запроса. Например, JSON-схема (https://json-schema.org/) может представлять схему данных запроса с помощью карты. В следующем листинге приведен

пример.

Листинг A.28. Схема JSON для данных запроса addAuthor

var addAuthorRequestSchema = {

"type": "object", ❶

"required": ["firstName", "lastName"], ❷

"properties": {

"firstName": {"type": "string"}, ❸

"lastName": {"type": "string"}, ❹

"books": {"type": "integer"} ❺

}

};

Принципы дата-ориентированного программирования

423

❶ Ожидается, что данные будут представлять собой карту (в JSON карта называется объектом).

❷ Только поля firstName и lastName являются обязательными.

❸ Поле firstName должно быть строкой.

❹ Поле lastName должно быть строкой.

❺ Поле books должно быть числом (когда оно предоставлено).

Библиотека валидации данных используется, чтобы проверить соответствие фрагмента схеме данных. Например, можно использовать валидатор схемы Ajv JSON

(https://ajv.js.org/) для валидации данных с помощью функции validate, которая

возвращает значение true, когда данные валидны, и false, когда данные невалидны.

В следующем листинге показан этот подход.

Листинг A.29. Валидация данных с помощью Ajv

var validAuthorData = {

firstName: "Isaac",

lastName: "Asimov",

books: 500

};

ajv.validate(addAuthorRequestSchema,

validAuthorData); // ❶

// → true

var invalidAuthorData = {

firstName: "Isaac",

lastNam: "Asimov",

books: "five hundred"

};

ajv.validate(addAuthorRequestSchema,

invalidAuthorData); ❷

// → false

❶ Данные валидны.

❷ Поле данных называется lastNam вместо lastName, а books — это строка, а не число.

Когда данные невалидны, сведения о сбоях валидации доступны в удобочитаемом

формате. В следующем листинге — пример этого подхода.

Листинг A.30. Получение подробной информации о сбое валидации данных

var invalidAuthorData = {

firstName: "Isaac",

lastNam: "Asimov",

424

Приложение А

books: "five hundred"

};

var ajv = new Ajv({allErrors: true}); ❶

ajv.validate(addAuthorRequestSchema, invalidAuthorData);

ajv.errorsText(ajv.errors); ❷

// → "data should have required property 'lastName',

// → data.books should be number"

❶ По умолчанию Ajv сохраняет только первую ошибку валидации данных. Установите

allErrors: true для сохранения всех ошибок.

❷ Ошибки валидации данных хранятся внутри в виде массива. Чтобы получить строку, удобную для чтения, используйте функцию errorsText.

A.4.2. Преимущества принципа № 4

Отделение схемы данных от представления данных обеспечивает многочисленные

преимущества. В следующих подразделах подробно описываются эти преимущества.

Свободный выбор того, какие данные должны быть валидированы.

Опциональные поля.

Расширенные условия валидации данных.

Автоматическое создание визуализации модели данных.

Преимущество № 1: возможность свободно выбирать,

какие данные следует валидировать

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

мгновенное прототипирование или экспериментирование;

рефакторинг кода и валидация данных.

Рассмотрим мгновенное прототипирование. В классическом ООП нужно создавать

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

определение схемы данных на более поздний этап.

Одним из распространенных шаблонов рефакторинга является рефакторинг с разделением этапов (https://refactoring.com/catalog/splitPhase.html), когда одна

большая функция разделяется на множество более мелких функций с приватной

областью действия. Мы вызываем эти функции с данными, которые уже были

валидированы более крупной функцией. В ДОП нет необходимости указывать

Принципы дата-ориентированного программирования

425

форму аргументов внутренних функций: можно полагаться на валидацию данных, которая уже произошла.

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

ранее при иллюстрации принципа № 2 код для вычисления полного имени и уровня

плодовитости автора, можно было бы использовать функцию displayAuthorInfo, как

показано в следующем листинге.

Листинг А.31. Отображение информации об авторе книги

class NameCalculation {

static fullName(data) {

return data.firstName + " " + data.lastName;

}

}

class AuthorRating {

static isProlific (data) {

return data.books > 100;

}

}

var authorSchema = {

"type": "object",

"required": ["firstName", "lastName"],

"properties": {

"firstName": {"type": "string"},

"lastName": {"type": "string"},

"books": {"type": "integer"}

}

};

function displayAuthorInfo(authorData) {

if(!ajv.validate(authorSchema, authorData)) {

throw "displayAuthorInfo called with invalid data";

};

console.log("Author full name is: ",

NameCalculation.fullName(authorData));

if(authorData.books == null) {

console.log("Author has not written any book");

} else {

if (AuthorRating.isProlific(authorData)) {

console.log("Author is prolific");

} else {

console.log("Author is not prolific");

}

}

}

426

Приложение А

Обратите внимание: первое, что происходит внутри тела displayAuthorInfo, — это

валидация того, что функции был передан аргумент. Теперь примените шаблон

рефакторинга с расщепленной фазой к этому простому примеру и разделите тело

displayAuthorInfo на две внутренние функции:

displayFullName отображает полное имя автора;

displayProlificity отображает, является ли автор плодовитым.

В следующем листинге показан получившийся код.

Листинг A.32. Применение шаблона рефакторинга с расщепленной фазой

function displayFullName(authorData) {

console.log("Author full name is: ",

NameCalculation.fullName(authorData));

}

function displayProlificity(authorData) {

if(authorData.books == null) {

console.log("Author has not written any book");

} else {

if (AuthorRating.isProlific(authorData)) {

console.log("Author is prolific");

} else {

console.log("Author is not prolific");

}

}

}

function displayAuthorInfo(authorData) {

if(!ajv.validate(authorSchema, authorData)) {

throw "displayAuthorInfo called with invalid data";

};

displayFullName(authorData);

displayProlificity(authorData);

}

Когда схема данных отделена от представления данных, исчезает необходимость

указывать схему данных для аргументов внутренних функций displayFullName и

displayProlificity. Это делает процесс рефакторинга чуть более плавным. В некоторых случаях внутренние функции могут быть сложнее, тогда все же стоит указать схему данных для их аргументов. ДОП дает нам свободу выбора!

Преимущество № 2: наличие опциональных полей

В ООП непросто позволить элементу класса присутствовать в качестве опции. Например, в Java потребуется специальный конструкт, подобный классу Optional, вве-денному в Java 8 (http://mng.bz/4jWa). В ДОП декларировать поле на карте как

Принципы дата-ориентированного программирования

427

опциональное — это нормально. На самом деле в JSON-схеме все поля являются

опциональным по умолчанию.

Чтобы сделать поле не опциональным, а обязательным, его название должно быть

включено в массив required, например, как в схеме для данных об авторе в листинге A.33, где обязательными полями являются только firstName и lastName, а поле

books — опциональное. Обратите внимание: когда опциональное поле определено

на карте, его значение валидируется на соответствие схеме.

Листинг А.33. Схема с опциональным полем

var authorSchema = {

"type": "object",

"required": ["firstName", "lastName"], ❶

"properties": {

"firstName": {"type": "string"},

"lastName": {"type": "string"},

"books": {"type": "number"} ❷

}

};

❶ Поле books не включено в массив required, потому что это опциональное поле.

❷ Если поле books присутствует, его значение должно выражаться числом.

Сейчас мы проиллюстрируем, как функция валидации работает с опциональными

полями. Карта без поля books считается валидной, как показано в листинге A.34.

При этом карта с полем books, в которой его значение не является числом, считается невалидной, что можно увидеть в листинге А.35.

Листинг A.34. Валидная карта без опционального поля

var authorDataNoBooks = {

"firstName": "Yehonathan",

"lastName": "Sharvit"

};

ajv.validate(authorSchema, authorDataNoBooks); ❶

// → true

❶ Валидация проходит успешно, т. к. books является необязательным полем.

Листинг A.35. Невалидная карта с невалидным опциональным полем

var authorDataInvalidBooks = {

"firstName": "Albert",

"lastName": "Einstein",

"books": "Five"

};

428

Приложение А

validate(authorSchema, authorDataInvalidBooks); ❶

// → false

❶ Валидация неуспешна, т. к. значение поля books не является числом.

Преимущество № 3:

наличие расширенных условий валидации данных

В ДОП валидация данных происходит во время выполнения программы. Это позволяет определять для валидации условия, которые выходят за рамки типа поля.

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

JSON-схема поддерживает множество других расширенных условий валидации

данных, таких как валидация регулярных выражений для строковых или числовых

полей, которые должны быть кратны заданному числу. В схеме данных об авторе

в листинге A.36 ожидается, что поля firstName и lastName будут строками длиной

менее 100 символов, а поле books — числом от 0 до 10 000.

Листинг А.36. Схема с расширенными условиями валидации данных

var authorComplexSchema = {

"type": "object",

"required": ["firstName", "lastName"],

"properties": {

"firstName": {

"type": "string",

"maxLength": 100

},

"lastName": {

"type": "string",

"maxLength": 100

},

"books": {

"type": "integer",

"minimum": 0,

"maximum": 10000

}

}

};

Преимущество № 4: возможность

автоматического создания визуализации модели данных

Поскольку схема данных определена как сами данные, можно использовать

несколько инструментов для создания визуализаций модели данных. С помощью

таких инструментов, как JSON Schema Viewer (https://navneethg.github.io/

Принципы дата-ориентированного программирования

429

jsonschemaviewer/) и Malli (https://github.com/metosin/malli), можно сгенерировать UML-диаграмму (англ. Unified Modeling Language, унифицированный язык

моделирования) из JSON-схемы.

Например, JSON-схема в листинге A.37 определяет форму поля bookList, представ-ляющего собой массив с данными о книгах, в котором каждая книга — это карта, а

на рис. A.7 эта схема визуализирована в виде UML-диаграммы. Эти инструменты

создают UML-диаграмму из JSON-схемы.

Листинг А.37. JSON-схема с массивом объектов

{

"type": "object",

"required": ["firstName", "lastName"],

"properties": {

"firstName": {"type": "string"},

"lastName": {"type": "string"},

"bookList": {

"type": "array",

"items": {

"type": "object",

"properties": {

"title": {"type": "string"},

"publicationYear": {"type": "integer"}

}

}

}

}

}

Рис. А.7. UML-визуализация

JSON-схемы из листинга А.37

A.4.3. Издержки принципа № 4

За применение принципа № 4 придется заплатить свою цену. В следующих подразделах рассматриваются такие издержки:

слабая связь между данными и их схемой;

небольшое снижение производительности.

430

Приложение А

Издержка № 1: слабая связь между данными и их схемой

По определению, когда схема данных и представление данных разделены, связь

между данными и их схемой слабее, чем когда данные представлены классами.

Более того, язык определения схемы (например, JSON-схема) не является частью

используемого языка программирования. Разработчик должен самостоятельно решить, когда валидация данных необходима, а когда излишня. Как говорится, у кого

много прав, у того и много ответственности.

Издержка № 2: небольшое снижение производительности

Как упоминалось ранее, варианты реализации для валидации JSON-схемы существуют в большинстве языков программирования. В ДОП валидация данных происходит во время выполнения, и для ее осуществления потребуется некоторое время.

В ООП валидация данных обычно происходит во время компиляции.

Этот недостаток компенсируется тем фактом, что даже в ООП некоторые этапы

валидации данных проходят во время выполнения. Например, конвертирование

полезной нагрузки, содержащееся в запросе JSON, в объект происходит во время

выполнения программы. Кроме того, в ДОП довольно часто некоторые этапы валидации данных вводятся только во время разработки и деактивируются при запуске

готовой системы. Как следствие, происходящее снижение производительности незначительно.

A.4.4. Основная суть принципа № 4

В ДОП данные представлены при помощи неизменяемых обобщенных структур.

Когда требуется дополнительная информация о форме данных, можно определить

схему данных (например, с использованием схемы JSON). Отделяя схему данных

от представления данных, мы получаем свободу решать, когда именно данные

должны быть валидированы.

Помимо этого, валидация данных происходит во время выполнения. Следовательно, могут быть поставлены условия валидации, которые выходят за рамки статических типов данных (например, длины строки). Однако много прав — это еще и

много ответственности, и разработчику нельзя забывать о необходимости валидировать данные.

ПРИНЦИП ДОП № 4: ОТДЕЛЯЙТЕ СХЕМУ ДАННЫХ ОТ ПРЕДСТАВЛЕНИЯ ДАННЫХ

Придерживаясь этого принципа, мы проводим разделение между схемой данных и пред-ставлением данных. Диаграмма на рис. А.8 иллюстрирует это правило.

Рис. A.8. Принцип ДОП № 4: отделяйте схему данных от представления данных

Принципы дата-ориентированного программирования

431

Преимущества:

Свобода выбора того, какие именно данные нужно валидировать.

Опциональные поля.

Расширенные условия валидации данных.

Автоматическое создание визуализации модели данных.

Издержки реализации принципа № 4:

Слабая связь между данными и их схемой.

Небольшое снижение производительности.

Заключение

ДОП упрощает проектирование и реализацию информационных систем, относясь

к данным как к сущностям первого класса. Это возможно благодаря четырем базовым принципам, не зависящим от языка (рис. A.9).

Код отделен от данных.

Данные приложения представлены с помощью обобщенных структур.

Данные считаются неизменяемыми.

Схема данных отделена от представления данных.

В этом приложении было показано, как именно каждый принцип можно применить

в языках ФП и ООП. Также мы дали высокоуровневое описание преимуществ

и издержек соблюдения каждого из принципов.

Рис. А.9. Принципы ДОП

432

Приложение А

Приложение B

Обобщенный доступ к данным

в статически типизированных языках

Представление данных с помощью обобщенных структур данных естественным

образом вписывается в динамически типизированные языки программирования, такие как JavaScript, Ruby или Python. Однако в статически типизированных языках

программирования, таких как Java или C#, представление данных в виде строковых

карт со значениями неопределенного типа не является естественным по нескольким

причинам:

для доступа к полям карты требуется приведение типа;

имена полей карты не проверяются во время компиляции;

автозаполнение и другие удобные функции IDE недоступны.

В этом приложении рассматриваются различные способы улучшения доступа

к обобщенным данным на статически типизированных языках. Мы рассмотрим: геттеры значений для карт, позволяющие избежать приведения типов при доступе к полям карты;

типизированные геттеры для карт, позволяющие использовать проверки имен

полей карты во время компиляции;

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

B.1. Динамические геттеры

для строковых карт

Давайте начнем с уточнения подхода, который мы представили в части 1. А именно — мы представили записи в виде строковых карт и получили доступ к полям

карты с помощью динамических геттеров и приведения типов.

ПРИМЕЧАНИЕ. Большинство фрагментов кода в этом приложении используют Java, но

иллюстрированные подходы могут быть применены и к другим объектно-ориентированным

статически типизированным языкам, таким как C# или Go.

434

Приложение B

B.1.1. Доступ к невложенным полям карты

с помощью динамических геттеров

В этом приложении мы проиллюстрируем различные способы обеспечения общего

доступа к данным с использованием учетной записи. Наша запись составлена из

этих частей:

title (строка);

isbn (строка);

publicationYear (целое число).

В листинге B.1 показано представление двух книг «Watchmen» и «7 Habits of Highly Effective People» на языке Java. Эти строковые сопоставления содержат значения

типа Object.

Листинг B.1. Две книги, представленные в виде карт

Map watchmenMap = Map.of(

"isbn", "978-1779501127",

"title", "Watchmen",

"publicationYear", 1987

);

Map sevenHabitsMap = Map.of(

"isbn", "978-1982137274",

"title", "7 Habits of Highly Effective People",

"publicationYear", 2020

);

К полям карты можно получить обобщенный доступ с помощью динамического

геттера. В следующем листинге показана реализация.

Листинг B.2. Реализация динамического геттера для полей карты

class DynamicAccess {

static Object get(Map m, String k) {

return (m).get(k);

}

}

Недостатком динамических геттеров является то, что для управления значением

поля карты требуется приведение типа. Например, как показано в листинге B.3, приведение к String необходимо для вызова строкового метода toUpperCase для значения поля title.

Обобщенный доступ к данным в статически типизированных языках

435

Листинг B.3. Доступ к полям карты с помощью динамического геттера

и приведения типов

((String)DynamicAccess.get(watchmenMap, "title")).toUpperCase();

// → "WATCHMEN"

Динамические геттеры обеспечивают обобщенный доступ к данным в том смысле, что они не требуют специальных знаний о типе данных, которые представляет

строковая карта. Как следствие, имя поля может быть получено динамически (например, от пользователя), как показано в листинге B.4. Это работает, потому что

для доступа к полю данных книги в строковой карте нет необходимости импортировать класс, который определяет книгу.

Листинг B.4. Отображение поля карты с помощью динамического геттера

и приведения типов

var books = List.of(watchmenMap, sevenHabitsMap);

var fieldName = "title";

books.stream()

.map(x -> DynamicAccess.get(x, fieldName))

.map(x -> ((String)x).toUpperCase())

.collect(Collectors.toList())

// → ["WATCHMEN", "7 HABITS OF HIGHLY EFFECTIVE PEOPLE"]

Другой аспект универсальности динамических геттеров заключается в том, что они

работают с данными любого типа. Например, динамический геттер для title работает не только с книгами, но и с любым фрагментом данных, имеющим поле title.

B.1.2. Доступ к вложенным полям карты

с помощью динамических геттеров

В листинге B.5 представлен пример результатов поиска. Предположим, что результаты поиска представлены в виде карты строк, где:

ключи — это книжные ISBN;

значения — это книжные данные, представленные в виде карт строк, как

в предыдущем разделе.

Листинг B.5. Результаты поиска, представленные в виде карты

Map searchResultsMap = Map.of(

"978-1779501127", Map.of(

"isbn", "978-1779501127",

"title", "Watchmen",

"publicationYear", 1987

),

436

Приложение B

"978-1982137274", Map.of(

"isbn", "978-1982137274",

"title", "7 Habits of Highly Effective People",

"publicationYear", 2020

)

);

Поля книги вложены в карту результатов поиска. Для доступа к полям вложенной

карты в класс DynamicAccess в листинге B.6 добавлен метод get. Этот геттер получает список строк, представляющих информационный путь вложенного поля карты.

Листинг B.6. Реализация динамического геттера для вложенных полей карты

class DynamicAccess {

static Object get(Map m, String k) {

return (m).get(k);

}

static Object get(Map m, List<String> path) {

Object v = m;

for (String k : path) {

v = get((Map)v, k);

if (v == null) {

return null;

}

}

return v;

}

}

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

рассмотрим, как избежать приведения типов при манипулировании значениями

в сопоставлении строк.

Листинг B.7. Вложенные поля карты с динамическим геттером и приведением типов

((String)DynamicAccess.get(searchResultsMap,

List.of("978-1779501127", "title"))).toUpperCase();

// → "WATCHMEN"

B.2. Геттеры значений для карт

Самый простой способ избежать приведения типов при манипулировании значением поля строковой карты — это использовать динамический тип данных (см. приложение А). Динамические типы данных поддерживаются в таких языках, как C#,

Обобщенный доступ к данным в статически типизированных языках

437

но не в таких языках, как Java. Далее мы проиллюстрируем, как геттеры значений

позволяют избежать приведения типов.

B.2.1. Доступ к невложенным полям карты

с помощью геттеров значений

В этом разделе книги по-прежнему представлены в виде строковых карт со значениями типа Object. В следующем листинге показано это представление.

Листинг B.8. Две книги, представленные в виде карт

Map watchmenMap = Map.of(

"isbn", "978-1779501127",

"title", "Watchmen",

"publicationYear", 1987

);

Map sevenHabitsMap = Map.of(

"isbn", "978-1982137274",

"title", "7 Habits of Highly Effective People",

"publicationYear", 2020

);

Идея геттеров значений довольно проста. Вместо того чтобы выполнять приведение типов вне геттера, оно выполняется внутри геттера. Геттер значения требуется

для каждого типа: getAsString для строк, getAsInt для целых чисел, getAsFloat для

чисел с плавающей запятой, getAsBoolean для логических значений и т. д.

Геттер значений используется библиотеками Java, такими как Apache Wicket (http://mng.bz/wnqQ) и Gson (https://github.com/google/gson). В листинге B.9 показана реализация getAsString, которая извлекает значение поля карты в виде строки.

Листинг B.9. Реализация геттера значений для полей карты

class DynamicAccess {

static Object get(Map m, String k) {

return (m).get(k);

}

static String getAsString(Map m, String k) {

return (String)get(m, k);

}

}

Доступ к полю карты можно получить без приведения типов. Например, мы можем

использовать getAsString для управления названием книги, как показано в следующем листинге.

438

Приложение B

Листинг B.10. Доступ к невложенным полям с помощью геттера значений

DynamicAccess.getAsString(watchmenMap, "title").toUpperCase();

// → "WATCHMEN"

Сопоставление книг с помощью геттера значений немного удобнее без приведения

типов. Взгляните на следующий листинг.

Листинг B.11. Отображение списка карт с помощью геттера значения

var books = List.of(watchmenMap, sevenHabitsMap);

books.stream()

.map(x -> DynamicAccess.getAsString(x, "title"))

.map(x -> x.toUpperCase())

.collect(Collectors.toList())

// → ["WATCHMEN", "7 HABITS OF HIGHLY EFFECTIVE PEOPLE"]

B.2.2. Доступ к вложенным полям карты

с помощью геттеров значений

Подход геттера значения естественным образом применяется к вложенным полям

карты. Как и в разделе динамического геттера, предположим, что результаты поиска представлены в виде сопоставления строк, как в листинге B.12. Поля книги вложены в карту результатов поиска, где:

ключи — это книжные ISBN;

значения — это книжные данные, представленные в виде карт строк, как в предыдущем разделе.

Листинг B.12. Результаты поиска, представленные в виде карты

Map searchResultsMap = Map.of(

"978-1779501127", Map.of(

"isbn", "978-1779501127",

"title", "Watchmen",

"publicationYear", 1987

),

"978-1982137274", Map.of(

"isbn", "978-1982137274",

"title", "7 Habits of Highly Effective People",

"publicationYear", 2020

)

);

Чтобы получить доступ к вложенным полям карты без приведения типов, мы добавили метод getAsString в класс DynamicAccess. Этот класс получает список строк,

Обобщенный доступ к данным в статически типизированных языках

439

который представляет информационный путь вложенного поля карты, как в следующем листинге.

Листинг B.13. Реализация геттера значений для вложенных полей карты

class DynamicAccess {

static Object get(Map m, String k) {

return (m).get(k);

}

static Object get(Map m, List<String> p) {

Object v = m;

for (String k : p) {

v = get((Map)v, k);

if (v == null) {

return null;

}

}

return v;

}

static String getAsString(Map m, String k) {

return (String)get(m, k);

}

static String getAsString(Map m, List<String> p) {

return (String)get(m, p);

}

}

С помощью геттера вложенных значений заголовками книг можно манипулировать

в результатах поиска без приведения типов. Следующий листинг демонстрирует

это.

Листинг B.14. Доступ к полям вложенной карты с помощью геттера значений

var informationPath = List.of("978-1779501127", "title"); DynamicAccess.getAsString(searchResultsMap, informationPath)

.toUpperCase();

// → "WATCHMEN"

Геттеры значений делают доступ к данным немного более удобным, если избегать

приведения типов. В следующем разделе показано, как типизированные геттеры

позволяют извлекать выгоду из проверок во время компиляции, даже когда данные

представлены в виде строковых карт.

440

Приложение B

B.3. Типизированные геттеры для карт

Типизированный подход к геттеру применим в статически типизированных языках, которые поддерживают универсальные типы, такие как Java и C#. В этом разделе

мы проиллюстрируем типизированный подход к геттеру в Java.

B.3.1. Доступ к невложенным полям карты

с помощью типизированных геттеров

Как и в предыдущих разделах, мы будем использовать представление двух книг

«Watchmen» и «7 Habits of Highly Effective People» на языке Java в виде строковых

карт. В следующем листинге показаны карты, значения которых имеют тип Object.

Листинг B.15. Две книги, представленные в виде карт

Map watchmenMap = Map.of(

"isbn", "978-1779501127",

"title", "Watchmen",

"publicationYear", 1987

);

Map sevenHabitsMap = Map.of(

"isbn", "978-1982137274",

"title", "7 Habits of Highly Effective People",

"publicationYear", 2020

);

Идея типизированных геттеров заключается в создании универсального объекта.

Затем этот объект будет содержать информацию об:

имени поля;

типе значения поля.

Теперь мы можем использовать этот объект на карте строк, чтобы получить типи-зированное значение поля на карте. Например, в листинге B.16 есть типизированный геттер с именем TITLE, который извлекает значение поля с именем title в виде

строки. Реализация типизированного геттера приведена в листинге B.17.

Листинг B.16. Доступ к полям карты с помощью типизированного геттера

Getter<String> TITLE = new Getter("title");

TITLE.get(watchmenMap).toUpperCase();

// → "WATCHMEN"

Листинг B.17. Реализация типизированного геттера

class Getter <T> {

private String key;

public <T> Getter (String k) {

Обобщенный доступ к данным в статически типизированных языках

441

this.key = k;

}

public T get (Map m) {

return (T)(DynamicAccess.get(m, key));

}

}

СОВЕТ. Типизированные геттеры являются универсальными объектами. В отличие от

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

В некотором смысле типизированные геттеры поддерживают проверку во время

компиляции и автозаполнение. Если имя введенного геттера TITLE написано

с ошибкой, компилятор выдает ошибку. Ввод первых нескольких букв TITLE в IDE

обеспечивает автоматическое заполнение символа введенного геттера. Однако

когда вы создаете экземпляр типизированного геттера, имя поля должно быть

передано в виде строки, и ни проверки во время компиляции, ни автозаполнение

недоступны. Сопоставление по списку карт с помощью типизированного геттера

довольно просто, как вы можете видеть в следующем листинге.

Листинг B.18. Отображение поверх списка карт с помощью типизированного геттера

var books = List.of(watchmenMap, sevenHabitsMap);

books.stream()

.map(x -> TITLE.get(x))

.map(x -> x.toUpperCase())

.collect(Collectors.toList())

// → ["WATCHMEN", "7 HABITS OF HIGHLY EFFECTIVE PEOPLE"]

B.3.2. Доступ к вложенным полям карты

с помощью типизированных геттеров

Подход с типизированным геттером хорошо распространяется на вложенные поля

карты. Как и в разделе получения значений, предположим, что результаты поиска, отраженные в листинге B.19, представлены в виде карты строк, где:

ключи — это книжные ISBN;

значения — это книжные данные, представленные в виде карт строк, как в предыдущем разделе.

Листинг B.19. Результаты поиска, представленные в виде карты

Map searchResultsMap = Map.of(

"978-1779501127", Map.of(

"isbn", "978-1779501127",

442

Приложение B

"title", "Watchmen",

"publicationYear", 1987

),

"978-1982137274", Map.of(

"isbn", "978-1982137274",

"title", "7 Habits of Highly Effective People",

"publicationYear", 2020

)

);

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

Листинг B.20. Вложенный типизированный геттер

class Getter <T> {

private List<String> path;

private String key;

private boolean nested;

public <T> Getter (List<String> path) {

this.path = path;

nested = true;

}

public <T> Getter (String k) {

this.key = k;

nested = false;

}

public T get (Map m) {

if(nested) {

return (T)(DynamicAccess.get(m, path));

}

return (T)(DynamicAccess.get(m, key));

}

}

Вложенные поля карты обрабатываются с помощью типизированных геттеров без

какого-либо приведения типов. В следующем листинге пример.

Листинг B.21. Доступ к полям вложенной карты

с помощью типизированного геттера

var informationPath = List.of("978-1779501127",

"title");

Обобщенный доступ к данным в статически типизированных языках

443

Getter<String> NESTED_TITLE = new Getter(informationPath);

NESTED_TITLE.get(searchResultsMap).toUpperCase();

// → "WATCHMEN"

Зачем использовать типизированные геттеры? Типизированные геттеры обеспечивают несколько преимуществ:

не требуется приведение типов;

нет необходимости в реализации геттера для каждого типа;

проверка времени компиляции во время использования;

автозаполнение во время использования.

Однако во время создания доступ к полям карты осуществляется как к строкам.

В следующем разделе показано, как обеспечить обобщенный доступ, когда данные

представлены не в виде сопоставлений строк, а в виде классов.

B.4. Обобщенный доступ к членам класса

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

доступ к данным.

ПРИМЕЧАНИЕ. Подход общего доступа к членам класса применим в языках со статическим типом, которые поддерживают отражение, таких как Java и C#. Этот раздел иллюстрирует подход в Java.

B.4.1. Обобщенный доступ

к не вложенным членам класса

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

в виде классов только с элементами данных, обеспечивая обобщенный доступ

к членам класса через отражение. Этот подход интересен тем, что требуется доступ

только к данным для чтения. Однако при создании новых версий данных или добавлении новых полей данных лучше представлять данные с помощью карт, как

в части 1 этой книги.

ПРИМЕЧАНИЕ. Подход, представленный в этом разделе, применим только для доступа

к данным для чтения.

Вот несколько рекомендаций для того, чтобы представить книгу как класс. Убедитесь, что:

класс содержит только элементы данных (без методов);

элементы являются общедоступными;

элементы являются неизменяемыми;

методы hashcode(), equals() и tostring() реализованы должным образом.

444

Приложение B

Например, в Java пометьте элементы как public и final, как в листинге B.22. В листинге реализация методов hashCode(), equals() и toString() опущена для простоты.

Листинг B.22. Представление книг классом

public class BookData {

public final String isbn;

public final String title;

public final Integer publicationYear;

public BookData (

String isbn,

String title,

Integer publicationYear) {

this.isbn = isbn;

this.title = title;

this.publicationYear = publicationYear;

}

public boolean equals(Object o) {

// Опущено для простоты

}

public int hashCode() {

// Опущено для простоты

}

public String toString() {

// Опущено для простоты

}

}

Начиная с Java 14, существует более простой способ представления данных с использованием записей данных (http://mng.bz/q2q2), как показано в листинге B.23.

Записи данных обеспечивают:

частные поля final;

общедоступные средства доступа для чтения;

открытый конструктор, сигнатура которого получена из списка компонентов

записи;

реализации методов equals() и hashcode(), которые указывают, что две записи

равны, если они одного типа и их компоненты записи равны;

реализацию tostring(), которая включает строковое представление компонентов

записи с их именами.

Листинг B.23. Представление книг записью

public record BookData (String isbn,

String title,

Обобщенный доступ к данным в статически типизированных языках

445

Integer publicationYear

) {}

Создадим два объекта (или записи) для «Watchmen» и «7 Habits of Highly Effective People». В следующем листинге представлен код для двух объектов.

Листинг B.24. Две книжные записи

BookData watchmenRecord = new BookData(

"978-1779501127",

"Watchmen",

1987

);

BookData sevenHabitsRecord = new BookData(

"978-1982137274",

"7 Habits of Highly Effective People",

2020

);

Традиционный способ доступа к элементу данных — через его метод доступа (например, watchmen.title() для получения названия «Watchmen»). Чтобы получить

доступ к элементу данных, имя которого получено из динамического источника, такого как переменная (или как часть полезной нагрузки запроса), нам нужно использовать отражение. В Java доступ к полю title в книге выглядит как фрагмент

кода в следующем листинге.

Листинг B.25. Доступ к элементу данных с помощью отражения

watchmenRecord

.getClass()

.getDeclaredField("title")

.get(watchmenRecord)

// → "watchmen"

В листинге B.26 показано, как отражение может быть использовано для предоставления доступа к любому элементу данных. Список предоставляет реализацию

динамического доступа к не вложенным членам класса.

Листинг B.26. Динамический доступ к не вложенным членам класса

class DynamicAccess {

static Object get(Object o, String k) {

if(o instanceof Map) {

return ((Map)o).get(k);

}

446

Приложение B

try {

return (o.getClass().getDeclaredField(k).get(o));

} catch (IllegalAccessException | NoSuchFieldExceptione) {

return null;

}

}

static String getAsString(Object o, String k) {

return (String)get(o, k);

}

}

Теперь элементы данных доступны с той же универсальностью и динамичностью, что и поля в строковой карте. Код в следующем листинге показывает, как это делается.

Листинг B.27. Динамический доступ к члену класса

((String)DynamicAccess.get(watchmenRecord, "title")).toUpperCase();

// → "WATCHMEN"

Без каких-либо изменений кода геттеры значений (представленные ранее в этом

приложении в контексте отображения строк) теперь могут работать с классами и

записями. В следующем листинге таким образом используются геттеры значений.

Листинг B.28. Доступ к члену класса с помощью геттера

DynamicAccess.getAsString(watchmenRecord, "title").toUpperCase();

// → "WATCHMEN"

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

Листинг B.29. Отображение списка объектов с помощью геттера значений

var books = List.of(watchmenRecord, sevenHabitsRecord);

books.stream()

.map(x -> DynamicAccess.getAsString(x, "title"))

.map(x -> x.toUpperCase())

.collect(Collectors.toList())

// → ["WATCHMEN", "7 HABITS OF HIGHLY EFFECTIVE PEOPLE"]

Типизированные геттеры, которые мы представили ранее в приложении, могут

быть использованы для объектов. Взгляните на следующий листинг, чтобы увидеть, как это делается.

Обобщенный доступ к данным в статически типизированных языках

447

Листинг B.30. Отображение списка объектов с помощью типизированного геттера

var books = List.of(watchmenRecord, sevenHabitsRecord);

books.stream()

.map(x -> TITLE.get(x))

.map(x -> x.toUpperCase())

.collect(Collectors.toList())

// → ["WATCHMEN", "7 HABITS OF HIGHLY EFFECTIVE PEOPLE"]

B.4.2. Обобщенный доступ

к членам вложенного класса

В предыдущем разделе было показано, как обеспечить тот же доступ к данным для

классов, который мы использовали для строковых карт. Это становится действительно мощным, когда мы объединяем классы и карты. Например, листинг B.31

представляет результаты поиска в виде карты, где:

ключи — это книжные ISBN (строки);

значения — это данные книги, представленные в виде классов данных (или записей), как в предыдущем разделе.

Листинг B.31. Результаты поиска, представленные в виде карты записей

Map searchResultsRecords = Map.of(

"978-1779501127", new BookData(

"978-1779501127",

"Watchmen",

1987

),

"978-1982137274", new BookData(

"978-1982137274",

"7 Habits of Highly Effective People",

2020

)

);

Для этой реализации необходимо добавить два дополнительных метода. Нам нужно

объявить статические методы get и getAsString(), которые получают список строк, как показано в следующем листинге.

Листинг B.32. Реализация метода получения значений

для вложенных членов класса

class DynamicAccess {

static Object get(Object o, String k) {

if(o instanceof Map) {

448

Приложение B

return ((Map)o).get(k);

}

try {

return (o.getClass().getDeclaredField(k).get(o));

} catch (IllegalAccessException | NoSuchFieldExceptione) {

return null;

}

}

static Object get(Object o, List<String> p) {

Object v = o;

for (String k : p) {

v = get(v, k);

}

return v;

}

static String getAsString(Object o, String k) {

return (String)get(o, k);

}

static String getAsString(Object o, List<String> p) {

return (String)get(o, p);

}

}

Теперь к элементу данных, вложенному в строковую карту, можно получить доступ

через его информационный путь, как, например, в листинге B.6. В следующем листинге приведен код для доступа к элементу данных с помощью геттера.

Листинг B.33. Доступ к члену класса, вложенному в карту

var informationPath = List.of("978-1779501127", "title"); DynamicClassAccess

.getAsString(searchResultsRecords, informationPath)

.toUpperCase();

// → "WATCHMEN"

Существует второй вид вложенных элементов данных, когда элемент данных сам

является объектом. Например, в листинге B.34 показано, как можно создать поле

bookAttributes из класса BookAttributes, а в листинге B.35 показан пример вложенного класса.

Листинг B.34. Представление атрибутов книги с помощью вложенного класса

public class BookAttributes {

public Integer numberOfPages;

Обобщенный доступ к данным в статически типизированных языках

449

public String language;

public BookAttributes(Integer numberOfPages, String language) {

this.numberOfPages = numberOfPages;

this.language = language;

}

}

public class BookWithAttributes {

public String isbn;

public String title;

public Integer publicationYear;

public BookAttributes attributes;

public Book (

String isbn,

String title,

Integer publicationYear,

Integer numberOfPages,

String language) {

this.isbn = isbn;

this.title = title;

this.publicationYear = publicationYear;

this.attributes = new BookAttributes(numberOfPages,

language);

}

}

Листинг B.35. Экземпляр вложенного класса

BookData sevenHabitsNestedRecord = new BookWithAttributes(

"978-1982137274",

"7 Habits of Highly Effective People",

2020,

432,

"en"

);

Методы получения значений работают без каких-либо изменений во вложенных

элементах данных. Мы можем сделать это с помощью кода в следующем листинге.

Листинг B.36. Доступ к члену вложенного класса с геттера значения

var informationPath = List.of("attributes",

"language");

DynamicClassAccess.getAsString(sevenHabitsNestedRecord, informationPath)

.toUpperCase();

// → "EN"

450

Приложение B

B.4.3. Автоматическая сериализация объектов JSON

Подход, аналогичный тому, который проиллюстрирован в предыдущем разделе, используется библиотеками сериализации JSON, такими как Gson (https://

github.com/google/gson), для того чтобы автоматически сериализовать объекты

в JSON. Gson использует отражение для просмотра членов класса, генерируя JSON-представление значения каждого члена. В листинге B.37 показан пример Gson в действии.

Листинг B.37. Сериализация объекта в формате JSON с помощью Gson

import com.google.gson.*;

var gson = new Gson();

BookData sevenHabitsRecord = new BookData(

"978-1982137274",

"7 Habits of Highly Effective People",

2020

);

System.out.println(gson.toJson(sevenHabitsRecord));

// → {"title":"7 Habits of Highly Effective People", ...}

В листинге B.38 показано, как это также работает с объектами, вложенными в карты. Далее в листинге B.39 приводится пример с объектами, вложенными в objects.

Листинг B.38. JSON-сериализация объектов, вложенных в карту с помощью Gson Map searchResultsRecords = Map.of(

"978-1779501127", new BookData(

"978-1779501127",

"Watchmen",

1987

),

"978-1982137274", new BookData(

"978-1982137274",

"7 Habits of Highly Effective People",

2020

)

);

System.out.println(gson.toJson(searchResultsRecords));

// → {"978-1779501127":{"isbn":"978-1779501127","title":"Watchmen", ...}}

Листинг B.39. Сериализация объекта в формате JSON,

вложенного в объект с помощью Gson

BookData sevenHabitsNestedRecord = new BookWithAttributes(

"978-1982137274",

Обобщенный доступ к данным в статически типизированных языках

451

"7 Habits of Highly Effective People",

2020,

432,

"en"

);

System.out.println(gson.toJson(sevenHabitsNestedRecord));

// → {"isbn":"978-1982137274",

// → "title":"7 Habits of Highly Effective People", ...}

Итоги

В этом приложении представлены различные способы обеспечения общего доступа

к данным на языках программирования со статическим типом. В табл. B.1 обобще-ны преимущества и недостатки каждого подхода. Внедряя методы DOP в свои программы, помните, что данные могут быть представлены либо в виде строковых

карт, либо в виде классов (или записей) и что извлекать выгоду из общего доступа

к данным можно:

через динамические геттеры;

средства получения значений;

типизированные геттеры;

отражения.

Таблица B.1. Различные способы обеспечения общего доступа к данным

в статически типизированных языках программирования

Подход Представление

Преимущества

Недостатки

Динамические

Карта

Обобщенный доступ

Требуется приведение

геттеры

типа

Геттеры значений

Карта

Нет приведения типа

Реализация для каждого

типа

Типизированные

Карта Проверка

использова-

Нет проверки во время

геттеры

ния во время компиля-

компиляции при создании

ции

Отражение Класс Полная

проверка

Не поддается изменению

во время компиляции

452

Приложение B

Приложение C

Дата-ориентированное программирование:

звено в цепи парадигм программирования

Дата-ориентированное программирование (ДОП) берет свое начало в 1950-х годах, когда был изобретен язык программирования Lisp. ДОП основывается на комбинации наилучших практическим приемов, которые можно найти как в функциональном программировании (ФП), так и в объектно-ориентированном программировании (ООП). Однако эта парадигма стала применяться в готовых системах в крупном масштабе только с 2010-х годов, когда разработчики начали эффективно

реализовывать персистентные структуры данных. В этом приложении мы просле-дим основные идеи и открытия, которые со временем привели к появлению ДОП

(рис. C.1).

C.1. Хронология

C.1.1. 1958 год: Lisp

Джону Маккарти, автору Lisp, пришла в голову неординарная идея представить

данные в виде обобщенных неизменяемых списков и изобрести язык, для которого

закономерно создавать списки, и получать доступ к любой части какого-либо списка. Lisp расшифровывается как LISt Processing — «обработка списков».

В некотором смысле списки Lisp являются предками объектных литералов

JavaScript. Идея о том, что разумно представлять данные с помощью обобщенных

структур (принцип ДОП № 2), несомненно, была взята из Lisp.

Основное ограничение списков Lisp заключается в том, что при обновлении списка

нужно создать новую версию путем его клонирования. Это негативно сказывается

на производительности как с точки зрения процессора, так и с точки зрения памяти.

C.1.2. 1981 год: значения и объекты

В короткой и легко читаемой статье под названием «Значения и объекты в языках

программирования» Брюс Макленнан проливает свет на различие между значениями и объектами.

454

Приложение C

Рис. С.1. Хронология ДОП

Вот краткое содержание его работы.

Значения (например, числа) — это вневременные абстракции, для которых кон-цепты обновления, совместного использования и создания экземпляров не имеют никакого значения.

Объекты (например, объект с данными о сотруднике в системе программного

обеспечения для управления персоналом) существуют во времени и, следовательно, могут быть созданы, уничтожены, скопированы, совместно использованы и обновлены.

ПРИМЕЧАНИЕ. Значение термина «объект» в этой статье не совсем такое, как в контексте

ООП.

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

часть кода работала со значениями. Вы можете ознакомиться с полным текстом

этой статьи по адресу http://mng.bz/7WNy.

Дата-ориентированное программирование: звено в цепи парадигм программирования

455

C.1.3. 2000 год: идеальные хеш-деревья

Фил Бэгвелл изобрел структуру данных под названием Hash Array Mapped Trie (HAMT). В статье «Идеальные хеш-деревья» он использовал HAMT для реализации хеш-карт с почти идеальными характеристиками с точки зрения как вычислений, так и использования памяти. Как мы проиллюстрировали в главе 9, HAMT и

идеальные хеш-деревья являются основой эффективных персистентных структур

данных. Пройдите по ссылке https://lampwww.epfl.ch/papers/idealhashtrees.pdf, чтобы прочитать его статью целиком.

C.1.4. 2006 год: «Из ямы со смолой»

В своей статье «Из ямы со смолой» («Out of the Tar Pit») Бен Мозли и Питер Маркс

утверждают, что сложность является единственным серьезным препятствием при

разработке крупномасштабных программных систем. В контексте их статьи сложность означает «то, что делает большие системы трудными для понимания».

Основная идея авторов заключается в том, что в основном элементы сложности

программных систем не закономерны, а случайны: сложность возникает не из-за

проблемы, которую мы должны решить, а из-за программных конструктов, которые

мы используем для решения проблемы. Мозли и Маркс предлагают различные способы понижения сложности программных систем.

В каком-то смысле ДОП — это способ вытащить себя из «ямы со смолой».

См. http://mng.bz/mxq2, чтобы прочитать эту работу полностью.

C.1.5. 2007 год: Clojure

Рич Хикки, эксперт по ООП, изобрел Clojure, чтобы упростить разработку информационных систем в масштабе. Хикки резюмирует основную суть Clojure такой

фразой: «Просто используй карты!» Под картами он подразумевает неизменяемые

карты, которыми можно эффективно манипулировать с помощью обобщенных

функций. Эти карты были реализованы с использованием структур данных, пред-ставленных Филом Бэгвеллом в статье «Идеальные хеш-деревья».

Язык Clojure был главным источником вдохновения для ДОП. В некотором смысле

эта книга представляет собой формализацию основополагающих принципов Clojure и того, как их применять в других языках программирования.

C.1.6. 2009 год: неизменяемость для всех

Эффективная реализация персистентных структур данных из Clojure была привле-кательна для разработчиков и других языков программирования. В 2009 году эти

структуры были перенесены на Scala. Со временем они были перенесены и на другие языки программирования такими организациями, как Facebook (создание

Immutable.js), а также отдельными авторами, такими как Глен Питерсон (создание

Paguro для Java). В настоящее время ДОП применимо практически на любом языке

программирования!

456

Приложение C

C.2. Принципы ДОП как наилучший подход

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

языках программирования. Инновация ДОП заключается в объединении этих

принципов в единое целое. В этом разделе мы рассмотрим каждый из четырех

принципов ДОП в более широком контексте.

C.2.1. Принцип № 1: отделяйте код от данных

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

без состояния, получающие в качестве явного аргумента данные, которыми они

манипулируют.

С годами это разногласие было улажено, поскольку в ФП стало возможным писать

функции с сохранением состояния, получающие данные, инкапсулированные

в лексическую область действия этих функций (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures). Более того, в языки ООП, такие как Java и C#, была добавлена поддержка анонимных функций (лямбда-функций).

C.2.2. Принцип № 2: представляйте данные

с помощью обобщенных структур

Одним из главных нововведений JavaScript, когда он был выпущен в декабре

1995 года, была возможность простого создания хеш-карт и управления ими с помощью объектных литералов. Растущая с годами популярность JavaScript как повсеместно используемого языка (интерфейсы, серверные приложения и рабочий

стол) повлияла на сообщество разработчиков, и везде, где есть возможность, они

начали представлять данные с помощью хеш-карт. Это более естественно в языках

программирования с динамической типизацией, но, как мы видели в приложении В, применимо также и в статически типизированных языках.

C.2.3. Принцип № 3: данные неизменяемы

Неизменяемость данных считается наилучшим подходом, поскольку делает поведение нашей программы более предсказуемым. Например, в книге «Эффективный

Java» (O'Reilly, 2017; http://mng.bz/5K81), Джошуа Блох приводит «минимизацию

изменяемости» в качестве одного из лучших приемов Java. У Алана Кэя, которого

многие считают изобретателем ООП, есть известная цитата о ценности неизменяемости:

Программисты, делайте, что хотите, только не вмешивайтесь во внутреннее

состояние, даже если оно представлено образно. Вместо этого объекты

Дата-ориентированное программирование: звено в цепи парадигм программирования

457

должны быть представлены как локации поведения более высокого уровня, более подходящие для использования в качестве динамических компонентов.

...К сожалению, большая часть того, что сегодня называется «объектно-ориентированным программированием» — это обыкновенное старомодное программирование, только с более вычурными конструктами. Многие программы

перегружены операциями «якобы присваивания», которые теперь выполняются

более дорогостоящими дополнительными процедурами (Алан К. Кей, «Начало

истории Smalltalk», 1993).

К сожалению, до 2007 года и до внедрения эффективных персистентных структур

данных в Clojure неизменяемость не была неприменима для масштабируемых готовых приложений. Как мы упоминали в главе 9, в настоящее время эффективные

персистентные структуры данных доступны в большинстве языков программирования. Краткая информация о них дана в табл. С.1.

Таблица С.1. Библиотеки персистентных структур данных

Язык Библиотека

Java Paguro

(https://github.com/GlenKPeterson/Paguro)

C#

Предоставляется языком (http://mng.bz/y4Ke)

JavaScript

Immutable.js (https://immutable-js.com/)

Python Pyrsistent

(https://github.com/tobgu/pyrsistent)

Ruby Hamster

(https://github.com/hamstergem/hamster)

Кроме того, многие языки нативно предоставляют поддержку объектов, доступных

только для чтения. В Java 14 были добавлены классы записей (http://mng.bz/q2q2).

В C# 9 был введен тип record. Был предложен ECMAScript, проект поддержки неизменяемых записей и кортежей в JavaScript (https://github.com/tc39/proposal-record-tuple). Наконец, в Python 3.7 были введены неизменяемые классы данных

(https://docs.python.org/3/library/dataclasses.html).

C.2.4. Принцип № 4: отделяйте схему данных

от представления данных

Одно из наиболее яростных критических замечаний в адрес динамически типизированных языков программирования было связано с нехваткой валидации данных.

В ответ на эти упреки динамически типизированные языки обычно напоминали

о том, что вместо безопасности данных они предоставляют гибкость данных. После

разработки языков для схем данных, таких как JSON Schema (https://jsonschema.org), стало естественным валидировать данные, даже если они представлены в виде хеш-карт. Как мы видели в главах 7 и 12, валидация данных не только возможна, но и

в некотором смысле более эффективна, чем при представлении данных с помощью

классов.

458

Приложение C

C.3. ДОП и другие парадигмы,

связанные с данными

В этом разделе мы поговорим о различиях между ДОП и двумя другими парадиг-мами программирования, названия которых также содержат слово «дата»: дата-ориентированная разработка (англ. data-oriented design) и дата-управляемое программирование (англ. data-driven programming).

В информатике есть только две сложные вещи: инвалидация кеша и присвоение

названий (Фил Карлтон).

Каждая парадигма ставит свою собственную цель и преследует ее, сосредоточива-ясь на разных аспектах данных. В табл. С.2 кратко изложены цели, и мы подробнее

рассмотрим каждую из этих парадигм следующих подразделах.

Таблица С.2 Парадигмы, связанные с данными: цели и избранный аспект данных

Парадигма Цель

На каком аспекте данных

фокусируется

Дата-ориентированная

Повышение производительности

Макет данных

разработка

Дата-управляемое

Увеличение понятности

Описанное данными

программирование

поведение

Дата-ориентированное

Понижение сложности

Представление данных

программирование

C.3.1. Дата-ориентированная разработка

Дата-ориентированная разработка (ДОР) — это подход к оптимизации программ, основанный на эффективном использовании кеша процессора. Такой метод используется в основном при разработке видеоигр. ДОР фокусируется на макете данных, разделении и сортировке полей в зависимости от того, когда они необходимы, и заставляет нас думать о трансформациях данных. В таком контексте важно то, как данные хранятся в памяти. Целью этой парадигмы является повышение производительности системы.

C.3.2. Дата-управляемое программирование

Дата-управляемое программирование (ДУП) содержит в себе идею о том, чтобы на

основе описательных данных создавать языки, специфичные для предметных областей (англ. domain-specific language, DSL). Это ветвь декларативного программирования. Здесь важно описать поведение программы в терминах данных. Цель этой

парадигмы — сделать код более понятным и снизить появление багов, связанных

с ошибками в реализации ожидаемого поведения программы.

Дата-ориентированное программирование: звено в цепи парадигм программирования

459

C.3.3. Дата-ориентированное программирование (ДОП)

Как мы проиллюстрировали во всей нашей книге, ДОП — это парадигма, которая

относится к системным данным как к привилегированным гражданам. Данные

представлены обобщенными неизменяемыми структурами, такими как карты и векторы, управляют которыми обобщенные функции, такие как map, filter, select, group, sort и т. д. Для ДОП важно представление данных программой. Цель этой

парадигмы состоит в том, чтобы понизить сложность системы.

Итоги

В этом приложении мы рассмотрели идеи и тенденции, которые послужили вдох-новением для создания ДОП. Мы рассмотрели открытия, которые сделали возможным применение ДОП на большинстве языков программирования для разработки

готовых систем в масштабе.

460

Приложение C

Приложение D

Ссылки на Lodash

На протяжении всей книги мы использовали Lodash (https://lodash.com/), чтобы

проиллюстрировать, как манипулировать данными с помощью универсальных

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

кода.

Более того, мы использовали Lodash FP (https://github.com/lodash/lodash/wiki/

FPGuide) для манипулирования данными без их изменения. По умолчанию порядок аргументов в неизменяемых функциях перемешивается. Код в листинге D.1

необходим при настройке Lodash, чтобы гарантировать, что сигнатура неизменяемых функций точно такая же, как и у изменяемых функций.

Листинг D.1. Настройка неизменяемых функций

_ = fp.convert({

"cap": false,

"curry": false,

"fixed": false,

"immutable": true,

"rearg": false

});

В этом коротком приложении перечислены 28 функций Lodash, используемых

в книге, чтобы помочь вам, если вы смотрите на фрагмент кода в книге, который

использует функцию Lodash, которую вы хотите понять. Функции разделены на три

категории:

функции для карт в табл. D.1;

функции для массивов в табл. D.2;

функция для коллекций (как массивов, так и карт) в табл. D.3.

Каждая таблица состоит из трех столбцов:

функция показывает функцию с ее сигнатурой;

описание содержит краткое описание функции;

глава — это номер главы, в которой функция появляется впервые.

462

Приложение D

Таблица D.1. Функции Lodash для карт

Функция Описание

Глава

at(map, [paths])

Создает массив значений, соответствующих paths карты

10

get(map, path)

Получает значение в path карты

3

has(map, path)

Проверяет, есть ли на карте поле в path

3

merge(mapA, mapB)

Создает карту, полученную в результате рекурсивного

3

слияния между mapA и mapB

omit(map, [paths])

Создает карту, состоящую из полей карты, которых нет

10

в paths

set(map, path, value)

Создает карту с теми же полями, что и map, но с добавлени-

4

ем поля <path, value>

values(map)

Создает массив значений map

3

Таблица D.2. Функции Lodash для массивов

Функция Описание

Глава

concat(arrA, arrB)

Создает новый массив, объединяющий arrA и arrB

5

flatten(arr)

Выравнивает arr на один уровень в глубину

14

intersection(arrA, arrB)

Создает массив уникальных значений как в arrA, так и

5

в arrB

nth(arr, n)

Получает элемент с индексом n в arr

10

sum(arr)

Вычисляет сумму значений в arr

14

union(arrA, arrB)

Создает массив уникальных значений из arrA и arrB

5

uniq(arr)

Создает массив уникальных значений из arr

14

Таблица D.3. Функции Lodash для коллекций (как массивов, так и карт) Функция Описание

Глава

every(coll, pred)

Проверяет, возвращает ли pred значение true для всех

14

элементов coll

filter(coll, pred)

Итерирует элементы coll, возвращая массив всех

3

элементов, для которых pred возвращает true

find(coll, pred)

Итерирует элементы coll, возвращая первый элемент,

15

для которого pred возвращает true

forEach(coll, f)

Перебирает элементы массива и вызывает f для каждо-

14

го элемента

groupBy(coll, f)

Создает карту, состоящую из ключей, сгенерированных

10

по результатам выполнения каждого элемента coll

через f. Соответствующее значение для каждого ключа

представляет собой массив элементов, отвечающих

за генерацию ключа

Ссылки на Lodash

463

Таблица D.3 (окончание)

Функция Описание

Глава

isEmpty(coll)

Проверяет, пуст ли coll

5

keyBy(coll, f)

Создает карту, состоящую из ключей, сгенерированных

11

по результатам выполнения каждого элемента coll

через f. Соответствующее значение для каждого клю-

ча — это последний элемент, отвечающий за генерацию

ключа

map(coll, f)

Создает массив значений, запуская каждый элемент

3

в coll через f

reduce(coll, f, initVal)

Сокращает coll до значения, которое является накоп-

5

ленным результатом выполнения каждого элемента

в coll через f, где каждому последующему вызову

предоставляется возвращаемое значение предыдущего

size(coll)

Получает размер coll

13

sortBy(coll, f)

Создает массив элементов, отсортированных в порядке

14

возрастания по результатам запуска каждого coll

в столбце через f

isEqual(collA, collB)

Выполняет глубокое сравнение между collA и collB

6

isArray(coll)

Проверяет, является ли coll массивом

5

isObject(coll)

Проверяет, является ли coll коллекцией

5

Назад: Итоги
На главную: Предисловие