Книга: Дата-ориентированное программирование: Пер. с англ.
Назад: Глава 3. Основные манипуляции с данными
Дальше: 7.3. Гибкость и строгость схемы

Листинг 5.2. Пользовательская реализация _.reduce

function reduce(coll, f, initVal) {

var currentRes = initVal;

for (var i = 0; i < coll.length; i++) { ❶

currentRes = f(currentRes, coll[i])

}

return currentRes;

}

❶ Можно использовать forEach вместо цикла for.

Убедившись, что код Тео работает так, как положено (см. листинг 5.3), Джо начинает гордиться Тео. Похоже, ученик схватывает быстрее, чем он ожидал.

Листинг 5.3. Тестирование пользовательской реализации reduce()

reduce([1, 2, 3], function(res, elem) {

return res + elem;

}, 0);

// → 6

Джо: Молодец!

5.4. Структурная разница

ПРИМЕЧАНИЕ. В этом разделе рассматривается реализация алгоритма структурной

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

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

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

Тео: Как вычислить разницу между множеством версий состояния системы?

Джо: Это самая сложная часть алгоритма согласования. Нам нужно имплементировать алгоритм структурной разницы для хеш-карт.

Тео: В каком смысле разница является структурной?

Джо: Алгоритм структурной разницы рассматривает структуру хеш-карт и игнорирует порядок полей.

Тео: Не могли бы вы привести мне пример?

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

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

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

На доске, — подумал Тео, — скоро совсем не останется места.

132

Часть 1. Гибкость

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

Вид

Первая карта

Вторая карта

Разница

Замена

{"a": 1}

{"a": 2}

{"a": 2}

Добавление

{"a": 1}

{"a": 1, "b": 2}

{"b": 2}

Удаление

{"a": 1, "b": 2}

{"a": 1}

Не поддерживается

Тео: Насколько я вижу, порядок расположения карт имеет большое значение. А как

насчет вложенных полей?

Джо: Идея та же, но вложения делают ее более трудной для понимания.

Джо изменяет несколько столбцов в табл. 5.3. Закончив, он показывает Тео вложенные поля в табл. 5.4.

Таблица 5.4. Виды структурных разниц для карт с вложенными полями

Вид

Первая карта

Вторая карта

Разница

Замена

{

{

{

"a": {

"a": {

"a": {

"x": 1

"x": 2

"x": 2

}

}

}

}

}

}

Добавление

{

{

{

"a": {

"a": {

"a": {

"x": 1

"x": 1,

"y": 2

}

"y": 2,

}

}

}

}

}

Удаление

{

{

Не поддерживается

"a": {

"a": {

"x": 1,

"y": 2

"y": 2,

}

}

}

}

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

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

Тео: Действительно, это труднее понять. А как насчет массивов?

Джо: Мы сравниваем элементы массивов по порядку: если они равны, разница равна null; если они различаются, разница равна значению второго массива.

Джо подытоживает виды разниц еще в одной таблице на доске. Тео смотрит на результат в табл. 5.5.

Глава 5. Основы контроля конкурентности

133

Таблица 5.5. Виды структурных разниц для массивов без вложенных элементов

Вид Первый

массив

Второй

массив

Разница

Замена

[1] [2] [2]

Добавление [1]

[1,

2]

[null, 2]

Удаление

[1, 2]

[1]

Не поддерживается

Тео: Такое использование null выглядит странно, но в целом сойдет. Сложно ли

имплементировать алгоритм структурной разницы?

Джо: Определенно! Потребовалось хорошенько позаниматься умственной гимна-стикой, чтобы придумать эти 30 строк кода.

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

Листинг 5.4. Имплементация структурной разницы

function diffObjects(data1, data2) {

var emptyObject = _.isArray(data1) ? [] : {}; ❶

if(data1 == data2) {

return emptyObject;

}

var keys = _.union(_.keys(data1), _.keys(data2)); ❷

return _.reduce(keys,

function (acc, k) {

var res = diff(

_.get(data1, k),

_.get(data2, k));

if((_.isObject(res) && _.isEmpty(res)) || ❸

(res == "no-diff")) { ❺

return acc;

}

return _.set(acc, [k], res);

},

emptyObject);

}

function diff(data1, data2) {

if(_.isObject(data1) && _.isObject(data2)) {

return diffObjects(data1, data2);

}

if(data1 !== data2) {

return data2;

}

134

Часть 1. Гибкость

return "no-diff"; ❺

}

❶ .isArray проверяет, является ли его аргумент массивом.

❷ _.union создает массив уникальных значений из двух массивов (похоже на объединение

двух наборов в математике).

❸ _.isObject проверяет, является ли его аргумент коллекцией (либо картой, либо массивом).

❹ _.isEmpty проверяет, является ли его аргумент пустой коллекцией.

❺ “no-diff” отмечает, что два значения совпадают.

Тео: Ого! Здесь есть рекурсия внутри сокращения! Я уверен, что Дейву такое

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

сейчас. Давайте сосредоточимся на том, что он делает, а не на том, как он это

делает.

Чтобы ознакомиться с алгоритмом структурной разницы, Тео запускает алгоритм

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

Тео печатают все более сложные примеры, его мысли сфокусировались на сфере

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

Листинг 5.5. Пример использования структурной разницы

var data1 = {

"a": {

"x": 1,

"y": [2, 3],

"z": 4

}

};

var data2 = {

"a": {

"x": 2,

"y": [2, 4],

"z": 4

}

}

diff(data1, data2);

//{

// "a": {

// "x": 2,

// "y": [

// undefined,

// 4

// ]

// }

//}

Глава 5. Основы контроля конкурентности

135

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

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

Тео: Что вы имеете в виду?

Джо: Когда применяется структурное совместное использование, большинство

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

СОВЕТ. Вычисление разницы между двумя версиями состояния эффективно, потому

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

Тео: Это еще одно преимущество неизменяемых данных... Я хочу посмотреть, как

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

с крошечной библиотеки без пользователей, а также каталога с единственной

книгой — «Хранители».

Листинг 5.6. Данные для крошечной библиотеки

var library = {

"catalog": {

"booksByIsbn": {

"978-1779501127": {

"isbn": "978-1779501127",

"title": "Watchmen",

"publicationYear": 1987,

"authorIds": ["alan-moore", "dave-gibbons"]

}

},

"authorsById": {

"alan-moore": {

"name": "Alan Moore",

"bookIsbns": ["978-1779501127"]

},

"dave-gibbons": {

"name": "Dave Gibbons",

"bookIsbns": ["978-1779501127"]

}

}

}

};

136

Часть 1. Гибкость

Джо: Я предлагаю начать с неконфликтующих изменений. А вы что предлагаете?

Тео: Предлагаю изменение, которое обновляет год издания «Хранителей», и изменение, которое обновляет и название, и имя автора «Хранителей».

На своем ноутбуке Тео создает три версии библиотеки. Он показывает Джо свой

код, в котором одно изменение обновляет год издания «Хранителей», а другое —

название и имя автора комикса.

Листинг 5.7. Два неконфликтующих изменения

var previous = library;

var next = _.set(

library,

["catalog", "booksByIsbn", "978-1779501127", "publicationYear"], 1986);

var libraryWithUpdatedTitle = _.set(

library,

["catalog", "booksByIsbn", "978-1779501127", "title"],

"The Watchmen");

var current = _.set(

libraryWithUpdatedTitle,

["catalog", "authorsById", "dave-gibbons", "name"],

"David Chester Gibbons");

Тео: Интересно узнать, как выглядит разница между previous и current.

Джо: Запустите код, и вы увидите.

Тео запускает фрагменты кода, чтобы узнать структурную разницу между previous и next, а также структурную разницу между previous и current. Удовлетворив свое

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

Листинг 5.8. Структурная разница между картами с одним отличием

diff(previous, next);

//{

// "catalog": {

// "booksByIsbn": {

// "978-1779501127": {

// "publicationYear": 1986

// }

// }

// }

//}

Глава 5. Основы контроля конкурентности

137

Листинг 5.9. Структурная разница между картами с двумя отличиями

diff(previous, current);

//{

// "authorsById": {

// "dave-gibbons": {

// "name": "David Chester Gibbons",

// }

// },

// "catalog": {

// "booksByIsbn": {

// "978-1779501127": {

// "title": "The Watchmen"

// }

// }

// }

//}

//

Джо: Подскажите, пожалуйста, информационный путь для одного поля в структурной разнице между previous и next?

Тео: Вот он: ["catalog", "booksByIsbn", "978-1779501127", "publicationYear"].

Джо: Точно. А каковы информационные пути полей в структурных разницах между previous и next?

Тео: Вот они: ["catalog", "booksByIsbn", "978-1779501127", "title"] для названия

книги и ["authorsById", "dave-gibbons", "name"] для имени автора.

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

Тео: Нам нужно проверить, есть ли у них общий информационный путь.

Джо: Верно! Если это так, значит, изменения конфликтуют.

Тео: Но я понятия не имею, как написать код, который извлекает информационные

пути вложенной карты.

Джо: Повторюсь, это непростой код, который включает в себя рекурсию внутри

reduce. Сейчас я загружу еще один фрагмент кода из своего репозитория и покажу его вам.

Листинг 5.10. Вычисление информационных путей (вложенной) карты

function informationPaths (obj, path = []) {

return _.reduce(obj,

function(acc, v, k) {

if (_.isObject(v)) {

return _.concat(acc,

138

Часть 1. Гибкость

informationPaths(v,

_.concat(path, k)));

}

return _.concat(acc, [_.concat(path, k)]);

},

[]);

}

Тео: Давайте посмотрим, работает ли ваш код со структурно разными изменениями

так, как ожидалось.

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

информационные пути структурной разницы между previous и current.

Листинг 5.11. Поля, которые отличаются в previous и next

informationPaths(diff(previous, next));

// → ["catalog.booksByIsbn.978-1779501127.publicationYear"]

Листинг 5.12. Поля, которые отличаются в previous и current

informationPaths(diff(previous, current));

// [

// [

// "catalog",

// "booksByIsbn",

// "978-1779501127",

// "title"

// ],

// [

// "authorsById",

// "dave-gibbons",

// "name"

// ]

//]

Тео: Славно! Я предполагаю, что в Lodash есть функция, которая проверяет, есть

ли у двух массивов общий элемент.

Джо: Почти так. Есть метод _.intersection, который возвращает массив уникальных значений, которые находятся в двух заданных массивах. Однако для нашей

цели нам нужно проверить, является ли пересечение пустым. Посмотрите на

этот пример (листинг 5.13).

Глава 5. Основы контроля конкурентности

139

Листинг 5.13. Проверка того, имеют ли две карты разниц

общий информационный путь

function havePathInCommon(diff1, diff2) {

return !_.isEmpty(_.intersection(informationPaths(diff1),

informationPaths(diff2)));

}

Тео: Вы уже говорили, что в случае неконфликтующих изменений можно безопасно пропатчить изменения, вызванные переходом от previous к next и в current.

Как это реализовать?

Джо: Нужно выполнить рекурсивное слияние между current и разницей между

previous и next.

Тео: А в Lodash есть неизменяемая версия рекурсивного слияния?

Джо: Есть, вот еще один пример. Взгляните на этот код.

Листинг 5.14. Применение патча

_.merge(current, (diff(previous, next)));

//{

// "authorsById": {

// "dave-gibbons": {

// "name": "David Chester Gibbons"

// }

// },

// "catalog": {

// "authorsById": {

// "alan-moore": {

// "bookIsbns": ["978-1779501127"]

// "name": "Alan Moore"

// },

// "dave-gibbons": {

// "bookIsbns": ["978-1779501127"],

// "name": "Dave Gibbons"

// },

// },

// "booksByIsbn": {

// "978-1779501127": {

// "authorIds": ["alan-moore", "dave-gibbons"],

// "isbn": "978-1779501127",

// "publicationYear": 1986,

// "title": "The Watchmen"

// }

// }

// }

//}

140

Часть 1. Гибкость

Тео: Неужели все так просто?

Джо: Даже очень.

5.5. Имплементация алгоритма согласования

Джо: Теперь у нас на руках все, что нужно для имплементации алгоритма согласования.

Тео: Какие нужны будут изменения?

Джо: Потребуются только изменения в коде SystemState.commit. Вот, посмотрите на

этот пример на моем ноутбуке.

Листинг 5.15. Класс SystemState

class SystemState {

systemData;

get() {

return this.systemData;

}

set(_systemData) {

this.systemData = _systemData;

}

commit(previous, next) {

var nextSystemData = SystemConsistency.reconcile(

this.systemData, ❶

previous,

next);

if(!SystemValidity.validate(previous, nextSystemData)

throw "The system data to be committed is not valid!";

};

this.systemData = nextSystemData;

}

}

❶ Класс SystemConsistency имплементирован в листинге 5.16.

Тео: Как SystemConsistency выполняет согласование?

Джо: Класс SystemConsistency запускает процесс согласования путем сравнения

previous и current. Если они совпадают, то мы производим быструю перемотку

вперед и возвращаемся к next. Посмотрите на этот код для SystemConsistency.

Листинг 5.16. Процесс согласования в действии

class SystemConsistency {

static threeWayMerge(current, previous, next) {

var previousToCurrent = diff(previous, current); ❶

var previousToNext = diff(previous, next);

Глава 5. Основы контроля конкурентности

141

if(havePathInCommon(previousToCurrent, previousToNext)) {

return _.merge(current, previousToNext);

}

throw "Conflicting concurrent mutations.";

}

static reconcile(current, previous, next) {

if(current == previous) {

return next; ❶

}

return SystemConsistency.threeWayMerge(current,

previous,

next);

}

}

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

Тео: Подождите-ка! Почему вы сравниваете previous и current по ссылке? Разве вы

не должны сравнивать их по значению? И будет дороговато сравнивать все листья двух вложенных хеш-карт!

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

знаем, что данные одни и те же.

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

Тео: А что скажете об имплементации алгоритма трехстороннего слияния?

Джо: Когда previous отличается от current, это означает, что были запущены конкурентные изменения. Чтобы определить, существует ли конфликт, мы вычисляем две разницы: разницу между previous и current и разницу между previous и

next. Если пересечение между двумя разницами пустое, это означает, что конфликта нет. Мы можем безопасно пропатчить изменения между previous и next в current.

Тео более подробно рассматривает код для класса SystemConsistency в листинге 5.16. Он пытается понять, является ли код потокобезопасным.

Тео: По-моему, код для класса SystemConsistency не является потокобезопасным!

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

система в классе SystemConsistency, и обновлением состояния в классе systemData, изменение может перекрыть изменения, внесённые на предыдущем шаге.

Джо: Вы совершенно правы! Этот код прекрасно работает в однопоточной среде, такой как JavaScript, где конкурентность обрабатывается с помощью цикла событий. Однако в многопоточной среде код необходимо доработать, чтобы он

был потокобезопасным. Когда-нибудь вам это покажу.

142

Часть 1. Гибкость

ПРИМЕЧАНИЕ. Класс SystemConsistency не является потокобезопасным. Мы сделаем его

таковым в главе 8.

Тео: Кажется, я понял, почему вы назвали это оптимистичным контролем конкурентности. Потому что мы предполагаем, что конфликты происходят не слишком часто. Верно?

Джо: Абсолютно! Интересно, что сказала бы ваш психотерапевт о конфликтах, которые не могут быть разрешены. Есть ли такие случаи, когда пара не может

прийти к согласию?

Тео: Не помню, чтобы она когда-либо допускала такую ситуацию.

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

Итоги

Оптимистичный контроль конкурентности позволяет при изменениях просить

прощения вместо разрешения.

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

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

Оптимистичный контроль конкурентности с неизменяемыми данными очень

эффективен.

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

Мы согласовываем конкурентные изменения примерно так, как Git обрабатывает слияние двух веток: производится либо ускоренная перемотка вперед, либо

трехстороннее слияние.

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

Этап вычисления выполняет их так, как если бы было запущено только одно-единственное изменение.

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

Алгоритм согласования универсален в том смысле, что его можно использовать

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

хеш-карты.

Имплементация алгоритма согласования эффективна, поскольку он использует

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

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

В системе, ориентированной на пользователя, конфликтующие конкурентные

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

Глава 5. Основы контроля конкурентности

143

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

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

общие.

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

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

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

удаления.

Функции Lodash, представленные в этой главе

Функция Описание

concat(arrA, arrB)

Создает новый массив, конкатенируя arrA и arrB

intersection(arrA, arrB)

Создает массив уникальных значений как в arrA, так и в arrB

union(arrA, arrB)

Создает массив уникальных значений из arrA и arrB

find(coll, pred)

Перебирает элементы коллекции coll, возвращая первый

элемент, для которого pred возвращает значение true

isEmpty(coll)

Проверяет, является ли coll пустой

reduce(coll, f, initVal)

Сокращает coll до значения, которое является накопленным

результатом выполнения каждого элемента в coll через f, где

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

возвращаемое значение предыдущего

isArray(coll)

Проверяет, является ли coll массивом

isObject(coll)

Проверяет, является ли coll коллекцией

144

Часть 1. Гибкость

Модульные тесты

Программирование в кофейне

В ЭТОЙ ГЛАВЕ РАССМАТРИВАЮТСЯ

Генерация минимального ввода данных для тестового кейса.

Сравнение выходных данных функции с ожидаемыми выходными

данными.

Рекомендации по качеству и количеству тестовых кейсов.

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

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

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

с ожидаемыми выходными данными. В этой главе мы пишем модульные тесты для

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

6.1. Простота

дата-ориентированных тестовых кейсов

Тео и Джо сидят за большим деревянным столом в углу «La vie est belle», милой

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

теме — модульным тестам. Тео просит у Джо объяснений.

Тео: Являются ли модульные тесты такой простой темой, что мы можем заняться

ею здесь, в кафе?

Джо: Модульные тесты в целом — нет. Но модульные тесты для дата-ориентированного кода — да!

146

Часть 1. Гибкость

Тео: Почему это имеет значение?

Джо: Подавляющее большинство кодовой базы дата-ориентированной системы

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

Тео: Да. Я заметил, что почти все функции, которые мы написали до сих пор, получают данные и возвращают их.

СОВЕТ. Большая часть кода в дата-ориентированной системе связана с манипулированием данными.

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

данных и сравнении выходных данных функции с ожидаемыми выходными

данными.

ШАГИ ТЕСТОВОГО КЕЙСА

1. Сгенерируйте входные данные: dataIn.

2. Сгенерируйте ожидаемый результат: dataOut.

3. Сравните выходные данные функции с ожидаемыми выходными данными: f(dataIn) и dataOut.

Тео: Это все?

Джо: Да. Как вы скоро увидите, в ДОП обычно нет необходимости в фиктивных

функциях.

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

Джо: Сравнивайте поле за полем.

Тео: Рекурсивно?

Джо: Да!

Тео: О нет! Я не могу написать какой-либо рекурсивный код в кафе. Мне нужно

спокойствие моего офиса для такого рода вещей.

Джо: Не волнуйся. В ДОП данные представлены в общем виде. В Lodash есть универсальная функция под названием _.isEqual для рекурсивного сравнения наборов данных. Он работает как с картами, так и с массивами.

Джо открывает свой ноутбук. Он может убедить Тео, выполнив несколько фрагментов кода с _.isEqual, чтобы сравнить равный набор данных с неравным.

Листинг 6.1. Рекурсивное сравнение одинакового набора данных

_.isEqual({

"name": "Alan Moore",

"bookIsbns": ["978-1779501127"]

}, {

"name": "Alan Moore",

Глава 6. Модульные тесты

147

"bookIsbns": ["978-1779501127"]

});

// → true

Листинг 6.2. Рекурсивное сравнение неравнозначного набора данных

_.isEqual({

"name": "Alan Moore",

"bookIsbns": ["978-1779501127"]

}, {

"name": "Alan Moore",

"bookIsbns": ["bad-isbn"]

});

// → false

Тео: Славно!

Джо: Большинство тестовых кейсов в ДОП следуют этому шаблону.

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

Листинг 6.3. Общий шаблон дата-ориентированного тестового кейса

var dataIn = {

// input

};

var dataOut = {

// expected output

};

_.isEqual(f(dataIn), dataOut);

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

данными, несложно.

Тео: Действительно, это похоже на то, чем мы можем заняться в кафе!

6.2. Модульные тесты для кода

манипулирования данными

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

148

Часть 1. Гибкость

Джо: Помните ли вы поток кода реализации поискового запроса?

Тео: Позвольте мне еще раз взглянуть на код, который реализует поисковый запрос.

Тео запускает реализацию поискового запроса на своем ноутбуке. Заметив, что Джо

снова грызет ногти, он быстро проверяет код.

Листинг 6.4. Код, участвующий в реализации поискового запроса

class Catalog {

static authorNames(catalogData, authorIds) {

return _.map(authorIds, function(authorId) {

return _.get(catalogData, ["authorsById", authorId,

"name"]);

});

}

static bookInfo(catalogData, book) {

var bookInfo = {

"title": _.get(book, "title"),

"isbn": _.get(book, "isbn"),

"authorNames": Catalog.authorNames(catalogData,

_.get(book, "authorIds"))

};

return bookInfo;

}

static searchBooksByTitle(catalogData, query) {

var allBooks = _.get(catalogData, "booksByIsbn");

var matchingBooks = _.filter(allBooks, function(book) {

return _.get(book, "title").includes(query);

});

var bookInfos = _.map(matchingBooks, function(book) {

return Catalog.bookInfo(catalogData, book);

});

return bookInfos;

}

}

class Library {

static searchBooksByTitleJSON(libraryData, query) {

var catalogData = _.get(libraryData, "catalog");

var results = Catalog.searchBooksByTitle(catalogData,

query);

var resultsJSON = JSON.stringify(results);

return resultsJSON;

}

}

Глава 6. Модульные тесты

149

6.2.1. Дерево вызовов функций

Официант приносит Тео кофе с молоком, а Джо крепкий эспрессо. Они продолжают свою дискуссию, наслаждаясь кофе.

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

Тео: Что вы подразумеваете под деревом вызовов функций?

Джо: Я нарисую дерево вызовов функций для потока кода Library.

searchBooksByTitleJSON.

Джо ставит свой эспрессо и берет салфетку из автомата. Он осторожно кладет ее на

стол и начинает рисовать. Закончив, он показывает иллюстрацию Тео (рис. 6.1).

Рис. 6.1. Дерево вызовов функций для потока кода поискового запроса

Тео: Славно! Можете ли вы научить меня, как нарисовать такое же дерево вызовов

функций?

Джо: Конечно. Корень дерева — это имя функции, для которой вы рисуете дерево, в нашем случае Library.searchBooksByTitleJSON. Дочерние элементы узла в дереве — это имена функций, вызываемых функцией. Например, если вы снова посмотрите на код для Library.searchBooksByTitleJSON (листинг 6.4), вы увидите, что он вызывает Catalog.searchBooksByTitle, _.get и JSON.stringify.

Тео: Как долго я могу продолжать рекурсивно расширять дерево?

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

дерева; например, функции из Lodash: _.get, _.map и т. д.

Тео: Что делать, если код функции не вызывает никаких других функций?

Джо: Функция, которая не вызывает никакой другой функции, была бы листом

в дереве.

Тео: Как насчет функций, которые вызываются внутри анонимных функций, таких

как Catalog.bookInfo?

150

Часть 1. Гибкость

Джо: Catalog.bookInfo отображается в коде Catalog.searchBooksByTitle. Поэтому он

считается дочерним узлом Catalog.searchBooksByTitle. Тот факт, что он вложен

в анонимную функцию, не имеет значения в контексте дерева вызовов функций.

ПРИМЕЧАНИЕ. Дерево вызовов функций для функции f — это дерево, корень которого

равен f, а дочерние элементы узла g в дереве — это функции, вызываемые g. Листья дерева — это функции, которые не являются частью кодовой базы приложения. Это функции, которые не вызывают никаких других функций.

Тео: Очень здорово визуализировать мой код в виде дерева, но я не понимаю, как

это связано с модульными тестами.

Джо: Дерево вызовов функций подсказывает нам, какое качество и количество

тест-кейсов мы должны написать.

Тео: Как?

Джо: Увидите через мгновение.

6.2.2. Модульные тесты для функций вниз по дереву

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

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

Листинг 6.5. Код Catalog.authorNames

Catalog.authorNames = function (catalogData, authorIds) {

return _.map(authorIds, function(authorId) {

return _.get(catalogData, ["authorsById", authorId, "name"]);

});

};

Тео: Ввод Catalog.authorNames — это catalogData и authorIds. Выходные данные —

это authorNames.

Джо: Не могли бы визуализировать это?

Тео: Конечно.

Теперь очередь Тео брать салфетку. Он рисует небольшой прямоугольник с двумя

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

Джо: Превосходно! Теперь, сколько комбинаций входных данных вы бы включили

в модульный тест для Catalog.authorNames?

Тео: Дайте-ка подумать.

Тео тянется за другой салфеткой. На этот раз он создает таблицу, чтобы собраться

с мыслями (табл. 6.1).

Глава 6. Модульные тесты

151

Рис. 6.2. Визуализация ввода и вывода Catalog.authorNames

Таблица 6.1. Таблица тест-кейсов для Catalog.authorNames

catalogData authorIds

authorNames

Каталог с двумя авторами

Пустой массив

Пустой массив

Каталог с двумя авторами

Массив с одним

Массив с одним именем автора

идентификатором автора

Каталог с двумя авторами

Массив с двумя

Массив с двумя именами авторов

идентификаторами автора

Тео: Для начала у меня был бы catalogData с двумя идентификаторами автора и

вызов Catalog.authorNames с тремя аргументами: пустой массив, массив с одним

идентификатором автора и массив с двумя идентификаторами автора.

Джо: Как бы вы сгенерировали catalogData?

Тео: Точно так же, как мы создавали его раньше.

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

его Джо.

Листинг 6.6. Полная карта catalogData

var catalogData = {

"booksByIsbn": {

"978-1779501127": {

"isbn": "978-1779501127",

"title": "Watchmen",

"publicationYear": 1987,

"authorIds": ["alan-moore", "dave-gibbons"],

"bookItems": [

{

"id": "book-item-1",

"libId": "nyc-central-lib",

"isLent": true

},

{

"id": "book-item-2",

"libId": "nyc-central-lib",

152

Часть 1. Гибкость

"isLent": false

}

]

}

},

"authorsById": {

"alan-moore": {

"name": "Alan Moore",

"bookIsbns": ["978-1779501127"]

},

"dave-gibbons": {

"name": "Dave Gibbons",

"bookIsbns": ["978-1779501127"]

}

}

};

Джо: Вы можете использовать свою большую карту catalogData для модульного

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

Catalog.authorNames. Вы можете избавиться от поля booksByIsbn каталога

catalogData и полей bookIsbns авторов.

Джо удаляет несколько строк из catalogData и получает карту гораздо меньшего

размера. Он показывает доработку Тео.

Листинг 6.7. Минимальная версия catalogData

var catalogData = {

"authorsById": {

"alan-moore": {

"name": "Alan Moore"

},

"dave-gibbons": {

"name": "Dave Gibbons"

}

}

};

Тео: Подождите минутку! Этот catalogData недействителен.

Джо: В ДОП достоверность данных зависит от контекста. В контексте Library.

searchBooksByTitleJSON и Catalog.searchBooksByTitle минимальная версия catalogData действительно недействительна. Однако в контексте Catalog.bookInfo и

Catalog.authorNames это вполне допустимо. Причина в том, что эти две функции

имеют доступ только к полю authorById в catalogData.

СОВЕТ. Достоверность данных зависит от контекста.

Глава 6. Модульные тесты

153

Тео: Почему в тест-кейсе лучше использовать минимальную версию данных?

Джо: По очень простой причине — чем меньше данных, тем легче ими манипулировать.

СОВЕТ. Чем меньше данных, тем легче ими манипулировать.

Тео: Я буду признателен за это, когда напишу модульные тесты!

Джо: Определенно! И последнее, прежде чем мы начнем программировать: как бы

вы проверили, что вывод Catalog.authorNames соответствует ожиданиям?

Тео: Я бы проверил, что значение, возвращаемое Catalog.authorNames, представляет

собой массив с ожидаемыми именами авторов.

Джо: Как бы вы справились со сравнением массивов?

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

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

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

я показал вам ранее (см. листинг 6.1), мы можем рекурсивно сравнить две коллекции данных по значению с помощью _.isEqual из Lodash.

СОВЕТ. Мы можем сравнить вывод и ожидаемый вывод наших функций с помощью

_.isEqual.

Тео: Звучит отлично! Позвольте мне написать тестовые кейсы.

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

вызова функции Catalog.authorNames, обернутого в _.isEqual.

Листинг 6.8. Модульный тест для Catalog.authorNames

var catalogData = {

"authorsById": {

"alan-moore": {

"name": "Alan Moore"

},

"dave-gibbons": {

"name": "Dave Gibbons"

}

}

};

_.isEqual(Catalog.authorNames(catalogData, []), []);

_. isEqual(Catalog.authorNames(

catalogData,

["alan-moore"]),

["Alan Moore"]);

154

Часть 1. Гибкость

_.isEqual(Catalog.authorNames(catalogData, ["alan-moore", "dave-gibbons"]),

["Alan Moore", "Dave Gibbons"]);

Джо: Отличная работа! Можете ли вы придумать больше тестовых кейсов?

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

С минимальными данными каталога и _.isEqual очень легко написать множество тестовых кейсов!

Тео действительно нравится этот вызов. Он создает еще несколько тестовых кейсов, чтобы показать их Джо.

Листинг 6.9. Дополнительные тест-кейсы для Catalog.authorNames

_.isEqual(Catalog.authorNames({}, []), []);

_.isEqual(Catalog.authorNames({}, ["alan-moore"]), [undefined]); _.isEqual(Catalog.authorNames(catalogData, ["alan-moore",

"albert-einstein"]), ["Alan Moore", undefined]); _.isEqual(Catalog.authorNames(catalogData, []), []);

_.isEqual(Catalog.authorNames(catalogData, ["albert-einstein"]),

[undefined]);

Тео: Как запустить эти модульные тесты?

Джо: Используйте предпочтительную тестовую среду.

ПРИМЕЧАНИЕ. Мы не имеем здесь дел с тест-раннерами и тестовыми фреймворками. Мы

занимаемся только логикой текст-кейсов.

6.2.3. Модульные тесты для узлов в дереве

Тео: Мне любопытно посмотреть, как выглядят модульные тесты для верхнего узла

в дереве вызовов функций.

Джо: Конечно. Давайте напишем модульный тест для Catalog.bookInfo. Сколько

тест-кейсов у вас будет для Catalog.bookInfo?

Листинг 6.10. Код Catalog.bookInfo

Catalog.bookInfo = function (catalogData, book) {

return {

"title": _.get(book, "title"),

"isbn": _.get(book, "isbn"),

"authorNames": Catalog.authorNames(catalogData,

_.get(book, "authorIds"))

};

};

Глава 6. Модульные тесты

155

Рис. 6.3. Визуализация ввода и вывода Catalog.bookInfo

Тео еще раз смотрит на код Catalog.bookInfo на своем ноутбуке. Затем, потянув-шись за другой салфеткой, чертит схему ее входа и выхода (рис. 6.3).

Тео: У меня было бы такое же количество тест-кейсов для Catalog.authorNames: книга с одним автором, с двумя авторами, с существующими авторами, с несущест-вующими авторами, с...

Джо: Ну ничего себе! Это не обязательно. Учитывая, что мы уже написали модульные тесты для Catalog.authorNames, нам не нужно снова проверять все кейсы.

Нам просто нужно написать минимальный тест-кейс, чтобы убедиться, что код

работает.

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

Тео: Это имеет смысл.

Джо: Как бы вы написали минимальный тест-кейс для Catalog.bookInfo?

Тео еще раз смотрит на код Catalog.bookInfo (листинг 6.10). Теперь он может ответить на вопрос Джо.

Тео: Я бы использовал те же данные каталога, что и для Catalog.authorNames и записи книги. Я бы проверил, что функция ведет себя так, как ожидалось, сравнив

возвращаемое значение с записью информации о книге, используя _.isEqual.

Позвольте-ка мне показать вам.

Тео требуется немного больше времени, чтобы написать модульный тест. Причина

в том, что и входные, и выходные данные Catalog.authorNames являются записями.

Работать с записью сложнее, чем с массивом строк (как в случае с Catalog.

authorNames). Тео ценит тот факт, что _.isEqual избавляет его от написания кода, сравнивающего свойство карт по свойствам. Закончив, он показывает результат

Джо и берет салфетку, чтобы вытереть лоб.

Листинг 6.11. Модульный тест для Catalog.bookInfo

var catalogData = {

"authorsById": {

"alan-moore": {

"name": "Alan Moore"

},

156

Часть 1. Гибкость

"dave-gibbons": {

"name": "Dave Gibbons"

}

}

};

var book = {

"isbn": "978-1779501127",

"title": "Watchmen",

"publicationYear": 1987,

"authorIds": ["alan-moore", "dave-gibbons"]

};

var expectedResult = {

"authorNames": ["Alan Moore", "Dave Gibbons"],

"isbn": "978-1779501127",

"title": "Watchmen",

};

var result = Catalog.bookInfo(catalogData, book);

_.isEqual(result, expectedResult);

Джо: Идеально! Теперь, как бы вы сравнили тип модульных тестов для Catalog

.bookInfo с модульными тестами для Catalog.authorNames?

Тео: С одной стороны, в модульном тесте Catalog.book-Info есть только один тест-кейс. С другой стороны, данные, задействованные в тест-кейсе, более сложны, чем данные, задействованные в тест-кейсах для Catalog.authorNames.

Джо: Именно так! Функции, которые появляются в глубоком узле дерева вызовов

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

СОВЕТ. Функции, которые отображаются на более низком уровне в дереве вызовов

функций, как правило, содержат менее сложные данные, чем функции, которые отображаются на более высоком уровне в дереве (табл. 6.2).

Таблица 6.2. Корреляция между глубиной функции в дереве вызовов функций

и качеством и количеством тест-кейсов

Глубина дерева

Сложность данных

Количество тест-кейсов

Ниже Выше Ниже

Выше Ниже Выше

Глава 6. Модульные тесты

157

6.3. Модульные тесты для запросов

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

функций, таких как Catalog.bookInfo и Catalog.authorNames. Теперь мы увидим, как

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

к корню дерева.

Джо: Тео, как бы вы написали модульный тест для кода точки входа поискового

запроса?

Чтобы вспомнить подробности, Тео проверяет код на Library.searchBooksByTitleJSON.

Хотя Джо был прав в том, что сегодняшняя тема достаточно проста, чтобы насладиться атмосферой кофейни, этим утром он довольно много программировал.

Листинг 6.12. Код Library.searchBooksByTitleJSON

Library.searchBooksByTitleJSON = function (libraryData, query) {

var catalogData = _.get(libraryData, "catalog");

var results = Catalog.searchBooksByTitle(catalogData, query);

var resultsJSON = JSON.stringify(results);

return resultsJSON;

};

Затем он размышляет о том, как написать модульный тест для этого кода. После

еще одного Ага!-момента теперь он получил это.

Тео: Входными данными для Library.searchBooksByTitleJSON являются данные библиотеки и строка запроса, а выходными данными является строка JSON

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

совпадают.

Рис. 6.4. Ввод и вывод Library.searchBooksByTitleJSON

Джо: Как насчет ожидаемых результатов тестов?

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

совпадает, ожидаемым результатом будет строка JSON с пустым массивом.

Джо: Хм...

Тео: Что?

158

Часть 1. Гибкость

Джо: Мне не нравится твой ответ.

Тео: Почему?

Джо: Потому что твой тест-кейс основан на сравнении строк, а не на сравнении

данных.

Тео: Какое это имеет значение? В конце концов, строки, которые я сравниваю, по-лучены в результате сериализации данных.

Джо: Сравнивать строки JSON по своей сути гораздо сложнее, чем сравнивать данные. Например, две разные строки могут быть сериализацией одного и того же

фрагмента данных.

Тео: О, правда? Как?

Джо: Взгляните на эти две строки. Они представляют собой сериализацию одних

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

порядке, но на самом деле они сериализуют одни и те же данные!

Джо поворачивает свой ноутбук к Тео. Когда Тео смотрит на код, он понимает, что

Джо снова прав.

Листинг 6.13. Две разные строки, которые сериализуют одни и те же данные

var stringA = "{\"title\":\"Watchmen\",\"publicationYear\":1987}"; var stringB = "{\"publicationYear\":1987,\"title\":\"Watchmen\"}"; СОВЕТ. Избегайте использования сравнения строк в модульных тестах для функций, которые работают с данными.

Тео: Ага... Хорошо, что я могу сделать вместо этого?

Джо: Вместо сравнения вывода Library.searchBooksByTitleJSON со строкой вы можете десериализовать вывод и сравнить его с ожидаемыми данными.

Тео: Что вы подразумеваете под десериализацией строки?

Джо: Например, десериализация строки s означает создание фрагмента данных, сериализация которого равна s.

Тео: Есть ли функция Lodash для десериализации строк?

Джо: На самом деле есть встроенная функция JavaScript для десериализации строк; это называется JSON.parse.

Джо достает свой ноутбук и показывает Тео пример десериализации строк. Код иллюстрирует обычное использование JSON.parse.

Листинг 6.14. Пример десериализации строки

var myString = "{\"publicationYear\":1987,\"title\":\"Watchmen\"}"; var myData = JSON.parse(myString);

_.get(myData, "title");

// → "Watchmen"

Глава 6. Модульные тесты

159

Тео: Круто! Позвольте мне попробовать написать модульный тест для Library.

searchBooksByTitleJSON, используя JSON.parse.

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

вводит модульный тест.

Листинг 6.15. Модульный тест для Library.searchBooksByTitleJSON

var libraryData = {

"catalog": {

"booksByIsbn": {

"978-1779501127": {

"isbn": "978-1779501127",

"title": "Watchmen",

"publicationYear": 1987,

"authorIds": ["alan-moore",

"dave-gibbons"]

}

},

"authorsById": {

"alan-moore": {

"name": "Alan Moore",

"bookIsbns": ["978-1779501127"]

},

"dave-gibbons": {

"name": "Dave Gibbons",

"bookIsbns": ["978-1779501127"]

}

}

}

};

var bookInfo = {

"isbn": "978-1779501127",

"title": "Watchmen",

"authorNames": ["Alan Moore",

"Dave Gibbons"]

};

_.isEqual(JSON.parse(Library.searchBooksByTitleJSON(libraryData,

"Watchmen")),

[bookInfo]);

_.isEqual(JSON.parse(Library.searchBooksByTitleJSON(libraryData,

"Batman")),

[]);

Джо: Отличная работа! Думаю, вы готовы перейти к последней части головоломки

и написать модульный тест для Catalog.searchBooksByTitle.

160

Часть 1. Гибкость

Поскольку Тео и Джо уже довольно давно обсуждают модульные тесты, они хотят

еще эспрессо. Они зовут официанта и делают заказ, затем Тео снова смотрит на код

Catalog.searchBooksByTitle.

Листинг 6.16. Код Catalog.searchBooksByTitle

Catalog.searchBooksByTitle = function(catalogData, query) {

var allBooks = _.get(catalogData, "booksByIsbn");

var matchingBooks = _.filter(allBooks, function(book) {

return _.get(book, "title").includes(query);

});

var bookInfos = _.map(matchingBooks, function(book) {

return Catalog.bookInfo(catalogData, book);

});

return bookInfos;

};

Написание модульного теста для Catalog.searchBooksByTitle — более приятное занятие для Тео, чем написание модульного теста для Library.searchBooksByTitleJSON.

Он ценит это по двум причинам:

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

данные;

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

Листинг 6.17. Модульный тест для Catalog.searchBooksByTitle

var catalogData = {

"booksByIsbn": {

"978-1779501127": {

"isbn": "978-1779501127",

"title": "Watchmen",

"publicationYear": 1987,

"authorIds": ["alan-moore",

"dave-gibbons"]

}

},

"authorsById": {

"alan-moore": {

"name": "Alan Moore",

"bookIsbns": ["978-1779501127"]

},

"dave-gibbons": {

"name": "Dave Gibbons",

"bookIsbns": ["978-1779501127"]

}

}

};

Глава 6. Модульные тесты

161

var bookInfo = {

"isbn": "978-1779501127",

"title": "Watchmen",

"authorNames": ["Alan Moore",

"Dave Gibbons"]

};

_.isEqual(Catalog.searchBooksByTitle(catalogData, "Watchmen"), [bookInfo]); _.isEqual(Catalog.searchBooksByTitle(catalogData, "Batman"), []); Джо: Это хорошее начало!

Тео: Я думал, что с меня хватит. Что я пропустил?

Джо: Вы забыли протестировать кейсы, когда строка запроса полностью строчная.

Тео: Вы правы! Позвольте мне быстро добавить еще один тест-кейс.

Менее чем за минуту Тео создает дополнительный тест-кейс и показывает его Джо.

Каково же было разочарование, когда Тео обнаружил, что тест-кейс с watchmen в нижнем регистре провалился!

Листинг 6.18. Дополнительный тест-кейс для Catalog.searchBooksByTitle _.isEqual(Catalog.searchBooksByTitle(catalogData, "watchmen"), [bookInfo]); Джо: Не расстраивайтесь слишком сильно, мой друг. В конце концов, цель модульных тестов — найти ошибки в коде, чтобы вы могли их исправить. Можете ли

вы исправить код Catalog-Data.searchBooksByTitle?

Тео: Конечно. Все, что мне нужно сделать, это перевести строку запроса и название

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

подобное.

Листинг 6.19. Исправленный код Catalog.searchBooksByTitle

Catalog.searchBooksByTitle = function(catalogData, query) {

var allBooks = _.get(catalogData, "booksByIsbn");

var queryLowerCased = query.toLowerCase();❶

var matchingBooks = _.filter(allBooks, function(book) {

return _.get(book, "title")

.toLowerCase()❷

.includes(queryLowerCased);

});

var bookInfos = _.map(matchingBooks, function(book) {

return Catalog.bookInfo(catalogData, book);

});

return bookInfos;

};

❶ Преобразует запрос в нижний регистр.

❷ Преобразует название книги в нижний регистр.

162

Часть 1. Гибкость

После исправления кода Catalog.searchBooksByTitle Тео снова запускает все тест-кейсы. На этот раз все они проходят — какое облегчение!

Листинг 6.20. Дополнительный тест-кейс для Catalog.searchBooksByTitle _.isEqual(Catalog.searchBooksByTitle(catalogData, "watchmen"), [bookInfo]); Джо: Это такое приятное чувство, когда все тест-кейсы пройдены.

Тео: Безусловно.

Джо: Я думаю, что мы написали модульные тесты для всего кода поискового

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

Слава богу, официант только что принес наш заказ.

6.4. Модульные мутационные тесты

Джо: Прежде чем писать модульные мутационные тесты для проверки добавления

члена, давайте нарисуем дерево вызовов функций для System.addMember.

Тео: Это я могу сделать.

Тео просматривает код функций, участвующих в добавлении членов. Он замечает, что код распределен по трем классам: System, Library и UserManagement.

Листинг 6.21. Функции, участвующие в операции добавления члена

System.addMember = function(systemState, member) {

var previous = systemState.get();

var next = Library.addMember(previous, member);

systemState.commit(previous, next);

};

Library.addMember = function(library, member) {

var currentUserManagement = _.get(library, "userManagement"); var nextUserManagement = UserManagement.addMember(

currentUserManagement, member);

var nextLibrary = _.set(library, "userManagement", nextUserManagement); return nextLibrary;

};

UserManagement.addMember = function(userManagement, member) {

var email = _.get(member, "email");

var infoPath = ["membersByEmail", email];

if(_.has(userManagement, infoPath)) {

throw "Member already exists.";

}

Глава 6. Модульные тесты

163

var nextUserManagement = _.set(userManagement,

infoPath,

member);

return nextUserManagement;

};

Тео хватает еще одну салфетку. Нарисовать дерево вызовов функций для

System.addMember теперь совсем несложно (рис. 6.5).

Рис. 6.5. Дерево вызовов функций для System.addMember

Джо: Превосходно! Итак, какие функции дерева должны быть протестированы на

изменения, касающиеся добавления членов?

Тео: Я думаю, что нам нужно протестировать следующие функции: System.

addMember, SystemState.get, SystemState.commit, Library.addMember и UserManagement.

addMember. Это так?

Джо: Вы совершенно правы. Давайте отложим написание модульных тестов для

функций, принадлежащих SystemState, на потом. Это общие функции, которые

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

осталось три функции: System.addMember, Library.addMember и UserManagement.

addMember.

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

вниз?

Джо: Начнем с самого главного — с UserManagement.addMember. Две другие функции

являются просто обертками.

Тео: Ладно.

Джо: Написание модульного теста для основной функции изменения требует

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

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

новое состояние системы на основе текущего состояния системы и некоторых

аргументов (рис. 6.6).

164

Часть 1. Гибкость

Рис. 6.6. Результат изменения сложнее, чем результат запроса

СОВЕТ. Написание модульного мутационного теста для основной функции требует

больше усилий, чем для запроса.

Тео: Это означает, что в тест-кейсах UserManagement.addMember как входные данные, так и ожидаемые выходные данные являются картами, описывающими состояние системы.

Джо: Именно. Начнем с простейшего случая, когда начальное состояние системы

пусто.

Тео: Вы имеете в виду, что userManagementData, переданный в UserManagement.

addMember, является пустой картой?

Джо: Ага.

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

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

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

Листинг 6.22. Тест-кейс для Catalog.addMember без членов

var member = {

"email": "[email protected]",

"password": "my-secret"

};

var userManagementStateBefore = {};

var expectedUserManagementStateAfter = {

"membersByEmail": {

"[email protected]": {

"email": "[email protected]",

"password": "my-secret"

}

}

};

var result = UserManagement.addMember(userManagementStateBefore, member); _.isEqual(result, expectedUserManagementStateAfter);

Глава 6. Модульные тесты

165

Джо: Очень хорошо! Продолжайте и напишите тест-кейс, когда начальное состояние не пусто.

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

Листинг 6.23. Тест-кейс для Catalog.addMember с существующими элементами

var jessie = {

"email": "[email protected]",

"password": "my-secret"

};

var franck = {

"email": "[email protected]",

"password": "my-top-secret"

};

var userManagementStateBefore = {

"membersByEmail": {

"[email protected]": {

"email": "[email protected]",

"password": "my-top-secret"

}

}

};

var expectedUserManagementStateAfter = {

"membersByEmail": {

"[email protected]": {

"email": "[email protected]",

"password": "my-secret"

},

"[email protected]": {

"email": "[email protected]",

"password": "my-top-secret"

}

}

};

var result = UserManagement.addMember(userManagementStateBefore, jessie); _.isEqual(result, expectedUserManagementStateAfter);

Джо: Потрясающий! Можете ли вы придумать другие тест-кейсы для UserManagement.

addMember?

Тео: Нет.

Джо: Как насчет случаев, когда изменение не работает?

166

Часть 1. Гибкость

Тео: Верно! Я всегда забываю об отрицательных тестах. Предполагаю, что это связано с тем, что я оптимистичный человек.

СОВЕТ. Не забудьте включить отрицательные тест-кейсы в ваши модульные тесты.

Джо: Я тоже. Чем больше я медитирую, тем больше я могу сосредоточиться на положительной стороне жизни. В любом случае как бы вы написали тест-кейс, в котором изменение не удалось?

Тео: Я бы передал в UserManagement.addMember член, который уже существует в

userManagementStateBefore.

Джо: И как бы вы проверили, что код ведет себя должным образом в случае сбоя?

Тео: Дайте-ка подумать. Когда член уже существует, UserManagement.addMember создает исключение. Поэтому в моем тест-кейсе мне нужно поместить код в блок

try/catch.

Джо: Звучит неплохо.

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

Закончив, он с нетерпением поворачивает свой ноутбук к Джо.

Листинг 6.24. Тест-кейс для UserManagement.addMember,

если ожидается, что он не сработает

var jessie = {

"email": "[email protected]",

"password": "my-secret"

};

var userManagementStateBefore = {

"membersByEmail": {

"[email protected]": {

"email": "[email protected]",

"password": "my-secret"

}

}

};

var expectedException = "Member already exists.";

var exceptionInMutation;

try {

UserManagement.addMember(userManagementStateBefore, jessie);

} catch (e) {

exceptionInMutation = e;

}

_.isEqual(exceptionInMutation, expectedException);

Глава 6. Модульные тесты

167

Тео: Теперь я думаю, что готов двигаться дальше и писать модульные тесты для

Library.addMember и System.addMember.

Джо: Я с вами согласен. Пожалуйста, начните с Library.addMember.

Тео: Library.addMember очень похож на UserManagement.addMember. Поэтому я думаю, что напишу аналогичные тест-кейсы.

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

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

Тео: Верно. Поэтому я просто напишу тест-кейс для существующих участников.

Джо: Действуйте!

Тео начинает с копирования и вставки кода из тестового примера UserManagement.

addMember с существующими элементами в листинге 6.23. После нескольких изменений модульный тест для Library.addMember готов.

Листинг 6.25. Модульный тест для Library.addMember

var jessie = {

"email": "[email protected]",

"password": "my-secret"

};

var franck = {

"email": "[email protected]",

"password": "my-top-secret"

};

var libraryStateBefore = {

"userManagement": {

"membersByEmail": {

"[email protected]": {

"email": "[email protected]",

"password": "my-top-secret"

}

}

}

};

var expectedLibraryStateAfter = {

"userManagement": {

"membersByEmail": {

"[email protected]": {

"email": "[email protected]",

"password": "my-secret"

},

168

Часть 1. Гибкость

"[email protected]": {

"email": "[email protected]",

"password": "my-top-secret"

}

}

}

};

var result = Library.addMember(libraryStateBefore, jessie);

_.isEqual(result, expectedLibraryStateAfter);

Джо: Красиво! Теперь мы готовы к последней части. Напишите модульный тест

для System .addMember. Прежде чем начать, не могли бы вы описать ввод и вывод

System.addMember?

Тео еще раз смотрит на код System.addMember и колеблется; он немного сбит с толку.

Функция ничего не возвращает!

Листинг 6.26. Код System.addMember

System.addMember = function(systemState, member) {

var previous = systemState.get();

var next = Library.addMember(previous, member);

systemState.commit(previous, next);

};

Тео: Ввод System.addMember — это экземпляр состояния системы и член. Но я не

уверен, что выводит System.addMember.

Джо: На самом деле System.addMember не имеет никакого вывода. Он относится

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

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

Джо подзывает официанта, чтобы узнать, можно ли принести еще салфеток. Решив

эту проблему, он рисует диаграмму на рис. 6.7.

Рис. 6.7. System.addMember не возвращает данные — он изменяет состояние системы!

Глава 6. Модульные тесты

169

Тео: Тогда как мы можем проверить, что код работает должным образом?

Джо: Мы получим состояние системы после выполнения кода и сравним его

с ожидаемым значением состояния.

Тео: Хорошо. Я попробую написать модульный тест.

Джо: Написание модульных тестов для кода с отслеживанием состояния сложнее, чем для кода манипулирования данными. Это требует спокойствия в офисе.

Тео: Тогда давайте вернемся в офис. Официант! Счет, пожалуйста.

Тео оплачивает счет, и они с Джо возвращаются на канатной дороге в Albatross.

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

Library.addMember.

Тео: Можем ли мы использовать _.isEqual с состоянием системы?

Джо: Определенно. Состояние системы — это карта, как и любая другая карта.

СОВЕТ. Состояние системы — это карта. Следовательно, в контексте тест-кейса мы

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

системы, используя _.isEqual.

Тео копирует и вставляет код для Library.addMember (листинг 6.21), который инициализирует данные для теста. Затем он передает объект SystemState, инициали-зированный с помощью libraryStateBefore, в System.addMember. Наконец, чтобы завершить тест, он сравнивает состояние системы после внесения изменения с ожидаемым значением состояния.

class SystemState {

systemState;

get() {

return this.systemState;

}

commit(previous, next) {

this.systemState = next;

}

}

window.SystemState = SystemState;

Листинг 6.27. Модульный тест для System.addMember

var jessie = {

"email": "[email protected]",

"password": "my-secret"

};

var franck = {

"email": "[email protected]",

"password": "my-top-secret"

};

170

Часть 1. Гибкость

var libraryStateBefore = {

"userManagement": {

"membersByEmail": {

"[email protected]": {

"email": "[email protected]",

"password": "my-top-secret"

}

}

}

};

var expectedLibraryStateAfter = {

userManagement": {

"membersByEmail": {

"[email protected]": {

"email": "[email protected]",

"password": "my-secret"

},

"[email protected]": {

"email": "[email protected]",

"password": "my-top-secret"

}

}

}

};

var systemState = new SystemState();❶

systemState.commit(null, libraryStateBefore); ❷

System.addMember(systemState, jessie); ❸

_.isEqual(systemState.get(),

expectedLibraryStateAfter); ❹

❶ Создает пустой объект SystemState (см. главу 4).

❷ Инициализирует состояние системы данными библиотеки перед изменением.

❸ Выполняет изменение объекта SystemState.

❹ Проверяет состояние после выполнения изменения.

Джо: Ничего себе, я впечатлен; вы сделали это! Поздравляю!

Тео: Спасибо. Я так рад, что в ДОП большая часть нашего кода связана с манипулированием данными. Определенно, приятнее писать модульные тесты для кода

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

Джо: Теперь, когда вы знаете основы ДОП, не хотели бы вы провести рефакторинг

кода своего прототипа Klafim в соответствии с принципами ДОП?

Глава 6. Модульные тесты

171

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

следующие шаги. Надеюсь, она будет готова работать с Albatross в долгосроч-ной перспективе.

Джо: Интересно. Знаете ли вы, что может повлиять на решение Нэнси?

Тео: Наша смета расходов, конечно, но я знаю, что она контактирует с другими

компаниями-разработчиками программного обеспечения. Если мы выступим

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

Джо: Я совершенно уверен, что после рефакторинга в ДОП реализация фич займет

гораздо меньше времени. Это означает, что вы должны иметь возможность

предложить Нэнси более низкую общую стоимость, чем у конкурентов, верно?

Тео: Я буду надеяться на лучшее.

Движение вперед

Встреча с Нэнси прошла хорошо. Albatross заключил сделку, Моника (босс Тео) довольна, и это будет долгосрочный проект с хорошим бюджетом. Им нужно будет

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

Джо: Как прошла встреча с Нэнси?

Тео: Мы заключили сделку!

Джо: Отлично! Я говорил вам, что с ДОП оценка затрат будет ниже.

Тео: На самом деле мы не собираемся использовать ДОП для этого проекта.

Джо: Почему?

Тео: После рефакторинга прототипа системы управления библиотеками в ДОП

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

ДОП может хорошо подойти для этапа прототипа, но он не будет хорошо работать в масштабе.

Джо: Не могли бы вы поделиться деталями вашего анализа?

Тео: Прямо сейчас не могу. Я за рулем.

Джо: Не могли бы мы встретиться в вашем офисе сегодня попозже?

Тео: Я очень занят новым проектом и жесткими сроками.

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

Тео: Хорошо. Тогда давай встретимся в четыре часа дня.

ПРИМЕЧАНИЕ. История продолжается в начале части 2.

172

Часть 1. Гибкость

Итоги

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

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

Тест-кейсы следуют одной и той же простой общей схеме:

• генерировать входные данные;

• генерировать ожидаемые выходные данные;

• сравнивать выходные данные функции с ожидаемыми выходными данными.

Чтобы сравнить вывод функции с ожидаемым выводом данных, нам нужно рекурсивно сравнить два фрагмента данных.

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

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

Дерево вызовов функций для функции f — это дерево, в котором корнем является f, а дочерними элементами узла g в дереве являются функции, вызываемые g.

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

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

Функции, расположенные на более низком уровне дерева вызовов функций, как

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

более высоком уровне дерева.

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

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

Модульные мутационные тесты сосредоточены на фазе расчета изменения.

Достоверность данных зависит от контекста.

Чем меньше данных, тем легче ими манипулировать.

Мы сравниваем выходные данные и ожидаемые выходные данные наших функций с универсальной функцией, которая рекурсивно сравнивает две части данных (например, _.isEqual).

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

должным образом. Это значительно уменьшает количество тест-кейсов в наших

модульных тестах.

Глава 6. Модульные тесты

173

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

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

усилий, чем для запроса.

Не забудьте включить отрицательные тест-кейсы в ваши модульные тесты.

Состояние системы — это карта. Следовательно, в контексте тест-кейса мы

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

174

Часть 1. Гибкость

Часть 2

Масштабируемость

Тео чувствует себя довольно неловко перед встречей с Джо. Он с таким энтузи-азмом рассказывал о ДОП и так хорошо его обучал. Каждая встреча с ним была

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

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

Джо: Я очень рад, что ты заключил сделку с Нэнси.

Тео: Ага. Мы в нашем офисе очень этому рады, но и немного переживаем.

Джо: А почему переживаете?

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

Джо: Но вы говорили, что не будете использовать ДОП. Я думал, сроки будут

обычные.

Тео: Нет, просто моя начальница Моника очень сильно хотела заключить сделку.

Она считает, что успех этого проекта стратегически важен для Albatross, поэтому стоит пойти на некоторый риск, обозначив, по ее выражению, «оптимистич-ные» сроки. Я сказал ей, что такие сроки абсолютно нереалистичные, но Моника

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

Джо: Понятно. Теперь я вижу, почему по телефону вы мне сказали, что очень заня-ты. Но все же, пожалуйста, поделитесь причинами, которые заставили вас

думать, что ДОП не подойдет в масштабе?

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

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

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

Джо: Я рад, что вам это пригодилось.

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

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

масштабе.

176

Часть 2. Масштабируемость

Джо: Поделитесь-ка причинами такого умозаключения.

Тео: Причин много. Прежде всего, поскольку ДОП работает только с общими

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

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

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

Джо: Я вас понял. Что еще, друг мой?

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

Джо: Понял. А еще что?

Тео: Я провел небольшое исследование по внедрению неизменяемых структур

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

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

Джо: Все понятно. Что-то еще?

Тео: По мере роста нашей системы мы будем использовать базу данных для хранения данных приложений и внешних сервисов для обогащения информации

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

Джо: Понял, понял. А еще-то что, дружище?

Тео: А вам не кажется, что у меня достаточно причин отказаться от ДОП?

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

Тео: Что вы имеете в виду?

Джо: С помощью медитации я научился не привязываться к возражениям, которые

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

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

медитация.

Тео: Я не понимаю, как дыхание убедит меня дать ДОП второй шанс.

Джо: В этом случае дыхания может быть и недостаточно, но более глубокое знание

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

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

Тео: Но у меня нет времени на дополнительные уроки. Мне нужно работать.

Джо: Вы слышали историю о молодом дровосеке и старике?

Тео: Нет.

Джо: Тогда я расскажу.

Часть 2. Масштабируемость

177

МОЛОДОЙ ДРОВОСЕК И СТАРИК

Молодой дровосек с большим трудом пытался спилить дерево. Старик, стоявший непо-далеку и наблюдавший за происходящим, спросил: «Что ты делаешь?»

«Ты что, слепой? — ответил дровосек. — Я рублю это дерево».

Старик ответил: «Ты же весь измучался! Сделай перерыв. Наточи свою пилу».

Молодой дровосек объяснил старику, что он пилил уже несколько часов и у него не было

времени сделать перерыв.

Старик настаивал: «Если ты наточишь пилу, то срубишь дерево гораздо быстрее».

Дровосек сказал: «У меня нет времени затачивать пилу. Разве ты не видишь, я слишком

занят!»

Тео на мгновение задумался над этой историей. Он задался вопросом, стоит ли ему

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

уровень практики.

Тео: Вы в самом деле думаете, что с ДОП реализация проекта займет гораздо

меньше времени?

Джо: Я в этом уверен!

Тео: Но если мы пропустим сроки, меня, вероятно, уволят. Вы ничем не рискуете, а я как раз наоборот.

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

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

в Albatross.

Тео: А что, если мы уложимся в срок?

Джо: Если вы уложитесь в срок, вас, вероятно, повысят в должности. В таком случае я попрошу вас купить подарок для моего сына Нерайи и моей дочери Аурелии.

Тео: Годится! Когда я получу свой первый урок о том, как глубже погрузиться

в ДОП?

Джо: Почему бы не начать прямо сейчас?

Тео: Так, сейчас перенесу свои встречи.

178

Часть 2. Масштабируемость

Основы валидации данных

Торжественный подарок

В ЭТОЙ ГЛАВЕ РАССМАТРИВАЮТСЯ

Важность валидации данных на границах системы.

Валидация данных с использованием языка JSON-схемы.

Интеграция валидации данных в существующую кодовую базу.

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

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

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

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

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

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

Эта глава представляет собой глубокое погружение в четвертый принцип ДОП.

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

7.1. Валидация данных в ДОП

Тео перенес свои встречи. Сроки намечены грозные, поэтому он все еще не уверен, верно ли было дать ДОП второй шанс.

ПРИМЕЧАНИЕ. Причина, по которой Тео перенес свои встречи, объясняется в начале

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

180

Часть 2. Масштабируемость

Джо: Как вы думаете, какого аспекта ООП вам будет не хватать больше всего

в вашем большом проекте?

Тео: Валидации данных.

Джо: Расскажите-ка поподробнее.

Тео: В ООП есть надежная гарантия того, что при создании экземпляра класса его

поля-элементы будут иметь правильные имена и правильные типы. Но с ДОП

слишком легко допустить ошибки в именах и типах полей.

Джо: Что ж, у меня для вас прекрасные новости! Существует способ валидации

данных и в ДОП.

Тео: А как это будет работать? Я думал, что ДОП и проверка данных — это две

взаимоисключающие вещи!

Джо: Вовсе нет. ДОП и в самом деле не вынуждает вас валидировать данные, но

это не мешает вам делать это самостоятельно. В ДОП схема данных отделена от

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

Тео: Я не понимаю, устранит ли это проблемы с согласованностью данных.

Джо: Согласно принципам ДОП наиболее важные для валидации данные — это

данные, которые пересекают границы системы.

Тео: Какие границы вы имеете в виду?

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

Тео: А можно посмотреть на диаграмму?

Джо подходит к доске и берет ручку. Затем он рисует диаграмму, подобную приведенной на рис. 7.1.

Рис. 7.1. Высокоуровневая

архитектура современного

веб-сервера

Джо: Эта архитектурная диаграмма определяет то, что мы называем границами

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

системы?

ПРИМЕЧАНИЕ. Границы системы определяются как области, в которых система обменивается данными.

Глава 7. Основы валидации данных

181

Тео: Сейчас подумаю. Первая — это граница с клиентом, затем идет граница с базой данных и, наконец, граница с веб-сервисом.

Джо: Именно! Важно определить границы системы, потому что в ДОП мы разгра-ничиваем два вида валидации данных: валидация, которая происходит на границах системы, и валидация, которая происходит внутри системы. Сегодня мы

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

Тео: Означает ли это, что валидация данных на границах системы более важна?

Джо: Конечно! Как только вы убедитесь, что данные, поступающие в систему и из

нее, являются валидными, вероятность появления неподходящих данных внутри

системы довольно мала.

СОВЕТ. После валидации данных на границах системы повторная валидация данных

внутри системы не является обязательной.

Тео: Тогда зачем нам нужна проверка данных внутри системы?

Джо: Это связано с упрощением кодирования системы по мере роста кодовой базы.

Тео: И какова же основная цель валидации данных на границах?

Джо: Чтобы предотвратить ввод невалидных данных и их вывод из системы, а также для отображения полезной информации об ошибках при столкновении с невалидными данными. Я нарисую на доске таблицу, и вы сможете увидеть отличия (табл. 7.1).

Таблица 7.1. Два вида валидации данных

Вид валидации данных

Цель

Среда

На границах

Защита

Использование

Внутри Облегчение

разработки

Разработка

Тео: А когда вы расскажете мне о валидации данных внутри системы?

Джо: Позже, когда кодовая база станет больше.

7.2. Суть JSON-схемы

Тео: На данный момент Система управления библиотекой — это приложение, которое выполняется в памяти, без базы данных и подключенных к нему HTTP-клиентов. Но Нэнси, вероятно, захочет, чтобы я превратил систему в настоящий

веб-сервер с клиентами, базой данных и внешними сервисами.

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

книг.

Тео: По сути, поисковый запрос состоит из строки и полей, которые вы хотели бы

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

является массивом строк.

182

Часть 2. Масштабируемость

Тео быстро записывает что-то на доске. Закончив, он отходит в сторону, чтобы Джо

смог взглянуть на получившийся код для поискового запроса.

Листинг 7.1. Пример поискового запроса

{

"title": "habit",

"fields": ["title", "weight", "number_of_pages"]

}

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

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

Тео: Что именно вы подразумеваете под «отдельно»?

Джо: Представление данных выступает само по себе, а схема данных — сама по

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

данных, как захотите и когда захотите.

СОВЕТ. В ДОП схема данных отделена от представления данных.

Тео: Как по мне, так это довольно абстрактно.

Джо: Это точно. Но через мгновение все прояснится. А пока я покажу вам, как создать схему данных для поискового запроса на языке схем, называемом JSON-схемой.

Тео: Я обожаю JSON!

ПРИМЕЧАНИЕ. Информацию о языке JSON-схема можно найти по адресу https://jsonschema.org. Для схем в этой книге используется JSON-схема версии 2020-12.

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

в случае запроса на поиск книг?

Тео: Карта.

Джо: В схеме JSON тип данных для карт называется object. Я покажу вам базовую

канву карты. Это карта с двумя полями: type и properties.

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

Листинг 7.2. Схема канвы карты

{

"type": "object",

"properties": {...}

}

Джо: Значение поля type — «object», а значение поля properties — карта со схемой

для полей карты.

Глава 7. Основы валидации данных

183

Тео: Насколько я понял, внутри поля свойств мы будем выражать схему полей карты в виде JSON-схемы.

Джо: Верно.

Тео: У меня уже голова кружится от рекурсии...

Джо: В JSON-схеме любая из схем обычно представляет собой объект JSON с полем, называемым type, которое определяет тип данных. Например, тип поля

title — string, а вот...

Тео: ...тип поля fields — array.

Джо: Да!

Теперь к доске подходит Тео. Он заполняет пробелы в схеме поискового запроса

при помощи информации о полях.

Листинг 7.3. Канва схемы для поискового запроса

{

"type": "object",

"properties": {

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

"fields": {"type": "array"}

}

}

Тео собрался было вернуться к своему столу, но Джо сделал ему знак «Пожалуйста, оставайтесь пока у доски». Тео разворачивается и возвращается к доске.

Джо: Можно уточнить свойство поля fields, предоставив информацию о типе элементов в массиве. В JSON-схеме схема массива имеет свойство под названием

items, значением которых является схема для элементов массива.

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

Джо результат.

Листинг 7.4. Схема поискового запроса с информацией об элементах массива

{

"type": "object",

"properties": {

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

"fields": {

"type": "array",

"items": {"type": "string"}

}

}

}

184

Часть 2. Масштабируемость

Прежде чем вернуться к своему столу, Тео спрашивает Джо:

Тео: Теперь-то мы закончили?

Джо: Еще нет. Можно также быть поточнее в отношении поля fields в поисковом

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

составить список разрешенных значений.

Тео: Что-то вроде значения перечисления?

Джо: Точно! На самом деле JSON-схема поддерживает значения перечислений

с помощью ключевого слова enum. Вместо {"type": "string"} вам нужно ввести

{"enum": [...]} и заменить точки на поддерживаемые поля.

Тео снова поворачивается к доске. Он заменяет точки информацией, о которой по-ведал Джо.

Листинг 7.5. Схема для поискового запроса со значениями перечисления

{

"type": "object",

"properties": {

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

"fields": {

"type": "array",

"items": {

"enum": [

"publishers",

"number_of_pages",

"weight",

"physical_format",

"subjects",

"publish_date",

"physical_dimensions"

]

}

}

}

}

Тео: Ну теперь-то все?

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

Тео: И как же нам выразить эту информацию в JSON-схеме?

Джо: Существует поле с именем required, значение которого представляет собой

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

Глава 7. Основы валидации данных

185

Тео добавляет на доску поле required и смотрит на Джо. На этот раз Джо делает

знак: «Теперь можете вернуться к своему столу».

Листинг 7.6. Схема поискового запроса

var searchBooksRequestSchema = {

"type": "object",

"properties": {

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

"fields": {

"type": "array",

"items": {

"enum": [

"publishers",

"number_of_pages",

"weight",

"physical_format",

"subjects",

"publish_date",

"physical_dimensions"

]

}

}

},

"required": ["title", "fields"]

};

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

Тео: Что значит произвести валидацию?

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

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

СОВЕТ. Валидация данных в ДОП — это проверка соответствия фрагмента данных

схеме.

Тео: Понял.

Джо: Есть несколько библиотек, которые обеспечивают валидацию для JSON-схемы. В них есть функция validate, которая получает схему и фрагмент данных

и возвращает значение true, если данные валидны, и false, когда данные невалидны. У меня на ноутбуке совершенно случайно оказался файл, содержащий

список библиотек с функцией валидации схемы (табл. 7.2). Если хотите, мы его

распечатаем.

Тео включает принтер, пока Джо ищет на ноутбуке нужную таблицу. Отыскав

файл, он сверяется с Тео и нажимает кнопку Печать.

186

Часть 2. Масштабируемость

Таблица 7.2. Библиотеки для валидации JSON-схемы

Язык Библиотека

URL

Назад: Глава 3. Основные манипуляции с данными
Дальше: 7.3. Гибкость и строгость схемы