Книга: Дата-ориентированное программирование: Пер. с англ.
Назад: 11.7. Доставка в срок
Дальше: Преимущество № 3: системы, как правило, менее сложны

if (books <= 10) {

return "low";

};

if (books >= 51) {

return "high";

}

return "medium";

};

var authorNameArgsSchema = {

"type": "array",

"prefixItems": [

authorSchema,

Глава 13. Полиморфизм

349

{"enum": ["markdown", "html"]}

]

};

function authorNameDispatch(author, format) {

if(dev()) {

if(!ajv.validate(authorNameArgsSchema, [author, format])) {

throw ("Author.myName called with invalid arguments: " +

ajv.errorsText(ajv.errors));

}

}

return [Author.prolificityLevel(author), format];

};

Author.myName = multi(authorNameDispatch);

Затем Дейв работает над методами, сначала формата HTML. В HTML жирный

текст заключается в тег <b>, а курсивный заключается в тег <i>. Например, в HTML три автора с разными уровнями плодовиции будут записаны следующим

образом.

Листинг 13.24. Примеры выделения жирным шрифтом и курсивом в HTML

<i>Yehonathan Sharvit<i> ❶

<b>Stephen Covey</b> ❷

<b><i>Isaac Asimov</i></b> ❸

❶ Курсивный шрифт для минимально плодовитых авторов.

❷ Жирный шрифт для умеренно плодовитых авторов.

❸ Форматирование жирным шрифтом и курсивом для очень плодовитых авторов.

С этой информацией на руках Дейв пишет три метода, которые занимаются форматированием в HTML. Легкотня!

Листинг 13.25. Методы, которые занимаются форматированием в HTML

function authorNameLowHtml(author, format) {

return "<i>" + _.get(author, "name") + "</i>";

}

Author.myName = method(["low", "html"],

authorNameLowHtml)(Author.myName);

function authorNameMediumHtml(author, format) {

return "<b>" + _.get(author, "name") + "</b>";

}

350

Часть 3. Удобство сопровождения

Author.myName = method(["medium", "html"], authorNameMediumHtml)(Author.myName); function authorNameHighHtml(author, format) {

return "<b><i>" + _.get(author, "name") + "</i></b>";

}

Author.myName = method(["high", "html"], authorNameHighHtml)(Author.myName); Затем Дейв переходит к трем методам, которые занимаются форматированием

в Markdown. В Markdown жирный текст заключается в две звездочки, а курсивный

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

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

методов Markdown приведен в листинге 13.27.

Листинг 13.26. Примеры выделения жирным шрифтом и курсивом в Markdown

*Yehonathan Sharvit* ❶

**Stephen Covey** ❷

***Isaac Asimov*** ❸

❶ Курсивный шрифт для минимально плодовитых авторов.

❷ Жирный шрифт для умеренно плодовитых авторов.

❸ Форматирование жирным шрифтом и курсивом для очень плодовитых авторов.

Листинг 13.27. Методы, которые занимаются форматированием в Markdown function authorNameLowMarkdown(author, format) {

return "*" + _.get(author, "name") + "*";

}

Author.myName =

method(["low", "markdown"], authorNameLowMarkdown)(Author.myName); function authorNameMediumMarkdown(author, format) {

return "**" + _.get(author, "name") + "**";

}

Author.myName =

method(["medium", "markdown"], authorNameMediumMarkdown)(Author.myName); function authorNameHighMarkdown(author, format) {

return "***" + _.get(author, "name") + "***";

}

Author.myName =

method(["high", "markdown"], authorNameHighMarkdown)(Author.myName);

Глава 13. Полиморфизм

351

Дейв решает протестировать получившийся код при помощи таинственного автора. В листингах 13.28 и 13.29 показаны эти тесты.

Листинг 13.28. Тестирование форматирования в HTML

var yehonathan = {

"name": "Yehonathan Sharvit",

"bookIsbns": ["9781617298578"]

};

Author.myName(yehonathan, "html");

// → "<i>Yehonathan Sharvit</i>"

Листинг 13.29. Тестирование форматирования в Markdown

Author.myName(yehonathan, "markdown");

// → "*Yehonathan Sharvit*"

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

Тео: А кто такой Йонатан Шарвит?

Дейв: Я даже и не знаю. Это имя я увидел вчера, когда гуглил «дата-ориентированное программирование». Шарвит написал книгу на эту тему. Я решил, что

будет прикольно использовать его ISBN в этом тесте.

Итоги

Основным преимуществом полиморфизма является расширяемость.

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

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

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

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

Каждый из методов, используемых в мультиметоде, обеспечивает реализацию

для определенного значения отправки.

Мультиметоды могут имитировать наследование классов ООП с помощью единичной отправки.

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

type.

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

352

Часть 3. Удобство сопровождения

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

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

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

от аргументов при выполнении программы.

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

Функция отправки мультиметода отвечает за:

• определение сигнатуры;

• влидацию аргументов;

• выдачу значения отправки.

Мультиметоды обеспечивают расширяемость за счет отделения инициализации

мультиметода от вариантов реализации методов.

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

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

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

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

порядку элементов во внутренней структуре методов.

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

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

size(coll)

Доставляет размер coll

Расширенная обработка данных

Всё, что хорошо задумано, ясно сказано

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

Манипулирование вложенными данными.

Написание четкого и лаконичного кода для бизнес-логики.

Разделение бизнес-логики и общих манипуляций с данными.

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

данными.

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

Когда наша бизнес-логика включает расширенную обработку данных, общих

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

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

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

легким для чтения другими разработчиками.

14.1. Обновление значения на карте

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

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

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

354

Часть 3. Удобство сопровождения

Дейв: Я думаю, что нашел ошибку в коде, который возвращает информацию о книге из Open Library API.

Тео: Какую ошибку?

Дейв: Иногда API возвращает повторяющиеся имена авторов, и мы передаем дубликаты клиенту.

Тео: Похоже, эту ошибку несложно исправить.

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

Тео: Критическое отношение к собственному коду — важное качество для про-гресса разработчика. Что именно тебе не нравится?

Дейв: Взгляни на код.

Листинг 14.1. Удаление дубликатов простым, но утомительным способом

function removeAuthorDuplicates(book) {

var authors = _.get(book, "authors");

var uniqAuthors = _.uniq(authors);

return _.set(book,"authors", uniqAuthors);

}

Дейв: Я использую _.get для получения массива с именами авторов, затем _.uniq для создания версии массива без дубликатов и, наконец, _.set для создания

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

Тео: Код утомительный, потому что следующее значение authorNames должно быть

основано на его текущем значении.

Дейв: Но это обычный вариант использования! Разве нет более простого способа

написать такой код?

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

узнать, доступен ли он для звонка.

Джо: Как дела, Тео?

Тео: Отлично! Вы вернулись со своей технической конференции?

Джо: Я только что приземлился. Сейчас я еду домой на такси.

Тео: Как прошел ваш разговор о ДОП?

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

рассказал им историю Albatross и Klafim, она оказалась вполне убедительной.

Тео: Да, в этом взрослые похожи на детей; они любят истории.

Джо: Что насчет вас? Удалось ли вам добиться полиморфизма с помощью мультиметодов?

Глава 14. Расширенная обработка данных

355

Тео: Да! Дейву даже удалось реализовать функцию в Klafim с помощью мультиметодов.

Джо: Отлично!

Тео: У вас есть время помочь Дейву с вопросом о программировании?

Джо: Конечно.

Дейв: Здравствуйте, Джо. Как дела?

Джо: Здравствуйте, Дейв. Неплохо. Какая помощь вам нужна?

Дейв: Мне интересно, есть ли более простой способ удалить дубликаты внутри

значения массива на карте. Использование _.get, _.uniq и _.set выглядит довольно утомительно.

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

данными.

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

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

Дейв: Каковы, по вашему мнению, были бы аргументы update?

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

Дейв: Что?!

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

Дейв: Я понимаю, что вы имеете в виду: лошадь — это реализация update, а теле-га — использование update.

Джо: В яблочко. Но помните: будет лучше, если вы сохраните общую функцию

update.

Дейв: Как?

Джо: Не ограничивая его вашим конкретным вариантом использования.

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

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

Дейв: До свидания, Джо, и спасибо!

Дейв заканчивает конференц-связь. Глядя на Тео, он повторяет разговор с Джо.

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

правильную подпись update.

1 Lodash предоставляет реализацию update, но ради обучения мы пишем нашу собственную реализацию.

356

Часть 3. Удобство сопровождения

Тео: Звучит как план.

Дейв: Джо называл это «ставить телегу впереди лошади».

Тео: Ох уж этот Джо и его забавные аналогии...

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

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

Листинг 14.2. Код, который удаляет дубликаты элегантным способом

function removeAuthorDuplicates(book) {

return update(book, "authors", _.uniq);

}

Тео: По-моему, выглядит неплохо!

Дейв: Ух ты! Теперь код с обновлением намного элегантнее, чем код с _.get и

_.set!

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

Дейв: Это довольно просто: update получает карту с именем map, путь с именем path и функцию с именем fun. Она возвращает новую версию map, где path связан

с fun(currentValue), а currentValue — это значение, связанное с path в map.

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

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

Рис. 14.1. Поведение update

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

Тео: С таким четким определением реализовать update будет проще простого!

Глава 14. Расширенная обработка данных

357

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

Листинг 14.3. Общая функция update

function update(map, path, fun) {

var currentValue = _.get(map, path);

var nextValue = fun(currentValue);

return _.set(map, path, nextValue);

}

Тео: Почему бы тебе не посмотреть, работает ли это в простом случае, таком как

увеличение числа на карте?

Дейв: Хорошая идея! Я попробую умножить значение на карте на 2 с помощью

update. Как это выглядит?

Листинг 14.4. Умножение значения на карте на 2

var m = {

"position": "manager",

"income": 100000

};

update(m, "income", function(x) {

return x * 2;

});

// → {"position": "manager", "income": 200000}

Тео: Отлично! Кажется, это работает.

14.2. Манипулирование вложенными данными

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

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

Дейва. Возвращаясь к настоящему моменту, Тео начинает.

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

Дейв: Типа каких?

Тео: Я приведу несколько примеров... Позволь мне найти электронное письмо, которое я получил вчера от Нэнси.

Дейв: ОК.

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

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

358

Часть 3. Удобство сопровождения

Дейв: Какой функцией я должен заняться в первую очередь?

Тео: Это не имеет значения, но ты должен сделать три из них до конца недели.

Удачи, и не стесняйся звонить мне, если понадобится помощь.

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

Дейв: Я начал работать над тремя функциями администратора, но мне не нравится

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

Тео: Не мог бы ты напомнить мне, как выглядит элемент в списке книг, возвра-щаемый из базы данных?

Дейв: Каждая книга представляет собой карту с полем массива authorIds.

Тео: Хорошо, так что, похоже, это должно быть сделано с помощью map поверх

книг.

Дейв: Это то, что я сделал, но это работает не так, как ожидалось. Вот мой код для

перечисления идентификаторов авторов книг.

Листинг 14.5. Получение идентификаторов авторов в книгах

в виде массива массивов

function authorIdsInBooks(books) {

return _.map(books, "authorIds");

}

Тео: В чем проблема?

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

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

Листинг 14.6. Идентификаторы автора в массиве массивов

[

["sean-covey", "stephen-covey"],

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

]

Тео: Это не такая уж большая проблема. Ты можешь сгладить массив массивов

с помощью _.flatten и получить ожидаемый результат.

Дейв: Хорошо! Это именно то, что мне нужно! Дайте мне минутку, чтобы исправить код authorIdsInBooks... вот.

Листинг 14.7. Получение идентификаторов авторов в книгах в виде массива строк

function authorIdsInBooks(books) {

return _.flatten(_.map(books, "authorIds"));

}

Глава 14. Расширенная обработка данных

359

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

Дейв: Может быть. Реализовать функцию flatMap довольно просто2. Как насчет

этого?

Листинг 14.8. Реализация flatMap

function flatMap(coll, f) {

return _.flatten(_.map(coll,f));

}

Тео: Здорово!

Дейв: Я не знаю... Это немного странно — иметь такую маленькую функцию.

Тео: Я не думаю, что размер кода — это то, что здесь имеет значение.

Дейв: В смысле?

Тео: Посмотри, что происходит, когда ты переписываешь authorIdsInBooks с помощью flatMap.

Дейв: Хорошо, вот как я бы использовал flatMap для перечисления идентификаторов авторов.

Листинг 14.9. Получение идентификаторов авторов в виде массива строк

с использованием flatMaps

function authorIdsInBooks(books) {

return flatMap(books, "authorIds");

}

Тео: Какую реализацию ты предпочитаешь: с flatten и map (в листинге 14.7) или

с flatMap (в листинге 14.9)?

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

Тео: Верно, но какая реализация более удобочитаема?

Дейв: Ну, предполагая, что я знаю, что делает flatMap, я бы сказал, что реализация

с помощью flatMap. Поскольку она более лаконична и немного более чита-бельна.

Тео: Опять же, дело не в размере кода. Речь идет о ясности намерений и силе именования вещей.

Дейв: Не понял.

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

Дейв: Давай.

2 Lodash предоставляет реализацию flatMap, но ради обучения мы пишем нашу собственную реализацию.

360

Часть 3. Удобство сопровождения

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

письма?

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

ручку на столе. После того как он передает Тео ручку, он спрашивает: Дейв: Почему ты просто не попросил ручку?

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

вместо имен, чтобы передать наше намерение.

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

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

Тео: Именно так.

Дейв: Давай перейдем ко второй функции администратора: вычислению коэффициента выдачи книги.

Тео: Перед этим, я думаю, мы заслуживаем короткого периода отдыха, когда мы

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

Дейв: Кофе-брейк!

14.3. Использование

наилучшего инструмента для работы

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

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

Тео: Покажи же мне!

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

Листинг 14.10. Список из двух книг с элементами bookItems

[

{

"isbn": "978-1779501127",

"title": "Watchmen",

"bookItems": [

{

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

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

"isLent": true

}

]

},

Глава 14. Расширенная обработка данных

361

{

"isbn": "978-1982137274",

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

"bookItems": [

{

"id": "book-item-123",

"libId": "hudson-park-lib",

"isLent": true

},

{

"id": "book-item-17",

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

"isLent": false

}

]

}

]

Тео: Довольно вложенный фрагмент данных!

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

довольно просто. Я просматриваю все элементы книги с помощью forEach и увеличиваю значение счетчика lent или notLent. В конце я возвращаю соотношение

между lent и (lent + notLent). Вот как я это делаю.

Листинг 14.11. Расчет коэффициента выдачи книг с помощью forEach

function lendingRatio(books) {

var bookItems = flatMap(books, "bookItems");

var lent = 0;

var notLent = 0;

_.forEach(bookItems, function(item) {

if(_.get(item, "isLent")) {

lent = lent + 1;

} else {

notLent = notLent + 1;

}

});

return lent/(lent + notLent);

}

Тео: Могу я откровенно сказать, что думаю о твоем коде?

Дейв: Если ты задаешь этот вопрос, это означает, что тебе он не нравится. Верно?

Тео: Я ничего не имею против тебя; но мне не нравится ни один фрагмент кода

с forEach.

Дейв: Что не так с forEach?

362

Часть 3. Удобство сопровождения

Тео: Это слишком обобщенно!

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

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

Дейв: Почему?

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

в реальной жизни.

Дейв: Что ты имеешь в виду?

Тео: Позволь мне привести пример. Вчера мне пришлось почистить свой беспилотник изнутри. Как ты думаешь, что я использовал — отвертку или швейцар-ский армейский нож, чтобы отвинтить крышку дрона?

Дейв: Отвертку, конечно же! Ею гораздо удобнее манипулировать.

Тео: Верно. Кроме того, представь, что кто-то смотрит на меня с помощью отверт-ки. Им совершенно ясно, что я заворачиваю гайку. Это ясно передает мои наме-рения.

Дейв: Хочешь сказать, что forEach подобен швейцарскому армейскому ножу для

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

Тео: Это хороший способ выразить это.

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

проблему.

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

по коллекции элементов книги?

Тео: Ты мог бы использовать _.reduce.

Дейв: Я думал, что reduce — это возврат данных из коллекции. Здесь мне не нужно

возвращать данные; мне нужно обновить две переменные, lent и notLent.

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

Дейв: Можешь ли ты показать мне, как переписать мою функцию lendingRatio с помощью reduce?

Тео: Конечно. Начальное значение, передаваемое для уменьшения, — это карта

{"lent": 0, "notLent": 0}, и внутри каждой итерации мы обновляем один из двух

ключей, вот так.

Листинг 14.12. Расчет коэффициента выдачи книги с использованием reduce function lendingRatio(books) {

var bookItems = flatMap(books, "bookItems");

Глава 14. Расширенная обработка данных

363

var stats = _.reduce(bookItems, function(res, item) {

if(_.get(item, "isLent")) {

res.lent = res.lent + 1;

} else {

res.notLent = res.notLent + 1;

}

return res;

}, {notLent: 0, lent:0});

return stats.lent/(stats.lent + stats.notLent);

}

Дейв: Вместо обновления переменных lent и notLent теперь мы обновляем поля

карты lent и notLent. В чем разница?

Тео: Работа с полями карты вместо переменных позволяет нам избавиться от reduce в нашем коде бизнес-логики.

Дейв: Как ты мог бы выполнить итерацию по коллекции без forEach и без reduce?

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

lendingRatio. В чем смысл вызова reduce?

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

Дейв: Я думаю, что он подсчитывает, сколько раз isLent оказывается true и false.

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

Дейв: Как именно?

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

Дейв: Хорошо, но, прежде чем реализовать эту функцию, позволь мне сначала переписать код lendingRatio, предполагая, что эта функция уже существует.

Тео: Ты определенно быстро учишься, Дейв!

Дейв: Спасибо! Я думаю, что при использовании countByBoolField код для вычисления коэффициента выдачи с использованием пользовательской служебной

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

Листинг 14.13. Расчет коэффициента выдачи книг

function lendingRatio(books) {

var bookItems = flatMap(books, "bookItems");

var stats = countByBoolField(bookItems, "isLent", "lent",

"notLent");

return stats.lent/(stats.lent + stats.notLent);

}

364

Часть 3. Удобство сопровождения

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

Тео: Идеально. Тебе не кажется, что этот код понятнее, чем код, использующий

_.reduce?

Дейв: Да! Код одновременно и более лаконичен, и цель более ясна. Позволь мне

посмотреть, смогу ли я реализовать countByBoolField прямо сейчас.

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

Дейв: Хорошая идея.

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

Листинг 14.14. Модульный тест для countByBoolField

var input = [

{"a": true},

{"a": false},

{"a": true},

{"a": true}

];

var expectedRes = {

"aTrue": 3,

"aFalse": 1

};

_.isEqual(countByBoolField(input, "a", "aTrue", "aFalse"), expectedRes); Тео: По-моему, выглядит неплохо. Теперь для реализации countByBoolField, думаю, тебе понадобится наша функция update.

Дейв: Ты прав. На каждой итерации мне нужно увеличивать значение либо aTrue, либо aFalse, используя update и функцию, которая увеличивает число на 1.

После нескольких проб и ошибок Дейв придумывает фрагмент кода, который использует reduce, update и inc. Он показывает Тео код для countByBoolField.

Листинг 14.15. Реализация countByBoolField

function inc (n) {

return n + 1;

}

function countByBoolField(coll, field, keyTrue, keyFalse) {

return _.reduce(coll, function(res, item) {

if (_.get(item, field)) {

return update(res, keyTrue, inc);

}

Глава 14. Расширенная обработка данных

365

return update(res, keyFalse, inc);

}, {[keyTrue]: 0,❶

[keyFalse]: 0});

}

❶ Создает карту с keyTrue и keyFalse, связанными с 0.

Тео: Отличная работа! Должны ли мы двигаться дальше и рассмотреть третью

функцию администратора?

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

Тео: Хорошо. Позвони мне, когда будешь готов к проверке кода.

14.4. Легкое разматывание

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

зовет Тео на помощь.

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

Тео: У меня есть всего пара минут до моей следующей встречи, но я могу попытаться тебе помочь. Каково точное определение группировки по библиотекам?

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

Листинг 14.16. Модульный тест для группировки книг по библиотекам

var books = [

{

"isbn": "978-1779501127",

"title": "Watchmen",

"bookItems": [

{

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

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

"isLent": true

}

]

},

{

"isbn": "978-1982137274",

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

"bookItems": [

{

"id": "book-item-123",

"libId": "hudson-park-lib",

366

Часть 3. Удобство сопровождения

"isLent": true

},

{

"id": "book-item-17",

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

"isLent": false

}

]

}

];

var expectedRes =

{

"hudson-park-lib": [

{

"bookItems": {

"id": "book-item-123",

"isLent": true,

"libId": "hudson-park-lib",

},

"isbn": "978-1982137274",

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

},

],

"nyc-central-lib": [

{

"bookItems": {

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

"isLent": true,

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

},

"isbn": "978-1779501127",

"title": "Watchmen",

},

{

"bookItems": {

"id": "book-item-17",

"isLent": false,

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

},

"isbn": "978-1982137274",

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

},

],

};

_.isEqual(booksByRack(books) , expectedRes);

Глава 14. Расширенная обработка данных

367

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

также было полезно для меня, когда я переделал Klafim с ООП на ДОП.

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

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

с сохранением состояния.

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

Тео: Что было сложного во внедрении booksByLib?

Дейв: Я начал со сложной реализации, включающей merge и reduce, прежде чем

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

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

Тео: Действительно, это нелегко реализовать.

Дейв: Я рад это слышать. Я думал, что делаю что-то не так.

Тео: Проблема здесь в том, что нужно работать с элементами книги, но название

книги и ISBN отсутствуют на карте элементов книги.

Дейв: Именно!

Тео: Это напоминает мне запрос, который мне пришлось написать год назад на

MongoDB, где данные были выложены аналогичным образом.

Дейв: И как выглядел твой запрос?

Тео: Я использовал оператор $unwind от MongoDB. Учитывая карту m с полем <arr, myArray>, она возвращает массив, где каждый элемент является картой, соответствующей m без arr и с элементом, связанным с элементом myArray.

Дейв: Для меня это немного абстрактно. Не мог бы ты привести пример?

Тео подходит к доске. Он рисует диаграмму, подобную приведенной на рис. 14.2.

Тео: В моем случае я имел дело с интернет-магазином, где корзина покупателя бы-ла представлена в виде карты с полем customer-id и полем массива items. Каждый элемент массива представляет товар в корзине. Я написал запрос с unwind, который извлекал элементы корзины с полем customer-id.

Дейв: Удивительно! Это именно то, что нам нужно. Давайте напишем нашу собственную функцию unwind!

Тео: Я был бы рад поработать с тобой в паре над этим классным материалом, но я

уже опаздываю на другую встречу.

Дейв: Я рад, что я не менеджер!

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

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

Как и советовал Джо, Дейв начинает с написания кода для booksByLib так, как если

бы unwind уже была реализован. Ему нужно просмотреть каждую книгу и развер-нуть ее элементы, используя flatMap и unwind. Затем он группирует элементы книги

368

Часть 3. Удобство сопровождения

Рис. 14.2. Поведение unwind

по их libId, используя _.groupBy. Довольный полученным кодом, он допивает свой

эспрессо.

Листинг 14.17. Группировка книг по библиотекам с помощью unwind

function booksByRack(books) {

var bookItems = flatMap(books, function(book) {

return unwind(book, "bookItems");

});

return _.groupBy(bookItems, "bookItems.libId")

}

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

четко и компактно. Дейв говорит себе, что сложность должна заключаться в реализации unwind — но вскоре он обнаруживает, что ошибается; это будет не так сложно, как он думал! Он начинает с написания модульного теста для unwind, аналогич-ного сценарию Тео MongoDB customer cart.

Листинг 14.18. Модульный тест для unwind

var customer = {

"customer-id": "joe",

"items": [

{

"item": "phone",

Глава 14. Расширенная обработка данных

369

"quantity": 1

},

{

"item": "pencil",

"quantity": 10

}

]

};

var expectedRes = [

{

"customer-id": "joe",

"items": {

"item": "phone",

"quantity": 1

}

},

{

"customer-id": "joe",

"items": {

"item": "pencil",

"quantity": 10

}

}

]

_.isEqual(unwind(customer, "items"), expectedRes)

Реализация unwind определенно не так сложна, как думал Дейв. Он извлекает массив arr, связанный с f в m, и создает для каждого элемента arr версию m, где f связан

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

Листинг 14.19. Реализация unwind

function unwind(map, field) {

var arr = _.get(map, field);

return _.map(arr, function(elem) {

return _.set(map, field, elem);

});

}

После нескольких минут созерцания своего прекрасного кода Дейв отправляет Тео

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

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

370

Часть 3. Удобство сопровождения

Итоги

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

Отделение бизнес-логики от манипулирования данными делает наш код не

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

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

в рамках четырехэтапного процесса:

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

• написание модульного теста для функции;

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

• реализация функции.

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

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

Выбирайте наименее универсальную служебную функцию, которая решает вашу

проблему.

Не используйте _.reduce или любую другую низкоуровневую функцию манипулирования данными внутри кода, который имеет дело с бизнес-логикой. Вместо

этого напишите служебную функцию с собственным именем, которая скрывает

_.reduce.

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

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

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

flatten(arr)

Выравнивает arr на один уровень глубже

sum(arr)

Вычисляет сумму значений в arr

uniq(arr)

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

every(coll, pred)

Проверяет, возвращает ли pred значение true для всех элементов

coll

forEach(coll, f)

Выполняет итерацию по элементам coll и вызывает f для каждого

элемента

sortBy(coll, f)

Создает массив элементов, отсортированных в порядке возрастания,

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

Отладка

Инновации в музее

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

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

данных.

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

данные.

Воспроизведение сценария в среде REPL.

Создание модульных тестов на основе багов.

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

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

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

15.1. Детерминизм в программировании

Через несколько месяцев Тео звонит Дейву и говорит, что он покидает Albatross. Не

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

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

назначить Дейва главным по ДОП в Albatross. Тео приглашает Дейва на прощальную вечеринку в офисе на следующей неделе, а также на последнюю индивидуаль-ную рабочую сессию в Музее науки «Эксплораториум».

В музее Дейву особенно понравилась выставка Cells to Self в галерее Living Systems, а Тео развлекался на выставке Colored Shadows в галерее Reflections.

372

Часть 3. Удобство сопровождения

Завершив осмотр, Тео и Дейв устраиваются в заднем ряду музейного зрительного

зала и открывают свои ноутбуки.

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

Тео: Помнишь, как-то раз Джо сказал нам, что однажды мы сможем внедрять ин-новации в ДОП?

Дейв: Помню.

Тео: Что ж, возможно, этот день настал. Кажется, я обнаружил интересную связь

между ДОП и наукой, и это касается отладки программ.

Дейв: Я заинтригован.

Тео: Ты веришь в детерминизм?

Дейв: В смысле верю ли я в то, что все происходящее во вселенной предопределе-но и что свобода воли — это иллюзия?

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

тем же следствиям?

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

вверх, или даже разбиться!

Тео: А как насчет детерминизма в программировании?

Дейв: Как бы ты определил причины и следствия в программировании?

Тео: Для простоты скажем, что в контексте программирования причины — это аргументы функций, а следствия — возвращаемые значения.

Дейв: А куда определить побочные эффекты?

Тео: Давай пока оставим их в стороне.

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

программы.

Тео: Вот почему состояния нужно избегать, насколько это возможно.

Дейв: Но в реальных приложениях не получится избежать состояния!

Тео: Это факт, но можно свести к минимуму количество модулей, работа которых

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

с неизменяемыми данными.

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

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

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

детерминировано: одни и те же аргументы всегда приводят к одним и тем же возвращаемым

значениям.

Глава 15. Отладка

373

Тео: Это точно. Давай дадим название значениям аргументов функции, с которыми

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

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

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

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

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

СОВЕТ. В ДОП контекст функции состоит из значений аргументов функции.

Тео: Ну вот! Теперь давай поговорим о репродуцируемости. Допустим, нам нужно

захватить контекст функции и репродуцировать его в другой среде.

Дейв: А не расскажешь ли поконкретнее о репродуцировании контекста функции

в другой среде?

Тео: Возьмем, к примеру, конечную точку веб-сервиса. Мы активируем конечную

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

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

Тео: Проблема в том, что мы не знаем значений аргументов этой функции. Вспомни, что мы не активировали foo напрямую. Мы активировали конечную точку.

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

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

Тео: Допустим, foo получает три аргумента: число, строку и огромную вложенную

карту. Как тогда зафиксировать аргументы и воспроизвести foo с теми же аргументами?

Дейв: Я не уверен, что именно ты подразумеваешь под воспроизведением foo.

Тео: Я имею в виду выполнение foo в среде REPL.

ПРИМЕЧАНИЕ. Среда REPL (англ. read-eval-print loop, цикл «чтение — вычисление —

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

со списком сред REPL для разных языков программирования.

Таблица 15.1. Среды REPL для разных языков программирования

JavaScript (Браузер)

Консоль браузера

Node.js Node

CLI

Java JShell

C# C#

REPL

374

Часть 3. Удобство сопровождения

Таблица 15.1 (окончание)

JavaScript (Браузер)

Консоль браузера

Python Интерпретатор

Python

Ruby Interactive

Ruby

Дейв: Обязательно ли цикл REPL должен быть частью процесса, который я отла-живаю?

Тео: Не обязательно. Представь, что среда REPL — это научная лаборатория, где

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

Дейв: ОК. С числами и строками все просто: я могу скопировать их значения в буфер обмена, вставить их в REPL и выполнить foo в REPL с теми же аргументами.

Тео: Это самая легкая часть. А как насчет вложенной карты?

Дейв: Даже не знаю. Я, наверное, не смогу скопировать вложенную карту из отладчика в буфер обмена!

Тео: На самом деле в отладчиках JavaScript это возможно. Например, в Chrome есть

опция копирования, которая появляется, если щелкнуть правой кнопкой мыши

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

Дейв: Никогда не обращал на это внимания.

Тео: Да и без этой опции можно сериализовать вложенную карту в строку JSON, скопировать строку в буфер обмена, а затем вставить строку JSON в REPL. Наконец, можно десериализовать строку в хеш-карту и вызвать foo с ней.

Дейв: Хитрый фокус!

Тео: Для меня это даже не фокус, а скорее фундаментальный аспект ДОП: данные

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

Дейв: Ясно. Сериализовать универсальную структуру данных легко.

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

сериализуем и десериализуем ее.

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

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

Тео: Верно, а второе?

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

СОВЕТ. Двумя условиями репродуцируемости в программировании являются неизменяемость и легкость (де-)сериализации.

Глава 15. Отладка

375

15.2. Репродуцируемость с числами

и строками

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

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

Тео: Изменив код рассматриваемой функции и выведя сериализацию аргументов

на консоль.

Дейв: Я ничего не понял.

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

Дейв: Давай.

Тео: Возьмем, к примеру, функцию, которая возвращает n-ю цифру числа.

Дейв: Ой, я ненавижу цифровую арифметику!

Тео: Не волнуйся, код для этого мы найдем в Интернете.

Тео гуглит «n-я цифра числа в JavaScript» и берет фрагмент кода из StackOverflow, который, похоже, работает.

Листинг 15.1. Вычисление n-й цифры числа

function nthDigit(a, n) {

return Math.floor((a / (Math.pow(10, n - 1)))) % 10;

}

Дейв: А ты понимаешь, как это работает?

Тео: Давай разбираться. Деление a на 10n–1 похоже на сдвиг а вправо на n–1 мест.

Затем нам нужно получить крайнюю правую цифру.

Дейв: А последняя цифра числа получается с помощью операции modulo 10?

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

код для захвата контекста.

Дейв: Что это?

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

Листинг 15.2. Захват контекста, состоящего из чисел

function nthDigit(a, n) {

console.log(a);

376

Часть 3. Удобство сопровождения

console.log(n);

return Math.floor((a / (Math.pow(10, n - 1)))) % 10;

}

Дейв: Выглядит очень просто.

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

Дейв: Когда конечная точка активируется, программа отобразит два числа, a и n, в консоли.

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

Дейв: Нужно скопировать значения a и n из консоли, вставить их в среду REPL и

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

Тео: А почему ты так уверен, что когда мы запустим функцию nthDigit в REPL, она

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

Дейв: Я знаю точно, что nthDigit зависит только от своих аргументов.

Тео: Хорошо. А еще как ты можешь быть уверен, что аргументы, которые переда-ешь ты, совпадают с аргументами, которые уже были переданы?

Дейв: Число есть число!

Тео: Я согласен с тобой. Давай пойдем дальше и посмотрим, что происходит со

строками.

Дейв: Полагаю, что все будет точно так же.

Тео: Все будет почти точно так же. Давай напишем функцию, которая получает

предложение и префикc, возвращая значение true, когда предложение содержит

слово, начинающееся с этого префикса.

Дейв: Зачем кому-то вообще может понадобиться такая странная функция?

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

Дейв: Интересно. Я поговорю об этом с Нэнси. Во всяком случае, написание кода

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

в массиве с этого префикса.

Тео: Как ты собираешься проверить, соответствует ли какой-либо элемент массива

этому условию?

Дейв: Пожалуй, я воспользуюсь функцией filter из Lodash и проверю длину возвращаемого массива.

Тео: Это сработает, но могут возникнуть проблемы с производительностью.

Дейв: Почему?

Тео: А ты сам подумай.

Глава 15. Отладка

377

Дейв: Я понял! Функция filter обрабатывает все элементы в массиве, а не оста-навливается после первого совпадения. Есть ли в Lodash функция, которая оста-навливается после первого совпадения?

Тео: Да, она называется find.

Дейв: Круто. Ей и воспользуюсь. Сейчас.

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

свою реализацию hasWordStartingWith с использованием _.find.

Листинг 15.3. Проверка того, содержит ли предложение слово,

начинающееся с префикса

function hasWordStartingWith(sentence, prefix) {

var words = sentence.split(" ");

return _.find(words, function(word) {

return word.startsWith(prefix);

}) != null;

}

Тео: Годится, теперь добавь, пожалуйста, в начало функции код захвата контекста.

Дейв: Сейчас, немного отредактирую код. Вуаля!

Листинг 15.4. Захват контекста, состоящего из строк

function hasWordStartingWith(sentence, prefix) {

console.log(sentence);

console.log(prefix);

var words = sentence.split(" ");

return _.find(words, function(word) {

return word.startsWith(prefix);

}) != null;

}

Тео: Дай-ка я посмотрю на твой код. Мне интересно, что произойдет, если проверить, содержит ли предложение «I like the word reproducibility» слово, начинающееся с «li».

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

Листинг 15.5. Тестирование hasWordStartingWith

hasWordStartingWith("I like the word \"reproducibility\"", "li");

// It returns true

378

Часть 3. Удобство сопровождения

// It displays the following two lines:

// I like the word "reproducibility"

// li

Дейв: Где кавычки вокруг строк? И где обратные косые черты перед кавычками, окружающими слово «reproducibility»?

Тео: Они исчезли!

Дейв: Почему?

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

кавычек. Это более удобочитаемо для человека.

Дейв: Облом! Это плохо для репродуцируемости. Итак, после того как я скопирую

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

Тео: К счастью, есть более простое решение. Если сериализовать строку в JSON, она будет содержать кавычки и обратные косые черты. Например, вот этот код

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

Листинг 15.6. Вывод на консоль сериализации строки

console.log(JSON.stringify(

"I like the word \"reproducibility\""));

// → "I like the word \"reproducibility\""

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

Тео: Как составные, так и примитивные типы данных являются валидными данными JSON.

Дейв: Круто! Я исправлю код, который фиксирует строковые аргументы для

hasWordStartingWith. Вот, посмотри.

Листинг 15.7. Захват контекста, состоящего из строк, с использованием

сериализации JSON

function hasWordStartingWith(sentence, prefix) {

console.log(JSON.stringify(sentence));

console.log(JSON.stringify(prefix));

var words = sentence.split(" ");

return _.find(words, function(word) {

return word.startsWith(prefix);

}) != null;

}

Тео: Замечательно! Захват строк требует чуть больше труда, чем захват чисел, но

с JSON все не так уж и сложно.

Глава 15. Отладка

379

Дейв: Ну да. Теперь мне хочется посмотреть, как работает сериализация JSON для

захвата контекста с числами.

Тео: Она работает. На самом деле этот подход хорошо работает с любыми данными, будь то примитивный тип или коллекция.

Дейв: Супер!

Тео: Дальше я покажу тебе, как использовать этот подход для репродуцирования

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

Дейв: Больше никакой арифметики?

Тео: Обещаю!

15.3. Репродуцируемость с любыми данными

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

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

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

Дейв: Я только что позвонил Нэнси, чтобы рассказать ей об улучшенной версии

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

книги.

Тео: И?

Дейв: Ей понравилась эта идея.

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

Дейв: С чего начнем?

Тео: Сначала нужно добавить код для захвата контекста внутрь функции, которая

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

Дейв: Эта функция — Catalog.searchBooksByTitle.

Тео: Каковы аргументы Catalog.searchBooksByTitle?

Дейв: У нее есть два аргумента: catalogData (это большая вложенная хеш-карта) и

query (это строка).

Тео: Сможешь отредактировать код и добавить фрагмент, захватывающий контекст?

Дейв: Конечно. Вот это подойдет?

Листинг 15.8. Захват аргументов функции Catalog.searchBooksByTitle Catalog.searchBooksByTitle = function(catalogData, query) {

console.log(JSON.stringify(catalogData));

console.log(JSON.stringify(query));

380

Часть 3. Удобство сопровождения

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

var queryLowerCased = query.toLowerCase();

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

return _.get(book, "title")

.toLowerCase()

.startsWith(queryLowerCased);

});

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

return Catalog.bookInfo(catalogData, book);

});

return bookInfos;

};

Тео: Идеально. Теперь давай активируем конечную точку поиска.

Тео активирует конечную точку поиска с запросом «Watch», надеясь получить подробную информацию о комиксе «Хранители» (Watchmen). Когда конечная точка

возвращается, Тео открывает консоль, и Дейв видит две строки вывода.

Листинг 15.9. Вывод на консоль при активации конечной точки поиска

{"booksByIsbn":{"978-1982137274":{"isbn":"978-1982137274"\

,"title":"7 Habits of Highly Effective People","authorIds":\

["sean-covey","stephen-covey"]},"978-1779501127":{"isbn":\

"978-1779501127","title":"Watchmen","publicationYear":\ 1987,"authorIds":["alan-moore", "dave-gibbons"]}},\

"authorsById":{"stephen-covey":{"name":"Stephen Covey",\

"bookIsbns":["978-1982137274"]},"sean-covey":{"name":"Sean Covey",\

"bookIsbns":["978-1982137274"]},"dave-gibbons":{"name":"Dave Gibbons",\

"bookIsbns":["978-1779501127"]},"alan-moore":{"name":"Alan Moore",\

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

"Watch"

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

прочитать.

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

их, чтобы репродуцировать вызов Catalog.searchBooksByTitle.

Дейв: Можно я попробую? Вот.

Листинг 15.10. Репродуцирование вызова функции

var catalogData = {"booksByIsbn":{"978-1982137274":

{"isbn":"978-1982137274","title":"7 Habits of Highly Effective People",

"authorIds":["sean-covey","stephen-covey"]},

"978-1779501127":

Глава 15. Отладка

381

{"isbn":"978-1779501127","title":"Watchmen",

"publicationYear":1987,

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

"authorsById":

{"stephen-covey":{"name":"Stephen Covey","bookIsbns":

["978-1982137274"]},"sean-covey":{"name":"Sean Covey",

"bookIsbns":

["978-1982137274"]},"dave-gibbons":

{"name":"Dave Gibbons","bookIsbns":

["978-1779501127"]},"alan-moore":

{"name":"Alan Moore","bookIsbns":

["978-1779501127"]}}};

var query = "Watch";

Catalog.searchBooksByTitle(catalogData, query);

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

Дейв: Например?

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

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

Дейв: Без активации конечной точки поиска?

Тео: Именно! Мы будем улучшать наш код до тех пор, пока он не заработает

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

консолью.

Дейв: Круто! В каталоге у нас есть книга «7 навыков высокоэффективных людей»

(7 Habits of Highly Effective People). Давай посмотрим, что произойдет, если по-искать книги, соответствующие слову «Habit».

Тео заменяет значение запроса в листинге 15.10 на слово «Habit». Теперь код возвращает пустой массив, как показано в листинге 15.11. Это и ожидалось, поскольку

текущая реализация ищет только книги, название которых начинается со слова-запроса, а название искомой книги начинается с цифры: «7 Habits».

Листинг 15.11. Тестирование searchBooksByTitle

Catalog.searchBooksByTitle(catalogData, 'Habit');

// → []

Тео: Хочешь реализовать улучшенный поиск?

Дейв: Это не так уж сложно; мы уже реализовали hasWordStartingWith. Вот улучшенный поиск.

382

Часть 3. Удобство сопровождения

Листинг 15.12. Улучшенная версия поиска книг

Catalog.searchBooksByTitle = function(catalogData, query) {

console.log(JSON.stringify(catalogData));

console.log(JSON.stringify(query));

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

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

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

});

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

return Catalog.bookInfo(catalogData, book);

});

return bookInfos;

};

Тео: Мне нравится. Давай посмотрим, работает ли все должным образом.

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

Тео: Дейв, не делай этого!

Дейв: Не делать чего?

Тео: Не активируй конечную точку для тестирования своего кода.

Дейв: Почему?

Тео: Потому что среда REPL дает гораздо более быструю обратную связь, чем

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

среде.

Дейв выполняет свой код для улучшенного поиска по слову «Habit». И уже на этот

раз он возвращает детали о книге «7 навыков высокоэффективных людей».

Листинг 15.13. Повторное тестирование searchBooksByTitle

Catalog.searchBooksByTitle(catalogData, 'Habit');

// → [ { "title": "7 Habits of Highly Effective People", ...}]

Дейв: Сработало!

Тео: Давайте попробуем другие запросы: «abit» и «bit» не должны возвращать ни

одной книги, а «habit» и «7 habits» должны возвращать только одну книгу.

В среде REPL Дейв пробует выполнить четыре запроса, предложенные Тео. Для

«abit» и «bit» код работает так, как ожидалось, но для «habit» «и «7 Habits» он завершается сбоем.

Дейв: Дай-ка я попробую исправить этот код.

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

Глава 15. Отладка

383

Дейв: Хорошая идея. Можно ли использовать репродуцируемость в контексте

модульных тестов?

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

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

Дейв: Как нам использовать репродуцируемость в модульном тестировании?

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

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

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

Дейв: Я это помню! Я написал много модульных тестов для Системы управления

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

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

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

Дейв: Неплохо! Сейчас я напишу модульный тест для Catalog.searchBooksByTitle, используя этот подход.

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

вставляет ее в код модульного теста.

Листинг 15.14. Модульный тест с захваченными данными

var catalogData =

{"booksByIsbn":{"978-1982137274":{"isbn":"978-1982137274",

"title":"7 Habits of Highly Effective People","authorIds":["sean-covey",

"stephen-covey"]},"978-1779501127":{"isbn":"978-1779501127","title":

"Watchmen","publicationYear":1987,"authorIds":["alan-moore",

"dave-gibbons"]}},"authorsById":{"stephen-covey":{"name":

"Stephen Covey","bookIsbns":["978-1982137274"]},"sean-covey":

{"name":"Sean Covey","bookIsbns":["978-1982137274"]},"dave-gibbons":

{"name":"Dave Gibbons","bookIsbns":["978-1779501127"]},"alan-moore":

{"name":"Alan Moore","bookIsbns":["978-1779501127"]}}}; var query = "Habit";

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

var expectedResult = [

{

"authorNames": [

"Sean Covey",

"Stephen Covey",

],

384

Часть 3. Удобство сопровождения

"isbn": "978-1982137274",

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

}

];

_.isEqual(result, expectedResult);

// → true

Тео: Молодец! Хочешь, я покажу тебе, как сделать то же самое без копирования и

вставки?

Дейв: Определенно.

Тео: Вместо того чтобы выводить захваченные данные на консоль, мы запишем их

в файл и прочитаем данные из этого файла внутри модульного теста.

Дейв: Где ты сохранишь файлы, в которых хранятся захваченные данные?

Тео: Эти файлы являются частью модульных тестов. Они должны находиться в том

же дереве файлов, что и сами тесты.

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

уже существующий?

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

(например, названия функции, в которой были захвачены данные) и универсального уникального идентификатора (англ. universally unique identifier, UUID).

Дейв: А как сгенерировать UUID?

Тео: В некоторых языках программирования это составная часть, но в других языках, таких как JavaScript, понадобится сторонняя библиотека, например, uuid.

Я добавлю ее сайт тебе в закладки. У меня есть список всех библиотек для гене-рирования UUID. Его я тебе тоже пришлю в виде таблицы.

Тео добавляет сайт сторонней библиотеки uuid (https://github.com/uuidjs/uuid) в закладки на компьютере Дейва. Затем, используя свой ноутбук, он находит весь

список библиотек и отправляет его Дейву. Дейв получает электронное письмо и

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

Таблица 15.2. Библиотеки для генерации UUID

Язык Библиотека

UUID

JavaScript

https://github.com/uuidjs/uuid

Java java.util.UUID

C# Guid.NewGuid

Python uuid

Ruby SecureRandom

Глава 15. Отладка

385

Тео: Код для функции dataFilePath, которая получает контекст и возвращает путь

к файлу, довольно прост. Посмотри-ка.

Листинг 15.15. Вычисление пути к файлу для хранения захваченных данных

var capturedDataFolder = "test-data"; ❶

function dataFilePath(context) {

var uuid = generateUUID(); ❷

return capturedDataFolder

+ "/" + context

+ "-" + ".json"; ❸

}

❶ Корневая папка для захваченных данных.

❷ Генерация UUID зависит от языка (см. табл. 15.2).

❸ Использует json в качестве расширения файла, потому что мы сериализуем данные

в JSON.

Дейв: Как нам хранить фрагмент данных в файле JSON?

Тео: Нужно сериализовать его и записать на диск.

Дейв: Синхронно или асинхронно?

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

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

Листинг 15.16. Выгрузка данных в формат JSON

function dumpData(data, context) {

var path = dataFilePath(context);

var content = JSON.stringify(data);

fs.writeFile(path, content, function () { ❶

console.log("Data for " +

context +

"stored in: " +

path); ❸

});

}

❶ Записывает асинхронно, чтобы предотвратить блокировку реальной работы.

❷ Третий аргумент — это функция обратного вызова, вызываемая по завершении записи.

❸ Отображает сообщение после записи данных в файл.

Дейв: Мне интересно посмотреть, смогу ли я использовать dumpData внутри

Catalog.searchBooksByTitle и захватить контекст в файл. Я думаю, вот так должно получиться.

386

Часть 3. Удобство сопровождения

Листинг 15.17. Захват контекста в файл

Catalog.searchBooksByTitle = function(catalogData, query) {

dumpData([catalogData, query], 'searchBooksByTitle');

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

var queryLowerCased = query.toLowerCase();

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

return _.get(book, "title")

.toLowerCase()

.startsWith(queryLowerCased);

});

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

return Catalog.bookInfo(catalogData, book);

});

return bookInfos;

};

Тео: Активируй конечную точку. Проверим, сработает ли код.

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

Листинг 15.18. Вывод консоли при активации конечной точки поиска

Data for searchBooksByTitle stored in

test-data/searchBooksByTitle-68e57c85-2213-471a-8442-c4516e83d786.json Листинг 15.19. Содержимое файла JSON, в котором захвачен контекст

[{"booksByIsbn":{"978-1982137274":{"isbn":"978-1982137274",

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

["sean-covey","stephen-covey"]},"978-1779501127":{"isbn":

"978-1779501127","title":"Watchmen","publicationYear":1987,

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

{"stephen-covey":{"name":"Stephen Covey","bookIsbns":

["978-1982137274"]},"sean-covey":{"name":"Sean Covey",

"bookIsbns":["978-1982137274"]},"dave-gibbons":

{"name":"Dave Gibbons","bookIsbns":["978-1779501127"]},

"alan-moore":{"name":"Alan Moore","bookIsbns":

["978-1779501127"]}}},"Habit"]

Дейв: Прочитать такой файл JSON очень сложно!

Тео: Если хочешь, можно сделать строку JSON более красивой.

Дейв: Как?

Глава 15. Отладка

387

Тео: Передав на JSON.stringify количество пробелов, которые будут использовать-ся для отступа. Сколько символов ты хочешь использовать?

Дейв: Два.

Добавив отступы в код dumpData, Дейв снова открывает файл JSON, упомянутый

в сообщении журнала (у него другое название!). Теперь он видит красивый JSON-массив с двумя элементами.

Листинг 15.20. Выгрузка данных в формат JSON с отступами

function dumpData(data, context) {

var path = dataFilePath(context);

var content = JSON.stringify(data, null, 2); ❶

fs.writeFile(path, content, function () {

console.log("Data for " + context + "stored in: " + path);

});

}

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

Листинг 15.21. Захваченный контекст с отступами в файле JSON

[

{

"booksByIsbn": {

"978-1982137274": {

"isbn": "978-1982137274",

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

"authorIds": [

"sean-covey",

"stephen-covey"

]

},

"978-1779501127": {

"isbn": "978-1779501127",

"title": "Watchmen",

"publicationYear": 1987,

"authorIds": [

"alan-moore",

"dave-gibbons"

]

}

},

"authorsById": {

"stephen-covey": {

"name": "Stephen Covey",

388

Часть 3. Удобство сопровождения

"bookIsbns": [

"978-1982137274"

]

},

"sean-covey": {

"name": "Sean Covey",

"bookIsbns": [

"978-1982137274"

]

},

"dave-gibbons": {

"name": "Dave Gibbons",

"bookIsbns": [

"978-1779501127"

]

},

"alan-moore": {

"name": "Alan Moore",

"bookIsbns": [

"978-1779501127"

]

}

}

},

"Habit"

]

Дейв: Когда я просматривал содержимое файла JSON, я подумал о том, что мы

записываем данные в файл асинхронным способом. Это означает, что данные

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

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

Дейв: Это я понял. Что произойдет, если код функции изменит данные, которые

мы записываем? Запишутся ли в файл исходные или измененные данные?

Тео: Я предоставлю тебе возможность поразмышлять над этим, а сам схожу в му-зейное кафе за чашечкой чая. А ты не хочешь кофе?

Дейв: Ты что, не будешь пить кофе?

Тео: Я наконец-то прочитал книгу Чарльза Дахигга «Сила привычки». Джо прочитал ее и перестал грызть ногти, поэтому я тоже решил почитать, чтобы избавиться от своей привычки постоянно пить кофе.

Дейв: Впечатляет, но мне, пожалуйста, эспрессо.

Пока Тео отлучился в кафе, Дейв принялся осматривать выставку «Стрелы ветра»

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

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

Глава 15. Отладка

389

пару минут к нему приходит вдохновение. Эврика! Он нашел ответ на вопрос

о функции, изменяющей данные.

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

зале. Дейв улыбается Тео и говорит:

Дейв: В ДОП мы никогда не изменяем данные. Таким образом, мой вопрос — это

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

записываем их в файл.

Тео: У тебя получилось! Сейчас я покажу тебе, как использовать данные из файла

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

ReadData.

Листинг 15.22. Чтение данных из файла JSON

function readData(path) {

return JSON.parse(fs.readFileSync(path));

}

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

захватывали данные?

Тео: Потому что функция ReadData предназначена для использования внутри

модульного теста, и мы не можем запустить тест до того, как данные будут счи-таны из файла.

Дейв: Логично. Использовать ReadData внутри модульного теста кажется простой

задачей. Можно я попробую прочитать наши захваченные данные?

Листинг 15.23. Модульный тест, который считывает захваченные данные из файла

var data = readData("test-data/" +

"searchBooksByTitle-68e57c85-2213-471a-8442-c4516e83d786.json"); var catalogData = data[0];

var query = data[1];

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

var expectedResult = [

{

"authorNames": [

"Sean Covey",

"Stephen Covey",

],

"isbn": "978-1982137274",

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

}

];

390

Часть 3. Удобство сопровождения

_.isEqual(result, expectedResult);

// → false

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

или с данными, считанными из файла?

Дейв: По ситуации. Когда объем данных минимален, мне нравится, чтобы данные

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

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

затрудняет чтение кода.

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

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

Дейв: Я об этом совсем забыл. Ты помнишь эти два запроса?

Тео: Да, «habit» и «7 Habits».

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

и запрос в нижний регистр.

Тео: А как насчет второго запроса?

Дейв: С ним гораздо труднее разобраться, потому что он состоит из двух слов. Мне

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

эти два префикса.

Тео: Ты знаком с метасимволом регулярного выражения \b?

Дейв: Нет.

Тео: Метасимвол \b соответствует позиции, которая называется границей слова.

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

Дейв: Круто. А приведешь мне пример?

Тео: Конечно. Например, по запросам «\bHabits» и «\b7 Habits» будет выдана книга

«7 Habits of Highly Effective People», а по запросу «abits» — нет.

Дейв: А как насчет запроса «\bHabits of»?

Тео: Тоже получится совпадение.

Дейв: Прекрасно. Это именно то, что мне нужно! Сейчас я исправлю код

hasWordStartingWith так, чтобы он выполнял проверку совпадения префикса без

учета регистра.

Листинг 15.24. Обновленная версия hasWordStartingWith

function hasWordStartingWith(sentence, prefix) {

var sentenceLowerCase = sentence.toLowerCase();

var prefixLowerCase = prefix.toLowerCase();

var prefixRegExp = new RegExp("\\b" + ❶

prefixLowerCase);

Глава 15. Отладка

391

return sentenceLowerCase.match(prefixRegExp) != null;

}

❶ При передаче \b конструктору регулярных выражений RegExp понадобится дополнительная обратная косая черта.

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

Дейв: По одному тесту на запрос?

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

не должны возвращать книг. Минуту.

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

Листинг 15.25. Модульный тест для нескольких запросов,

которые должны возвращать книгу

var data =

readData("test-data/" +

"searchBooksByTitle-68e57c85-2213-471a-8442-c4516e83d786.json"); var catalogData = data[0];

var queries = ["Habit", "habit", "7 Habit", "habits of"]; var expectedResult = [

{

"authorNames": [

"Sean Covey",

"Stephen Covey",

],

"isbn": "978-1982137274",

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

}

];

_.every(queries, function(query) {

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

return _.isEqual(result, expectedResult);

});

// → [true, true, true, true]

Листинг 15.26. Модульный тест для нескольких запросов,

которые не должны возвращать книгу

var data =

readData("test-data/" +

"searchBooksByTitle-68e57c85-2213-471a-8442-c4516e83d786.json"); var catalogData = data[0];

var queries = ["abit", "bit", "7 abit", "habit of"]; var expectedResult = [ ];

392

Часть 3. Удобство сопровождения

_.every(queries, function(query) {

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

return _.isEqual(result, expectedResult);

});

// → [true, true, true, true]

Дейв: Что такое _.every?

Тео: Это функция Lodash, которая получает коллекцию и предикат, возвращая true, если предикат возвращает true для каждого элемента коллекции.

Дейв: Неплохо!

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

Дейв: Так, а можно мне активировать конечную точку поиска с запросом

«7 Habits», чтобы подтвердить, что улучшенный поиск работает как положено?

Тео: Конечно. Я настоятельно советую не запускать систему извне только во время

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

Дейв активирует конечную точку поиска с запросом «7 Habits». Как и ожидалось, код возвращает информацию о книге «7 Habits of Highly Effective People».

15.5. Работа с внешними источниками данных

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

данных из внешнего источника данных, такого как база данных или внешний

сервис?

Тео: Почему бы и нет?

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

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

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

ответ.

Дейв: Я никогда не слышал о неизменяемых базах данных.

Тео: Иногда их называют функциональными базами данных или базами данных

только для добавления.

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

Тео: Базы данных, доступные только для чтения, конечно, неизменяемы, но они не

подходят для хранения состояния приложения.

Глава 15. Отладка

393

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

Тео: При наличии времени.

Дейв: Какое отношение время имеет к неизменяемости?

Тео: В неизменяемой базе данных запись имеет автоматически сгенерированную

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

с новой временно´й меткой. Более того, запрос всегда имеет временно´й диапазон

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

Дейв: И как это гарантирует, что на один и тот же запрос всегда будет возвращаться один и тот же ответ?

Тео: В неизменяемой базе данных запросы не работают с самой базой данных.

Вместо этого они работают с моментальным снимком базы данных, который

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

гарантированно возвращаются одни и те же ответы.

Дейв: Существуют ли такие базы данных на самом деле?

Тео: Да. Например, неизменяемая база данных Datomic используется некоторыми

цифровыми банками.

ПРИМЕЧАНИЕ. Посетите https://www.datomic.com, чтобы узнать больше о транзакцион-ной базе данных Datomic.

Дейв: Но большинство баз данных такого гарантировать не могут!

Тео: Это так, но на практике, когда мы отлаживаем проблему в нашей локальной

среде, данные обычно не меняются.

Дейв: Что ты имеешь в виду?

Тео: Возьмем, к примеру, базу данных у Klafim. Теоретически между моментом

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

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

на поисковый запрос будет другим.

Дейв: Именно.

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

локальной среде. Следовательно, такого произойти не должно.

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

аналогию?

Тео: Конечно!

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

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

394

Часть 3. Удобство сопровождения

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

стороне изображен Альберт Эйнштейн и его знаменитая цитата: «Бог не играет

в кости со Вселенной», на другой стороне — Алан Кэй и его цитата: «Делайте что

хотите, только не вмешивайтесь во внутреннее состояние».

Дейв благодарит Тео за такой подарок. У Тео к горлу подступает комок. Ему очень

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

Прощание

Через неделю после встречи с Дейвом в музее Тео приглашает Джо и Нэнси на

свою прощальную вечеринку в офисе Albatross. Там Джо впервые встретился

с Нэнси, и Тео решил сообщить предпринимательнице, что проект Klafim уложился

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

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

Тео: А я хочу сделать перерыв.

Дейв: Серьезно?

Тео: Да. Я собираюсь несколько месяцев путешествовать по миру.

Дейв: А после этого ты вернешься к работе в программировании?

Тео: Еще не уверен.

Дейв: У тебя на примете другие проекты?

Тео: Я подумываю о том, чтобы написать книгу.

Дейв: Книгу?

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

Дейв: Что ж, если ты такой же хороший рассказчик, как и учитель, я уверен, что

твоя книга станет бестселлером.

Тео: Спасибо тебе, Дейв!

Моника, Дейв, Нэнси, Джо и все остальные сотрудники Albatross поднимают бока-лы и кричат: «За Тео! За успех его книги!».

Итоги

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

функция, и воспроизводя его либо в среде REPL, либо в модульном тесте. В этой

главе мы называем это захватом контекста.

Глава 15. Отладка

395

В ДОП контекст функции состоит только из данных.

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

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

Воспроизведение сценария в среде REPL предоставляет короткий цикл обратной

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

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

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

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

возвращаемым значениям.

Контекст функции состоит из значений аргументов этой функции.

Среда REPL (англ. read-eval-print loop, цикл «чтение — вычисление — вывод»), иногда называемая языковой оболочкой, представляет собой программную среду, которая берет фрагменты кода, выполняет их и отображает результат.

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

Репродуцируемость позволяет восстановить сценарий в нетронутой среде.

Двумя условиями репродуцируемости в программировании являются неизменяемость и легкость (де-)сериализации.

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

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

find(coll, pred)

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

для которого pred возвращает true

396

Часть 3. Удобство сопровождения

Приложение А

Принципы

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

Дата-ориентированное программирование (ДОП) — это парадигма программирования, направленная на упрощение проектирования и реализации программных

систем, в которых в центре внимания находится информация, таких, например, как

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

СОВЕТ. В ДОП к данным относятся как к привилегированным гражданам.

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

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

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

Код (поведение) отделен от данных.

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

Данные рассматриваются как неизменяемые.

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

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

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

Обратите внимание, что принципы ДОП не связаны с языком. Их можно придерживаться (или нарушать) на:

языках объектно-ориентированного программирования (ООП), таких как Java, C#, C++ и других;

398

Приложение А

Рис. А.1. Принципы дата-ориентированного программирования

языках функционального программирования (ФП), таких как Clojure, OCaml, Haskell и других;

языках, поддерживающих как ООП, так и ФП, таких как JavaScript, Python, Ruby, Scala и других.

СОВЕТ. Принципы ДОП не связаны с языком.

ПРИМЕЧАНИЕ. Разработчикам ООП для перехода на ДОП может потребоваться сильнее

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

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

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

простых фрагментов кода. На протяжении всей книги подробно рассматривается

применение принципов ДОП к готовым информационным системам.

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

Принцип № 1 — это принцип проектирования, который рекомендует четкое разделение между кодом (поведением) и данными. Может показаться, что эта идея

лежит в основе ФП, но на самом деле такого принципа можно придерживаться (или

нарушать его) либо в ФП, либо в ООП.

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

399

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

Нарушать этот принцип в ФП означает скрывать состояние в лексической области действия функции.

Кроме того, этот принцип не относится к способу представления данных. Представление данных регулируется принципом № 2.

ПРИНЦИП № 1. Отделяйте код от данных таким образом, чтобы код заключался в функциях, поведение которых не зависит от данных, инкапсулированных в контексте функции.

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

Наше исследование принципа № 1 начинается с иллюстрации того, как он может

быть применен к ООП и ФП. Следующие разделы иллюстрируют, как этот принцип

может быть соблюден или нарушен в простой программе, которая работает с: объектом для имени автора с полями firstName и lastName, а также количеством

книг books, которые это автор написал;

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

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

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

Нарушение принципа № 1 в ООП

Нарушение принципа № 1 в ООП происходит, когда мы пишем код, объединяющий

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

выглядит.

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

class Author {

constructor(firstName, lastName, books) {

this.firstName = firstName;

this.lastName = lastName;

this.books = books;

}

fullName() {

return this.firstName + " " + this.lastName;

}

isProlific() {

return this.books > 100;

}

}

var obj = new Author("Isaac", "Asimov", 500); ❶

obj.fullName();

// → "Isaac Asimov"

❶ Да-да, Айзек Азимов написал около 500 книг!

400

Приложение А

Нарушение принципа № 1 в ФП

Нарушить этот принцип без классов в ФП означает скрыть данные в лексической

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

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

function createAuthorObject(firstName, lastName, books) {

return {

fullName: function() {

return firstName + " " + lastName;

},

isProlific: function () {

return books > 100;

}

};

}

var obj = createAuthorObject("Isaac", "Asimov", 500); obj.fullName();

// → "Isaac Asimov"

Соблюдение принципа № 1 в ООП

В листинге A.3 показан пример, в котором код придерживается принципа № 1

ООП. Исполнение этого принципа может быть достигнуто даже с классами путем

написания программ таким образом, чтобы:

код состоял из статических методов;

данные инкапсулировались в классах данных (классах, которые являются только

лишь контейнерами для данных).

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

class AuthorData {

constructor(firstName, lastName, books) {

this.firstName = firstName;

this.lastName = lastName;

this.books = books;

}

}

class NameCalculation {

static fullName(data) {

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

}

}

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

401

class AuthorRating {

static isProlific (data) {

return data.books > 100;

}

}

var data = new AuthorData("Isaac", "Asimov", 500); NameCalculation.fullName(data);

// → "Isaac Asimov"

Соблюдение принципа № 1 в ФП

В листинге A.4 показан пример, в котором код придерживается принципа № 1

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

Листинг A.4. Следование принципу № 1 в ФП

function createAuthorData(firstName, lastName, books) {

return {

firstName: firstName,

lastName: lastName,

books: books

};

}

function fullName(data) {

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

}

function isProlific (data) {

return data.books > 100;

}

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

// → "Isaac Asimov"

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

Мы проиллюстрировали, как можно следовать принципу № 1 или нарушать его

в ООП и ФП. Теперь обсудим преимущества, которые принцип № 1 привносит

в наши программы. Вот как тщательное отделение кода от данных идет на пользу

программам:

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

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

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

402

Приложение А

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

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

Представьте, что помимо объекта для сведений об авторе книги существует объект

для читателя библиотеки, который не имеет никакого отношения к авторам, но

имеет два таких же поля данных, что и объект для автора: firstName и lastName.

Логика вычисления полного имени одинакова для авторов и читателей: получить

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

и в версии с createAuthorObject в листинге A.5, код для вычисления fullName не может быть использован для имени читателя библиотеки непосредственно, поскольку

он заблокирован внутри класса Author.

Листинг A.5. Код для fullName заблокирован в классе Author

class Author {

constructor(firstName, lastName, books) {

this.firstName = firstName;

this.lastName = lastName;

this.books = books;

}

fullName() {

return this.firstName + " " + this.lastName;

}

isProlific() {

return this.books > 100;

}

}

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

композиция, чтобы разрешить классам User и Author использовать один и тот же

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

сложности.

В листинге A.6 показан простой способ избежать наследования. В этом листинге

мы дублируем код fullName внутри функции createUserObject.

Листинг A.6. Дублирование кода в ООП во избежание наследования

function createAuthorObject(firstName, lastName, books) {

var data = {firstName: firstName, lastName: lastName, books:

books};

return {

fullName: function fullName() {

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

}

};

}

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

403

function createUserObject(firstName, lastName, email) {

var data = {firstName: firstName, lastName: lastName, email:

email};

return {

fullName: function fullName() {

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

}

};

}

var obj = createUserObject("John", "Doe", "[email protected]"); obj.fullName();

// → "John Doe"

В ДОП-коде, который работает с объектами для имени автора, не требуется проводить никаких модификаций: этот код и так подойдет для объектов со сведениями

об имени читателя, потому что:

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

занимается созданием данных для автора;

функция, которая вычисляет полное имя, работает с любой хеш-картой, имеющей поля firstName и lastName.

Можно выгодно использовать тот факт, что данные, относящиеся к вычислению

полного имени читателя библиотеки и автора, имеют одинаковую форму. Без

каких-либо изменений функция fullName корректно работает как с данными для

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

Листинг A.7. Один и тот же код для объектов данных разных типов (в стиле ФП) function createAuthorData(firstName, lastName, books) {

return {firstName: firstName, lastName: lastName, books:

books};

}

function fullName(data) {

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

}

function createUserData(firstName, lastName, email) {

return {firstName: firstName, lastName: lastName, email:

email};

}

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

404

Приложение А

var userData = createUserData("John", "Doe", "[email protected]"); fullName(userData);

// → "John Doe"

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

как Java или C, нам придется создать общий интерфейс для AuthorData и UserData.

Однако в динамически типизированном языке, таком как JavaScript, это не требуется. Код для NameCalculation.fullName() работает с данными как для автора, так и для

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

Листинг A.8. Один и тот же код для объектов данных разных типов (в стиле ООП) class AuthorData {

constructor(firstName, lastName, books) {

this.firstName = firstName;

this.lastName = lastName;

this.books = books;

}

}

class NameCalculation {

static fullName(data) {

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

}

}

class UserData {

constructor(firstName, lastName, email) {

this.firstName = firstName;

this.lastName = lastName;

this.email = email;

}

}

var userData = new UserData("John", "Doe", "[email protected]"); NameCalculation.fullName(userData);

var authorData = new AuthorData("Isaac", "Asimov", 500); NameCalculation.fullName(authorData);

// → "John Doe"

СОВЕТ. Когда код отделен от данных, его можно использовать повторно в разных контекстах непосредственным образом. Это преимущество достижимо как в ФП, так и в ООП.

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

405

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

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

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

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

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

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

Листинг A.9. Тестирование кода, когда код и данные смешаны

var author = createAuthorObject("Isaac", "Asimov", 500); author.fullName() === "Isaac Asimov"

// → true

В этом простом сценарии смешение кода и данных не так уж и обременительно.

Мы всего лишь загружаем (без необходимости) код для isProlific. А вот в реальной ситуации создание экземпляра объекта может потребовать сложной и утоми-тельной настройки.

По версии ДОП, где createAuthorData и fullName разделены, мы можем создавать

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

Листинг A.10. Тестирование кода изолированно (в стиле ФП)

var author = {

firstName: "Isaac",

lastName: "Asimov"

};

fullName(author) === "Isaac Asimov"

// → true

Если используются классы, необходимо просто создать экземпляр объекта данных.

Нам не нужно загружать код для функции isProlific, которая не находится в том

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

Листинг A.11. Тестирование кода изолированно (в стиле ООП)

Назад: 11.7. Доставка в срок
Дальше: Преимущество № 3: системы, как правило, менее сложны