Книга: Дата-ориентированное программирование: Пер. с англ.
Назад: Глава 10. Операции с базой данных
Дальше: Итоги

"required": ["title", "isbn", "available"],

"properties": {

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

"available": {"type": "boolean"},

294

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

"publishers": {

"type": "array",

"items": {"type": "string"}

},

"number_of_pages": {"type": "integer"},

"weight": {"type": "string"},

"physical_format": {"type": "string"},

"subjects": {

"type": "array",

"items": {"type": "string"}

},

"isbn": {"type": "string"},

"publish_date": {"type": "string"},

"physical_dimensions": {"type": "string"}

}

};

Листинг 11.25. Схема для конечной точки расширенного поиска

(объединяет фрагменты)

class Catalog {

static enrichedSearchBooksByTitle(request) {

if(!ajv.validate(searchBooksRequestSchema, request)) {

var errors = ajv.errorsText(ajv.errors);

throw "Invalid request:" + errors;

}

var title = _.get(request, "title");

var fields = _.get(request, "fields");

var dbBookInfos = CatalogDataSource.matchingBooks(title);

var isbns = _.map(dbBookInfos, "isbn");

var openLibBookInfos =

OpenLibraryDataSource.multipleBookInfo(isbns,

fields);

var response = joinArrays(dbBookInfos, openLibBookInfos);

if(!ajv.validate(searchBooksResponseSchema, request)) {

var errors = ajv.errorsText(ajv.errors);

throw "Invalid response:" + errors;

}

return response;

}

}

Глава 11. Веб-сервисы

295

class Library {

static searchBooksByTitle(payloadBody) {

var payloadData = JSON.parse(payloadBody);

var results = Catalog.searchBooksByTitle(payloadData);

return JSON.stringify(results);

}

}

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

Джо прерывает момент медитации Тео. Просмотрев код в предыдущих списках, он

поздравляет Тео.

Джо: Отличная работа, мой друг! Кстати, прочитав книгу «The Power of Habit», я перестал грызть ногти.

Тео: Ух ты! Это потрясающе! Может быть, мне стоит прочитать эту книгу, чтобы

преодолеть свою привычку пить слишком много кофе.

Джо: Спасибо, и удачи с привычкой пить кофе.

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

функция готова.

Джо: Возможно, вам следует сказать ей, что она будет готова через неделю, что

даст вам время начать работу над следующим этапом.

11.7. Доставка в срок

Джо был прав! Тео вспоминает историю Джо о молодом дровосеке и старике. Тео

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

свою пилу и перейти на более глубокий уровень практики».

ПРИМЕЧАНИЕ. Если вы не можете вспомнить историю или пропустили ее, ознакомьтесь

с вступлением к части 2.

Проект Klafim увенчался успехом. Нэнси довольна. Босс Тео доволен. Тео получил

повышение. Чего еще может желать человек?

Тео помнит свою сделку с Джо. Прогуливаясь по магазинам Вестфилдского центра

Сан-Франциско в поисках подарка для детей Джо, Нерайи и Аурелии, он преиспол-нен чувства достигнутой цели и огромного удовольствия. Он покупает беспилотник

DJI Mavic Air 2 для Нерайи и новейшие Apple Airpod Pro для Аурелии. Он также

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

жены Джейн. Это способ поблагодарить ее за то, что она выдержала его долгие дни

на работе с самого начала проекта Klafim.

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

296

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

Итоги

Мы строим внутреннюю часть наших систем так же, как строим внешнюю.

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

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

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

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

того, чтобы:

• реализовать каждый шаг потока данных внутри веб-сервиса;

• проанализировать запрос от клиента;

• применить бизнес-логику к запросу;

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

веб-сервисов);

• применить бизнес-логику к ответам из внешних источников;

• сериализовать ответ клиенту.

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

предметной области.

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

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

keyBy(coll, f)

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

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

для каждого ключа является последним элементом, ответственным

за генерацию ключа

Часть 3

Удобство сопровождения

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

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

Моника, босс Тео, решает назначить Дейва для обслуживания проекта Klafim.

В этом есть смысл. За последние несколько месяцев Дейв продемонстрировал

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

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

Тео: Привет, Джо. Будет ли у вас время в течение следующих нескольких недель, чтобы научить Дейва принципам ДОП?

Джо: Да, но я предпочту этого не делать.

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

Джо: Это не имеет никакого отношения к твоему опыту, Дейв.

Тео: Тогда почему нет?

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

Тео: Но я даже не знаю всех частей ДОП!

Дейв: Давай же! Никакой ложной скромности, мой друг.

Джо: Знание никогда не бывает полным. Как говорил великий Сократ: «Чем больше я знаю, тем больше я понимаю, что ничего не знаю». Я уверен, что вы сможете самостоятельно выучить недостающие части и, возможно, даже изобрести

некоторые из них.

Тео: Как я смогу изобрести недостающие части?

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

298

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

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

практиковать ДОП, я совершенно уверен, что вы тоже придумаете какие-нибудь

собственные изобретения.

Тео: Что ты скажешь, Дейв? Готов ли ты научиться ДОП у меня?

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

Тео: Джо, будете ли вы по-прежнему доступны, если нам время от времени понадобится ваша помощь?

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

Расширенная проверка данных

Подарок, сделанный своими руками

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

Проверка аргументов функции.

Проверки возвращаемых функцией значений.

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

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

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

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

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

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

ошибок.

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

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

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

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

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

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

12.1. Проверка аргументов функции

Первая задача Дейва — внедрить пару новых конечных точек HTTP для загрузки

каталога в виде CSV-файла, поиска книг по авторам и оценки книг. Как только он

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

ПРИМЕЧАНИЕ. Участие Дейва в проекте Klafim объясняется в начале части 3. Пожалуйста, найдите минутку, чтобы прочитать вступительное слово, если вы его пропустили.

300

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

Тео: Было ли трудно разобраться в коде ДОП?

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

Тео: Круто!

Дейв: Но есть кое-что, к чему я не могу привыкнуть.

Тео: Что же это?

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

данные. В ООП я знаю ожидаемую форму аргументов для каждой функции.

Тео: Проверял ли ты данные на границах системы, как это сделал я?

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

Тео: Отлично!

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

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

ошибке.

Тео: Тогда в чем проблема?

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

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

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

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

Дейв: Собираемся ли мы также использовать схему JSON?

Тео: Да.

Дейв: Круто... Мне нравится JSON-схема.

Тео: Основная цель проверки данных на границах системы — предотвратить попа-дание в систему недопустимых данных, в то время как основная цель проверки

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

для визуализации нарисовать таблицу на доске (табл. 12.1).

Таблица 12.1. Два вида проверки данных

Вид проверки данных

Цель

Среда

Границы Защита Производство

Внутри Простота

разработки

Разработка

Глава 12. Расширенная проверка данных

301

Дейв: Упрощая разработку системы, ты имеешь в виду помощь разработчикам

в понимании ожидаемой формы аргументов функции, как в ООП?

Тео: Именно.

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

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

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

в базе данных.

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

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

Дейв: Когда ты говоришь о более интересной схеме данных, ты имеешь в виду более сложную для написания?

Тео: Более сложную для написания, но в то же время более проницательную.

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

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

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

Листинг 12.1. Реализация поиска без проверки данных

class Catalog {

static authorNames(catalogData, book) {

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

var names = _.map(authorIds, function(authorId) {

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

});

return names;

}

static bookInfo(catalogData, book) {

var bookInfo = {

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

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

"authorNames": Catalog.authorNames(catalogData, book)

};

return bookInfo;

}

static searchBooksByTitle(catalogData, query) {

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

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

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

});

302

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

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

return Catalog.bookInfo(catalogData, book);

});

return bookInfos;

}

}

Тео: Дейв, пожалуйста, напомни мне об ожидаемых формах для catalogData и query.

Дейв: query должен быть строкой, а catalogData — картой, соответствующей модели данных каталога.

Тео: Что такое модель данных каталога?

Дейв: Дай-ка посмотрю. Я где-то видел ее схему.

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

Klafim. Найдя то, что искал, он рисует схему на доске (рис. 12.1).

Рис. 12.1. Модель данных каталога

ПРИМЕЧАНИЕ. Схемы, представленные в этой книге, используют схему JSON версии 2020-12.

Тео: Можешь ли ты написать схему JSON для модели данных каталога?

Дейв: Разрешено ли мне использовать внутренние переменные для схем книг и

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

Тео: Схема JSON является частью кода. Если ты чувствуешь, что использование

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

Дейв: Хорошо. Теперь мне нужен чертеж схемы JSON, который Джо подарил тебе.

Глава 12. Расширенная проверка данных

303

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

Он дает Дейву шпаргалку схемы JSON.

Листинг 12.2. Шпаргалка схемы JSON

{

"type": "array",❶

"items": {

"type": "object",❷

"properties": {❸

"myNumber": {"type": "number"},❹

"myString": {"type": "string"},❺

"myEnum": {"enum": ["myVal", "yourVal"]},❻

"myBool": {"type": "boolean"}❼

},

"required": ["myNumber", "myString"],❽

"additionalProperties": false❾

}

}

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

❷ Каждый элемент массива представляет собой карту.

❸ Свойства каждого поля на карте.

❹ myNumber — число.

❺ myString — строка.

❻ MyEnum — значение перечисления с двумя возможностями: myVal и yourVal.

❼ myBool — логическое значение.

❽ Обязательными полями на карте являются myNumber и myString. Другие поля являются

необязательными.

❾ Мы не разрешаем поля, которые явно не упомянуты в схеме.

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

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

в ДОП, наблюдает за реакцией Тео.

Листинг 12.3. Схема автора

var authorSchema = {

"type": "object",

"required": ["id", "name", "bookIsbns"],

"properties": {

"id": {"type": "string"},

"name": {"type": "string"},

"bookIsbns": {

"type": "array",

304

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

"items": {"type": "string"}

}

}

};

Тео: Отличная работа! Давай теперь перейдем к схеме книги.

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

Листинг 12.4. Схема экземпляра книги

var bookItemSchema = {

"type": "object",

"properties":{

"id": {"type": "string"},

"libId": {"type": "string"},

"purchaseDate": {"type": "string"},

"isLent": {"type": "boolean"}

},

"required": ["id", "libId", "purchaseDate", "isLent"]

};

var bookSchema = {

"type": "object",

"required": ["title", "isbn", "authorIds", "bookItems"],

"properties": {

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

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

"isbn": {"type": "string"},

"authorIds": {

"type": "array",

"items": {"type": "string"}

},

"bookItems": {

"type": "array",

"items": bookItemSchema

}

}

};

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

вложенные схемы в переменных, чтобы облегчить чтение схем.

Тео: Почему ты не включил publicationYear в список обязательных полей в схеме

книги?

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

Глава 12. Расширенная проверка данных

305

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

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

полями: booksByIsbn и authorsById. Оба значения должны быть индексами, пред-ставленными на диаграмме модели фигурными скобками. Я понятия не имею, как определить схему для индекса.

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

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

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

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

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

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

Тео: Все так. Мы называем карты для записей гетерогенными картами, а карты для

индексов — гомогенными картами.

СОВЕТ. В ДОП записи представлены в виде гетерогенных карт, тогда как индексы

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

Дейв: Тогда как мы определяем схему гомогенной карты в схеме JSON?

Тео: Я не знаю. Давай проверим онлайн-документацию по схеме JSON.

ПРИМЕЧАНИЕ. Смотрите https://json-schema.org /, чтобы получить доступ к онлайн-документации для схемы JSON версии 2020-12.

После нескольких минут копания в онлайн-документации схемы JSON Тео находит

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

чем принять решение.

Тео: Я думаю, мы могли бы использовать additionalProperties. Вот схема JSON для

однородной карты, где значения — числа.

Листинг 12.5. Схема JSON для однородной карты с численными значениями

{

"type": "object",

"additionalProperties": {"type": "number"}

}

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

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

схему. В этом случае это означает, что свойства, не упомянутые в схеме, должны иметь значение схемы, связанное с AdditionalProperties.

306

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

Дейв: Ага. Но какое это имеет отношение к однородным картам?

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

Дейв: Хитро!

СОВЕТ. В схеме JSON однородные строковые карты имеют type: object без properties и additionalProperties, связанных со схемой.

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

Тео быстро печатает на своем ноутбуке. Он показывает Дейву схему каталога.

Листинг 12.6. Схема данных каталога

var catalogSchema = {

"type": "object",

"properties": {

"booksByIsbn": {

"type": "object",

"additionalProperties": bookSchema

},

"authorsById": {

"type": "object",

"additionalProperties": authorSchema

}

},

"required": ["booksByIsbn", "authorsById"]

};

Дейв: Готовы ли мы подключить каталог и схему запроса к реализации Catalog.

searchBooksByTitle?

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

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

Дейв: Как мы объединим две схемы в одну?

Тео: Ты знаешь, что такое кортеж?

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

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

разной формы.

Дейв: Хорошо. Итак, как мы определяем кортежи в схеме JSON?

Еще раз Тео изучает онлайн-документацию по схеме JSON. К счастью, он добавил

страницу в закладки и в кратчайшие сроки находит нужную ему информацию.

Тео: Я нашел это! Например, мы используем prefixItems в определении кортежа, состоящего из строки и числа.

Глава 12. Расширенная проверка данных

307

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

Листинг 12.7. Схема кортежа, состоящего из строки и числа

{

"type": "array",

"prefixItems": [

{ "type": "string" },

{ "type": "number" }

]

}

Дейв: Понимаю. И как бы ты определил схему для аргументов Catalog.

searchBooksByTitle?

Тео: Ну, это кортеж размера 2, где первый элемент — это каталог, а второй —

строка.

Дейв: Что-то вроде этой схемы?

Листинг 12.8. Схема для аргументов Catalog.searchBooksByTitle

var searchBooksArgsSchema = {

"type": "array",

"prefixItems": [

catalogSchema,

{ "type": "string" },

]

};

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

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

Тео: Это похоже на то, как мы проверяем данные на границах системы. Основное

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

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

отключена, когда код выполняется в рабочей среде.

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

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

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

Дейв: Когда ты говоришь о времени разработки, включает ли это тестирование

и промежуточные среды?

Тео: Да, все среды, кроме производства.

Дейв: Понимаю. Это похоже на утверждения в Java. Они отключены в ujnjdjv-коде.

308

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

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

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

Листинг 12.9. Реализация поиска с проверкой аргументов функции

Catalog.searchBooksByTitle = function(catalogData, query) {

if(dev()) {❶

var args = [catalogData, query];

if(!ajv.validate(searchBooksArgsSchema, args)) {

var errors = ajv.errorsText(ajv.errors);

throw ("searchBooksByTitle called with invalid arguments: " +

errors);

}

}

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

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

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

});

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

return Catalog.bookInfo(catalogData, book);

});

return bookInfos;

};

❶ Реализация dev() зависит от среды выполнения: она возвращает true, когда код выполняется в среде разработки, и false, когда он выполняется в рабочей среде.

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

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

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

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

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

12.2. Проверка возвращаемого значения

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

функций?

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

Глава 12. Расширенная проверка данных

309

Дейв: Класс. Позволь мне попытаться написать схему JSON для возвращаемого

значения Catalog.searchBooksByTitle.

Через несколько минут Дейв придумывает схему. Сделав глубокий вдох, затем вы-дох, он показывает код Тео.

Листинг 12.10. Схема возвращаемого значения Catalog.searchBooksByTitle var searchBooksResponseSchema = {

"type": "array",

"items": {

"type": "object",

"required": ["title", "isbn", "authorNames"],

"properties": {

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

"isbn": {"type": "string"},

"authorNames": {

"type": "array",

"items": {"type": "string"}

}

}

}

};

Тео: Отличная работа! Не хочешь ли ты попробовать добавить проверку возвращаемого значения в код Catalog.searchBooksByTitle?

Дейв: Конечно, хочу!

Дейв немного работает в своей IDE. На этот раз немного более уверенно он показывает результат Тео.

Листинг 12.11. Поиск с проверкой данных как для ввода, так и для вывода

Catalog.searchBooksByTitle = function(catalogData, query) {

if(dev()) {

if(!ajv.validate(searchBooksArgsSchema, [catalogData,

query])) {

var errors = ajv.errorsText(ajv.errors);

throw ("searchBooksByTitle called with invalid arguments: " +

errors);

}

}

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

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

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

});

310

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

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

return Catalog.bookInfo(catalogData, book);

});

if(dev()) {

if(!ajv.validate(searchBooksResponseSchema, bookInfos)) {

var errors = ajv.errorsText(ajv.errors);

throw ("searchBooksByTitle returned an invalid value: " + errors);

}

}

return bookInfos;

};

Тео: Превосходно! Теперь нам нужно выяснить, как справиться с расширенной

проверкой данных.

12.3. Расширенная проверка данных

Дейв: Что ты подразумеваешь под расширенной проверкой данных?

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

Дейв: Можешь привести пример?

Тео: Конечно. Возьмем, к примеру, год издания книги. Это целое число, но что еще

ты мог бы сказать об этом числе?

Дейв: Это должно быть обязательно положительное число. Я бы сказал, что это

положительное целое число.

Тео: Давай, Дейв! Будь смелее, выйди за рамки типов.

Дейв: Я не знаю. Я бы сказал, что это число должно быть больше 1900. Я не думаю, что имеет смысл иметь книгу, опубликованную до 1900 года.

Тео: Точно. А как насчет более высокого предела?

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

Тео: Очень хорошо! Я вижу, что схема JSON поддерживает числовые диапазоны.

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

и должно находиться между 1900 и 2021 годами.

Листинг 12.12. Схема для целого числа между 1900 и 2021 годами

var publicationYearSchema = {

"type": "integer",

"minimum": 1900,

"maximum": 2021

};

Дейв: Почему такого рода проверка данных невозможна в ООП?

Тео: Я дам тебе возможность подумать об этом.

Глава 12. Расширенная проверка данных

311

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

компиляции. Во время компиляции у нас есть только информация о статических

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

данных ДОП можно выйти за рамки типов.

ПРИМЕЧАНИЕ. Конечно, в традиционном ООП также возможно написать пользовательскую проверку данных во время выполнения. Здесь, однако, мы сравниваем схему данных со

статическими типами.

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

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

ПРИМЕЧАНИЕ. Посетите http://mng.bz/OGNP для руководства по регулярным выражени-ям JavaScript.

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

должен быть UUID.

Дейв: Ага.

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

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

Листинг 12.13. Регулярное выражение для допустимого UUID

[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}

Дейв: Как нам теперь подключить регулярное выражение к схеме JSON?

Тео: Пока ты искал регулярное выражение UUID, я прочитал о поле pattern. Вот

как мы можем подключить регулярное выражение UUID к схеме JSON.

Листинг 12.14. Схема для UUID

var uuidSchema = {

"type": "string",

"pattern": "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}" +

"-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"

};

Дейв: Супер! Позволь мне улучшить схему каталога и уточнить схему для

PurchaseDate, isbn, libId и authorId с помощью регулярных выражений.

Тео: Однако прежде, чем ты это сделаешь, позволь мне рассказать кое-что, что я

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

Дейв: И как же это работает?

Тео: С помощью поля format.

312

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

ПРИМЕЧАНИЕ. Согласно спецификации схемы JSON поле format предназначено только

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

также используют format для проверки.

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

Листинг 12.15. Схема для даты

{

"type": "string",

"format": "date"

}

СОВЕТ. В ДОП проверка данных выходит за рамки статических типов (например, диапа-зонов чисел, регулярных выражений и т. д.).

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

схемы каталога?

Тео: Да, дерзай!

Дейву требуется немного времени, чтобы написать регулярные выражения для isbn, AuthorID и libId. Но с помощью Google (опять же) и небольшого упрощения Дейв

придумал схему:

Листинг 12.16. Уточненная схема данных каталога (часть 1)

var isbnSchema = {

"type": "string",

"pattern": "^[0-9-]{10,20}$"

};

var libIdSchema = {

"type": "string",

"pattern": "^[a-z0-9-]{3,20}$"

};

var authorIdSchema ={

"type": "string",

"pattern": "[a-z-]{2,50}"

};

var bookItemSchema = {

"type": "object",

"additionalProperties": {

"id": uuidSchema,

"libId": libIdSchema,

"purchaseDate": {

"type": "string",

"format": "date"

},

Глава 12. Расширенная проверка данных

313

"isLent": {"type": "boolean"}

}

};

Листинг 12.17. Уточненная схема данных каталога (часть 2)

var bookSchema = {

"type": "object",

"required": ["title", "isbn", "authorIds", "bookItems"],

"properties": {

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

"publicationYear": publicationYearSchema,

"isbn": isbnSchema,

"publisher": {"type": "string"},

"authorIds": {

"type": "array",

"items": authorIdSchema

},

"bookItems": bookItemSchema

}

};

var authorSchema = {

"type": "object",

"required": ["id", "name", "bookIsbns"],

"properties": {

"id": {"type": "string"},

"name": {"type": "string"},

"bookIsbns": {

"items": isbnSchema

}

}

};

var catalogSchema = {

"type": "object",

"properties": {

"booksByIsbn": {

"type": "object",

"additionalProperties": bookSchema

},

"authorsById": {

"type": "object",

"additionalProperties": authorSchema

}

},

"required": ["booksByIsbn", "authorsById"]

};

314

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

12.4. Автоматическое создание

диаграмм модели данных

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

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

начинает дискуссию.

Джо: Ребята, вы начинаете чувствовать силу проверки данных а-ля ДОП?

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

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

Тео: Мы также поняли, что нам не нужно проверять данные для каждой функции.

Джо: Верно. Теперь позвольте мне показать вам еще одну классную вещь, которую

мы можем сделать с помощью схемы JSON.

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

Джо: Генерация диаграммы модели данных.

Дейв: Ого! И как это работает?

Джо: Существуют инструменты, которые получают схему JSON в качестве входных данных и создают диаграмму в формате модели данных.

Дейв: Что такое формат модели данных?

Джо: Это формат, который позволяет вам определять модель данных в виде обыч-ного текста. После этого вы можете сгенерировать изображение из текста. Мой

любимый формат данных — PlantUML.

ПРИМЕЧАНИЕ. Подробнее о PlantUML на https://plantuml.com /.

Дейв: Знаете ли вы о других инструментах, которые генерируют диаграммы моделей данных?

Джо: Я использовал JSON Schema Viewer и Malli.

ПРИМЕЧАНИЕ. Вы можете найти информацию JSON Schema Viewer по адресу

https://navneethg.github.io/jsonschemaviewer/ и о Malli на https://github.com/metosin/malli.

Джо показывает Дейву и Тео диаграмму PlantUML, сгенерированную Malli (листинг 12.18) из схемы каталога в листингах 12.16 и 12.17.

Листинг 12.18. Схема PlantUML, созданная на основе схемы данных каталога

@startuml

Entity1 *-- Entity2

Entity1 *-- Entity4

Глава 12. Расширенная проверка данных

315

Entity2 *-- Entity3

class Entity1 {

+ booksByIsbn: {Entity2}

+ authorsById: {Entity4}

}

class Entity2 {

+ title : String

+ publicationYear: Number

+ isbn: String

+ authorIds: [String]

+ bookItems: [Entity3]

}

class Entity3 {

+ id: String

+ libId: String

+ purchaseDate: String

+ isLent: Boolean

}

class Entity4 {

+ id: String

+ name: String

+ bookIsbns: [String]

}

@enduml

Дейв: Можно ли визуализировать эту диаграмму?

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

ПРИМЕЧАНИЕ. Посетите https://www.planttext.com/, чтобы узнать об онлайн-инстру-менте PlantText больше.

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

Дейв: Это круто! Но почему объекты диаграммы называются Entity1, Entity2

и т. д.?

Джо: Потому что в схеме JSON нет способа присвоить имя схеме. Malli должен

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

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

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

316

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

Рис. 12.2. Визуализация диаграммы PlantUML, созданной на основе схемы данных каталога

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

Дейв: В любом случае это очень круто!

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

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

Дейв: Что она делает?

Джо: Автоматическую генерацию модульных тестов.

Тео: Звучит захватывающе!

12.5. Автоматическая генерация

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

Джо: После того как вы определили схему данных для аргументов функции и ее

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

этой функции.

Дейв: Как?

Джо: Что ж, подумайте об этом. В чем суть модульного теста для функции?

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

Джо: Точно! Теперь давайте адаптируем его к контексту схемы данных и ДОП.

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

Глава 12. Расширенная проверка данных

317

Дейв: Ладно.

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

случайными аргументами, которые соответствуют схеме аргументов функции.

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

возвращаемого значения. Позвольте мне нарисовать это.

Джо подходит к доске. Он рисует диаграмму, представленную на рис. 12.3.

Рис. 12.3. Поток модульного теста на основе схемы

Дейв: Как вы генерируете случайные данные, соответствующие схеме?

Джо: Используя такой инструмент, как JSON Schema Faker. Например, давайте

начнем с простой схемы: схемы для UUID. Позвольте мне показать вам, как

генерировать случайные данные, соответствующие схеме.

ПРИМЕЧАНИЕ. Вы найдете более подробную информацию о JSON Schema Faker на

https://github.com/json-schema-faker/json-schema-faker.

Джо некоторое время печатает на клавиатуре. Затем он показывает код для генерации случайных данных Дейву и Тео.

Листинг 12.19. Генерация случайных данных, соответствующих схеме UUID

var uuidSchema = {

"type": "string",

"pattern": "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}" +

"-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"

};

JSONSchemaFaker.generate(uuidSchema);

// −−>7aA8CdF3-14DF-9EF5-1A19-47dacdB16Fa9"

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

возвращает другой UUID.

Дейв: Очень круто! Позвольте мне посмотреть, как это работает с более сложными

схемами, такими как схема каталога.

318

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

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

Листинг 12.20. Генерация случайных данных, соответствующих схеме каталога

{

"booksByIsbn": {

"Excepteur7": {

"title": "elit veniam anim",

"isbn": "5419903-3563-7",

"authorIds": [

"vfbzqahmuemgdegkzntfhzcjhjrbgfoljfzogfuqweggchum",

"inxmqh-",

],

"bookItems": {

"ullamco5": {

"id": "f7dac8c3-E59D-bc2E-7B33-C27F3794E2d6",

"libId": "4jtbj7q7nrylfu114m",

"purchaseDate": "2001-08-01",

"isLent": false

},

"culpa_3e": {

"id": "423DCdDF-CDAe-2CAa-f956-C6cd9dA8054b",

"libId": "6wcxbh",

"purchaseDate": "1970-06-24",

"isLent": true

}

},

"publicationYear": 1930,

"publisher": "sunt do nisi"

},

"aliquip_d7": {

"title": "aute",

"isbn": "348782167518177",

"authorIds": ["owfgtdxjbiidsobfgvjpjlxuabqpjhdcqmmmrjb-ezrsz-u"],

"bookItems": {

"ipsum__0b": {

"id": "6DfE93ca-DB23-5856-56Fd-82Ab8CffEFF5",

"libId": "bvjh0p2p2666vs7dd",

"purchaseDate": "2018-03-30",

"isLent": false

}

},

"publisher": "ea anim ut ex id",

"publicationYear": 1928

}

},

Глава 12. Расширенная проверка данных

319

"authorsById": {

"labore_b88": {

"id": "adipisicing nulla proident",

"name": "culpa in minim",

"bookIsbns": [

"6243029--7",

"5557199424742986"

]

},

"ut_dee": {

"id": "Lorem officia culpa qui in",

"name": "aliquip eiusmod",

"bookIsbns": [

"0661-8-5772"

]

}

}

}

Джо: Я вижу, что у вас есть некоторые ошибки в ваших регулярных выражениях.

Тео: Как вы это видите?

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

ISBN.

Дейв: Вы правы. Я ненавижу регулярные выражения!

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

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

Catalog.searchBooksByTitle.

Листинг 12.21. Реализация потока модульного теста на основе схемы

function searchBooksTest () {

var catalogRandom = JSONSchemaFaker.generate(catalogSchema);

var queryRandom = JSONSchemaFaker.generate({ "type": "string"

});

Catalog.searchBooksByTitle(catalogRandom, queryRandom);

}

Дейв: Подождите минутку. Я не вижу, где вы проверяете, что Catalog.

searchBooksByTitle возвращает значение, соответствующее схеме возвращаемого

значения.

Тео: Если вы присмотритесь к коду повнимательнее, вы это увидите.

Дейв внимательно рассматривает код для Catalog.searchBooksByTitle. И теперь он

это видит.

320

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

Листинг 12.22. Реализация Catalog.searchBooksByTitle

Catalog.searchBooksByTitle = function(catalogData, query) {

if(dev()) {

if(!ajv.validate(searchBooksArgsSchema, [catalogData, query])) {

var errors = ajv.errorsText(ajv.errors);

throw ("searchBooksByTitle called with invalid arguments: " +

errors);

}

}

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

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

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

});

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

return Catalog.bookInfo(catalogData, book);

});

if(dev()) {

if(!ajv.validate(searchBooksResponseSchema, bookInfos)) {

var errors = ajv.errorsText(ajv.errors);

throw ("searchBooksByTitle returned an invalid value: " +

errors);

}

}

return bookInfos;

};

Дейв: Конечно! Это в коде Catalog.searchBooksByTitle. Если возвращаемое значение не соответствует схеме, оно выдает исключение, и тест завершается неудачей.

Джо: Верно. Теперь давайте улучшим код нашего модульного теста и вернем значение false при возникновении исключения внутри Catalog.searchBooksByTitle.

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

Листинг 12.23. Полный модульный тест на основе схемы данных для поиска книг

function searchBooksTest () {

var catalogRandom = JSONSchemaFaker.generate(catalogSchema);

var queryRandom = JSONSchemaFaker.generate({ "type": "string" }); try {

Catalog.searchBooksByTitle(catalogRandom, queryRandom);

return true;

} catch (error) {

return false;

}

Глава 12. Расширенная проверка данных

321

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

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

Дейв: Что?

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

Дейв: Как мы собираемся найти запрос, который гарантированно соответствует

хотя бы одной книге?

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

Джо быстро печатает и показывает Тео и Дейву свой усовершенствованный тест.

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

Листинг 12.24. Усовершенствованный модульный тест на основе схемы данных

для поиска книг

function searchBooksTest () {

var catalogRandom = JSONSchemaFaker.generate(catalogSchema);

var queryRandom = JSONSchemaFaker.generate({ "type": "string" }); try {

var firstBook = _.values(_.get(catalogRandom, "booksByIsbn"))[0]; var query = _.get(firstBook, "title").substring(0,1);

Catalog.searchBooksByTitle(catalogRandom, query);

return true;

} catch (error) {

return false;

}

}

Дейв: Понимаю. Это не так сложно, как я думал. Часто ли бывает так, что вам

нужно настроить случайные данные?

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

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

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

Листинг 12.25. Выполнение модульного теста на основе схемы

searchBooksTest();

// → false

322

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

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

Тео: Возможно, модульный тест выявил ошибку в реализации Catalog.

searchBooksByTitle.

Дейв: Давайте проверим это. Есть ли способ заставить модульный тест отображать

возвращаемое значение функции?

Джо: Да, вот оно.

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

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

для Catalog.searchBooksByTitle.

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

модульного теста

function searchBooksTest () {

var catalogRandom = JSONSchemaFaker.generate(catalogSchema);

var queryRandom = JSONSchemaFaker.generate({ "type": "string" }); try {

var firstBook = _.values(_.get(catalogRandom, "booksByIsbn"))[0]; var query = _.get(firstBook, "title").substring(0,1);

Catalog.searchBooksByTitle(catalogRandom, query);

return true;

} catch (error) {

console.log(error);

return false;

}

}

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

снова.

Листинг 12.27. Повторный запуск модульного теста на основе схемы

searchBooksTest();

// → searchBooksByTitle returned a value that doesn\'t conform to schema:

// data[0].authorNames[0] should be string,

// data[0].authorNames[1] should be string,

// data[1].authorNames[0] should be string

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

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

searchBooksByTitle, тогда как в схеме мы ожидаем строку.

Тео: И как же мы исправим это?

Дейв: Очень просто. Укажите, что Catalog.authorNames возвращает строку Not available, если автора нет в каталоге. Может быть, что-то вроде этого.

Глава 12. Расширенная проверка данных

323

Листинг 12.28. Исправление ошибки в реализации поиска книг

Catalog.authorNames = function(catalogData, book) {

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

var names = _.map(authorIds, function(authorId) {

return _.get(catalogData,

["authorsById", authorId, "name"],

"Not available");❶

});

return names;

};

❶ Когда с ключом ["authorsById", AuthorID, "name"] не связано значение, мы возвращаем

"Not available".

Дейв снова выполняет модульный тест. К счастью, на этот раз он успешен.

Листинг 12.29. Повторный запуск модульного теста на основе схемы

searchBooksTest();

// → true

Джо: Отлично сработано, Дейв!

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

обнаружить ошибку в реализации Catalog.searchBooksByTitle.

Джо: Не волнуйтесь. Такое случалось со мной много раз.

Дейв: Проверка данных а-ля ДОП — это действительно круто!

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

это нравится!

Дейв: Должен признаться, мне все еще не хватает одной классной функции IDE от

ООП.

Джо: Какой же?

Дейв: Автоматическое заполнение имен полей в классе.

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

в Clojure через clj-kondo и интеграцию, которую он обеспечивает с Malli.

ПРИМЕЧАНИЕ. Перейдите на https://github.com/clj-kondo/clj-kondo и https://github.com/

metosin/malli для функции автозаполнения, предоставляемой clj-kondo, и ее интеграции

с Malli.

Дейв: Как вы думаете, когда-нибудь эта функциональность будет доступна на других языках программирования?

Джо: Безусловно. IDE, такие как IntelliJ и Visual Studio Code, уже поддерживают

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

324

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

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

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

12.6. Новый подарок

Когда Джо уходит, Дейву приходит в голову интересная идея. Он делится ею с Тео.

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

Тео: Отличная идея! Но тебе придется сделать это самостоятельно. Мне нужно

бежать на встречу!

После своей встречи Тео возвращается к столу Дейва. Увидев Тео, Дейв берет со

своего стола маленький сверток, похожий на тот, что Джо подарил Тео несколько

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

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

«Шпаргалка по расширенной схеме JSON». Тео улыбается, просматривая схему

JSON (листинг 12.30). Затем он переворачивает бумагу и обнаруживает, что обратная сторона также заполнена рисунками, на этот раз клавиатурами и мышами.

В центре документа Тео читает надпись «Пример достоверных данных» (листинг 12.31).

Листинг 12.30. Шпаргалка по расширенной схеме JSON

{

"type": "array",❶

"items": {

"type": "object",❷

"properties": {❸

"myNumber": {"type": "number"},❹

"myString": {"type": "string"},❺

"myEnum": {"enum": ["myVal", "yourVal"]},❻

"myBool": {"type": "boolean"}❼

"myAge": {❽

"type": "integer",

"minimum": 0,

"maximum": 120

},

"myBirthday": {❾

"type": "string",

"format": "date"

},

Глава 12. Расширенная проверка данных

325

"myLetters": {❿

"type": "string",

"pattern": "[a-zA-Z]*"

}

"myNumberMap": {⓫

"type": "object",

"additionalProperties": {"type": "number"}

},

"myTuple": {⓬

"type": "array",

"prefixItems": [

{ "type": "string" },

{ "type": "number" }

]

}

},

"required": ["myNumber", "myString"],⓭

"additionalProperties": false⓮

}

}

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

❷ Каждый элемент массива представляет собой карту.

❸ Свойства каждого поля на карте.

❹ myNumber — это число.

❺ myString — это строка.

❻ MyEnum — это значение перечисления с двумя возможностями: "myVal" и "yourVal".

❼ myBool — это логическое значение.

❽ myAge — это целое число от 0 до 120.

❾ myBirthday — это строка, соответствующая формату даты.

❿ myLetters — это строка, состоящая только из букв (строчных или прописных).

⓫ myNumberMap — это однородная строковая карта, где все значения являются числами.

⓬ myTuple — это кортеж, где первым элементом является строка, а вторым элементом — число.

⓭ Обязательными полями на карте являются myNumber и myString. Другие поля являются

необязательными.

⓮ Мы не разрешаем поля, которые явно не упомянуты в схеме.

Листинг 12.31. Пример достоверных данных

[

{

"myNumber": 42,

"myString": "I-love-you",

"myEnum": "myVal",

"myBool": true,

326

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

"myTuple": ["Hello", 42]

},

{

"myNumber": 54,

"myString": "Happy",

"myAge": 42,

"myBirthday": "1978-11-23",

"myLetters": "Hello",

"myNumberMap": {

"banana": 23,

"apple": 34

}

}

]

Итоги

Мы определяем схемы данных, используя язык, подобный JSON-схеме, для

аргументов функций и возвращаемых значений.

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

форму аргументов функций, которые они хотят вызвать.

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

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

данных.

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

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

Мы визуализируем схему данных, генерируя диаграмму модели данных из схемы JSON.

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

основе схемы.

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

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

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

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

однородных карт.

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

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

Полиморфизм

Резвясь с животными в сельской местности

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

Имитация объектов с помощью мультиметодов (единичная

отправка).

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

(множественная отправка).

Динамическая реализация мультиметодов для нескольких

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

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

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

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

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

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

значения аргументов (динамическая отправка).

13.1. Сущность полиморфизма

Для сегодняшней сессии Дейв пригласил Тео приехать и навестить его в доме его

родителей в сельской местности. После того как Тео проехал через мост Золотые

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

в природу привело его в отличное настроение. Неплохой способ начать неделю!

Дейв встретил Тео в джинсах и футболке, что резко контрастировало с элегантной

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

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

328

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

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

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

в ООП и которой мне не хватает.

Тео: И что это за вещь?

Дейв: Полиморфизм.

Тео: А какой именно полиморфизм?

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

один и тот же интерфейс по-разному.

Тео: Понятно. И в чем, по-твоему, ценность полиморфизма?

Дейв: В том, что он позволяет нам отделить интерфейс от его реализаций.

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

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

классический полиморфизм в ООП при помощи животных.

Тео: Хорошая идея!

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

Тео: Прикольно, как в антропоморфных комиксах.

Дейв: Антро-что?

Тео: Это комиксы, где животные могут ходить, говорить и т. д., как Микки Маус.

Дейв: А, ну да, но такого термина я не знал. Откуда он произошел?

Тео: Слово «антропоморфизм» происходит от греческих слов anthropos, что означает «человек», и morphe, что означает «форма».

Дейв: Понятно. Значит, антропоморфная книга — это книга, в которой животные

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

Тео: Безусловно. Слово «полиморфизм» происходит от греческих слов polús, что

означает «много», и morphe, что, опять-таки, означает «форма».

Дейв: Логично. Полиморфизм — это способность разных объектов реализовывать

один и тот же метод по-разному. Это возвращает меня к моему примеру с животными. В ООП я бы определил интерфейс IAnimal с помощью метода greet, и каждый класс для животных реализовал бы метод greet по-своему. У меня как

раз есть пример.

Листинг 13.1. Полиморфизм в ООП, проиллюстрированный при помощи животных

interface IAnimal {

public void greet();

}

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

329

class Dog implements IAnimal {

private String name;

public void greet() {

System.out.println("Woof woof! My name is " + animal.name);

}

}

class Cat implements IAnimal {

private String name;

public void greet() {

System.out.println("Meow! I am " + animal.name);

}

}

class Cow implements IAnimal {

private String name;

public void greet() {

System.out.println("Moo! Call me " + animal.name);

}

}

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

между полиморфизмом в ООП и оператором switch?

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

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

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

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

сока уже допил.

Листинг 13.2. Свитч-кейс, где поведение зависит от типа

function greet(animal) {

switch (animal.type) {

case "dog":

console.log("Woof Woof! My name is: " + animal.name);

break;

case "cat":

console.log("Meow! I am: " + animal.name);

break;

case "cow":

console.log("Moo! Call me " + animal.name);

break;

};

}

330

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

Дейв: Как именно будет выглядеть animal?

Тео: Как я только что сказал, это карта с двумя полями: name и type. Сейчас я их

введу.

Листинг 13.3. Представление животных с помощью карт

var myDog = {

"type": "dog",

"name": "Fido"

};

var myCat = {

"type": "cat",

"name": "Milo"

};

var myCow = {

"type": "cow",

"name": "Clarabelle"

};

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

Тео: Безусловно. Это может быть что угодно.

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

с оператором switch и моим кодом с интерфейсом и тремя классами?

Тео: Ага.

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

Тео: Ты прав. Я это исправлю и валидирую входные данные.

Листинг 13.4. Валидация данных

var animalSchema = {

"type": "object",

"properties": {

"name": {"type": "string"},

"type": {"type": "string"}

},

"required": ["name", "type"],

};

function greet(animal) {

if(dev()) { ❶

if(!ajv.validate(animalSchema, animal)) {

var errors = ajv.errorsText(ajv.errors);

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

331

throw ("greet called with invalid arguments: " + errors);

}

}

switch (animal.type) {

case "dog":

console.log("Woof Woof! My name is: " + animal.name);

break;

case "cat":

console.log("Meow! I am: " + animal.name);

break;

case "cow":

console.log("Moo! Call me " + animal.name);

break;

};

}

❶ Подробнее о валидации данных читайте в главе 12.

ПРИМЕЧАНИЕ. Не используйте такие операторы switch в готовом коде. Мы показываем

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

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

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

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

Тео: Согласен, но я могу это исправить, создав отдельную функцию для каждого

животного, например так:

Листинг 13.5. Разные варианты реализации в разных функциях

function greetDog(animal) {

console.log("Woof Woof! My name is: " + animal.name);

}

function greetCat(animal) {

console.log("Meow! I am: " + animal.name);

}

function greetCow(animal) {

console.log("Moo! Call me " + animal.name);

}

function greet(animal) {

if(dev()) {

if(!ajv.validate(animalSchema, animal)) {

var errors = ajv.errorsText(ajv.errors);

throw ("greet called with invalid arguments: " + errors);

}

}

332

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

switch (animal.type) {

case "dog":

greetDog(animal);

break;

case "cat":

greetCat(animal);

break;

case "cow":

greetCow(animal);

break;

}

};

Дейв: Но что если ты захочешь расширить функциональность greet и добавить новое животное?

Тео: Вот теперь-то ты меня поймал. Признаю, что с помощью оператора switch я не

cмогу добавить новое животное без изменения исходного кода, тогда как в ООП

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

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

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

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

Тео отправляет сообщение Джо, чтобы спросить его о полиморфизме в ДОП. Джо

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

выступает с докладом о ДОП на технической конференции. Единственное, что он

успевает посоветовать Тео, это изучить мультиметоды.

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

Они решили попробовать мультиметоды после обеда.

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

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

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

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

Но кто сказал, что просто — это то же самое, что легко?

После обеда они решают выпить кофе. Дейв предлагает Тео самому перемолоть

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

продолжают свое исследование полиморфизма а-ля ДОП.

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

333

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

Дейв: Я не понимаю, как такое возможно.

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

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

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

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

будет разным в разных реализациях.

Дейв: Для меня это как-то абстрактно.

Тео: Кажется, я понял, как реализовать инструменты приветствий животных. Если

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

отправки и три метода. Давай вызовем функцию отправки greetDispatch. Она отправляет тип животного, будь то "dog", "cat" или "cow". Затем каждое значение

отправки обрабатывается определенным методом: "dog" обрабатывается с помощью greetDog, "cat" с помощью greetCat и "cow" с помощью greetCow.

Тео достает свой блокнот и открывает его на чистом листе бумаги. Он рисует диаграмму, подобную приведенной на рис. 13.1.

Рис. 13.1. Блок-схема (англ. logic flow) мультиметода greet

Дейв: А зачем стрелка между animal и методами в дополнение к стрелкам между

animal и функциями отправки?

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

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

Дейв: Аргументы? Во множественном числе? Я вижу только один аргумент.

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

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

334

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

Дейв: Ясно. Покажи мне, как написать код для мультиметода greet.

Тео: Для этого нам понадобится библиотека. Например, в JavaScript реализацию

мультиметодов предоставляет библиотека arrows/multimethod. Коротко говоря, мы вызываем функцию multi, чтобы создать мультиметод под названием method, служащий для добавления метода.

ПРИМЕЧАНИЕ. Посетите http://mng.bz/nY9v, чтобы посмотреть примеры использования и

документацию этой библиотеки.

Дейв: С чего же нам начинать?

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

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

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

выглядеть следующим образом.

Листинг 13.6. Функция отправки для мультиметода greet

function greetDispatch(animal) { ❶

if(dev()) {

if(!ajv.validate(animalSchema, animal)) { ❷

var errors = ajv.errorsText(ajv.errors);

throw ("greet called with invalid arguments: " +

errors);

}

}

return animal.type; ❸

}

var greet = multi(greetDispatch); ❹

❶ Определение сигнатуры.

❷ Валидация аргументов.

❸ Значение отправки.

❹ Инициализация мультиметода.

СОВЕТ. Функция отправки мультиметода отвечает за три вещи: она определяет сигнатуру мультиметода, валидирует аргументы и выдает значение отправки.

Дейв: Что дальше?

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

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

greetDog, которая принимает animal, а затем добавим метод dog в мультиметод

greet, используя функцию method из библиотеки arrows/multimethod. Функция

method получает два аргумента: значение отправки и функцию, соответствующую значению отправки.

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

335

Листинг 13.7. Реализация метода greet для собак

function greetDog(animal) { ❶

console.log("Woof woof! My name is " + animal.name);

}

greet = method("dog", greetDog)(greet); ❷

❶ Реализация метода.

❷ Декларация метода.

Дейв: Обязательно ли реализация метода должна находиться в том же модуле, что

и инициализация мультиметода?

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

Это именно то, что делает мультиметоды расширяемыми.

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

Дейв: А что там с кошками и коровами?

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

Через мгновение Тео представляет себе эти реализации. Затем он пишет код еще

для двух методов greet для кошек и коров.

Листинг 13.8. Реализация метода greet для кошек

function greetCat(animal) {

console.log("Meow! I am " + animal.name);

}

greet = method("cat", greetCat)(greet);

Листинг 13.9. Реализация метода greet для коров

function greetCow(animal) {

console.log("Moo! Call me " + animal.name);

}

greet = method("cow", greetCow)(greet);

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

Дейв: Важны ли названия функций отправки и методов?

Тео: Согласно тому, что я прочитал, не особенно, но мне нравится следовать простому соглашению об именовании: использовать название мультиметода (на-

336

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

пример, greet) в качестве префикса для функции отправки (например, greetDispatch) и методов. Затем называем суффикс Dispatch для функции отправки и определенный суффикс для каждого метода (например, greetDog, greetCat и greetCow).

Дейв: Как работает механизм мультиметода «под капотом»?

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

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

Дейв: По-моему, ты еще не говорил, как вызвать мультиметод.

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

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

Листинг 13.10. Вызов мультиметода как обычной функции

greet(myDog);

// → "Woof woof! My name is Fido"

greet(myCat);

// → "Meow! I am Milo"

greet(myCow);

// → "Moo! Call me Clarabelle"

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

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

Тео: Желательно.

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

Тео: Вроде как когда у животного нет соответствующего метода?

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

Тео: В этом случае мы получим сообщение об ошибке. Например, библиотека

arrows/multimethods выдает исключение NoMethodError.

Дейв: Такие вещи бесят. Есть ли способ обеспечить реализацию по умолчанию?

Тео: Безусловно! Чтобы определить реализацию по умолчанию, нужно передать

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

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

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

337

Листинг 13.11. Определение реализации по умолчанию

function greetDefault(animal) {

console.log("My name is " + animal.name);

}

greet = method(greetDefault)(greet);

Листинг 13.12. Вызов мультиметода,

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

var myHorse = {

"type": "horse",

"name": "Horace"

};

greet(myHorse);

// → "My name is Horace"

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

Дейв: Круто!

13.3. Мультиметоды

с множественной отправкой

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

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

мультиметода в целом?

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

Тео: Да ты что, Дейв, возьми и начерти в моем блокноте.

Через несколько минут у Дейва получается схема как на рис. 13.2. Он возвращает

блокнот Тео.

Тео: Чудесно! Я надеюсь, ты увидел, что функция отправки может выдавать любое

значение.

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

Тео: Например, тип двух аргументов!

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

Тео: Представь, что наши животные — полиглоты.

Дейв: Поли-что?

338

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

Рис. 13.2. Логический поток мультиметодов

Тео: Слово «полиглот» происходит от греческих слов polús, что означает «много», и glôssa, что означает «язык». Полиглот — это человек, который может говорить

на многих языках.

Дейв: На каких языках будут говорить наши животные?

Тео: Я не знаю. Допустим, на английском и французском.

Дейв: Окей, а как представить язык в нашей программе?

Тео: В виде карты, конечно!

Дейв: Какие поля у нас будут на этой языковой карте?

Тео: Давай упростим пример и сделаем два поля: type и name.

Дейв: Как в карте для животных?

Тео: Не совсем так. На языковой карте поле type должно быть либо fr для француз-ского, либо en для английского языка, а в карте для животных поле type должно

быть либо dog, либо cat, либо cow.

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

Тео с радостью соглашается: его свежезаваренный кофе стынет! Дейв пишет свою

реализацию кода и показывает Тео.

Листинг 13.13. Схема языковой карты

var languageSchema = {

"type": "object",

"properties": {

"name": {"type": "string"},

"type": {"type": "string"}

},

"required": ["name", "type"],

};

Листинг 13.14. Две карты для языков

var french = {

"type": "fr",

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

339

"name": "Français"

};

var english = {

"type": "en",

"name": "English"

};

Тео: Отлично! Теперь давай напишем код для функции отправки и методы для наших животных-полиглотов. Назовем наш мультиметод greetLang. У нас будет

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

Дейв: Так, трое животных (собака, кошка и корова) умножаются на два языка (en и fr). Перед тем как реализовать это, я хотел бы нарисовать блок-схему. Она поможет мне все основательно прояснить.

Тео: Возьмешь опять мой блокнот?

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

Рис. 13.3. Логический поток мультиметода greetLang

Тео: Почему ты не нарисовал стрелку между аргументами и методами?

Дейв: Чтобы сохранить диаграмму легко читаемой. В противном случае было бы

слишком много стрелок.

Тео: Хорошо, я понял. Ты готов писать код?

Дейв: Да!

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

с двумя элементами: типом животного и типом языка.

340

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

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

Листинг 13.15. Инициализация мультиметода с помощью функции отправки

var greetLangArgsSchema = {

"type": "array",

"prefixItems": [animalSchema, languageSchema]

};

function greetLangDispatch(animal, language) {

if(dev()) {

if(!ajv.validate(greetLangArgsSchema, [animal, language])) {

throw ("greetLang called with invalid arguments: " +

ajv.errorsText(ajv.errors));

}

}

return [animal.type, language.type];

};

var greetLang = multi(greetLangDispatch);

Дейв: Имеет ли значение порядок элементов в массиве?

Тео: Не имеет, но он должен соответствовать внутренней структуре методов.

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

Листинг 13.16. Реализация методов greetLang

function greetLangDogEn(animal, language) {

console.log("Woof woof! My name is " +

animal.name +

"andIspeak"+

language.name);

}

greetLang = method(["dog", "en"], greetLangDogEn)(greetLang); function greetLangDogFr(animal, language) {

console.log("Ouaf Ouaf! Je m'appelle " +

animal.name +

" et je parle " +

language.name);

}

greetLang = method(["dog", "fr"], greetLangDogFr)(greetLang);

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

341

function greetLangCatEn(animal, language) {

console.log("Meow! I am " +

animal.name +

"andIspeak"+

language.name);

}

greetLang = method(["cat", "en"], greetLangCatEn)(greetLang); function greetLangCatFr(animal, language) {

console.log("Miaou! Je m'appelle " +

animal.name +

" et je parle " +

language.name);

}

greetLang = method(["cat", "fr"], greetLangCatFr)(greetLang); function greetLangCowEn(animal, language) {

console.log("Moo! Call me " +

animal.name +

"andIspeak"+

language.name);

}

greetLang = method(["cow", "en"], greetLangCowEn)(greetLang); function greetLangCowFr(animal, language) {

console.log("Meuh! Appelle moi " +

animal.name +

" et je parle " +

language.name);

}

greetLang = method(["cow", "fr"], greetLangCowFr)(greetLang); Дейв просматривает код методов, которые занимаются французским языком. Он

удивлен, увидев Ouaf Ouaf вместо Woof Woof для собак, Miaou вместо Meow для кошек и

Meuh вместо Moo для коров.

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

языке отличаются от английских!

Тео: Оно-что?

Дейв: Слово «ономатопея» происходит от греческих слов ónoma, что означает

«имя», и poiéo, что означает «производить». Этот термин относится к словам, которые звучат похоже на то, что они обозначают: например, «гав», «мяу»

и «му».

Тео: Точно, почему-то по-французски собаки говорят Ouaf, кошки Miaou, а коровы

Meuh.

342

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

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

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

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

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

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

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

множественную отправку.

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

greetLang(myDog, french);

// → "Ouaf Ouaf! Je m\'appelle Fido et je parle Français"

greetLang(myDog, english);

// → "Woof woof! My name is Fido and I speak English"

greetLang(myCat, french);

// → "Miaou! Je m\'appelle Milo et je parle Français"

greetLang(myCat, english);

// → "Meow! I am Milo and I speak English"

greetLang(myCow, french);

// → "Meuh! Appelle moi Clarabelle et je parle Français"

greetLang(myCow, english);

// → "Moo! Call me Clarabelle and I speak English"

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

Дейв: Теперь полностью согласен.

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

френч-пресса.

Дейв: Отличная идея! Заодно захватим на кухне по куску кекса со свежими апель-синами. Кажется, моя мама как раз испекла такой.

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

343

13.4. Мультиметоды

с динамической отправкой

Дейв снова разливает по чашкам кофе, а Тео берет два ломтика кекса и кладет их на

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

Дейв: Что такое динамическая отправка?

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

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

Дейв: Какое это может быть значение?

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

Дейв: И зачем такое нужно?

Тео: Представь, что наши животные вовсе не полиглоты, а страдают дисмакрилек-сией.

Дейв: От чего страдают?!

Тео: От дисмакрилексии. Это слово происходит от греческой приставки dus, выра-жающей идею трудности, а также слов makrýs, что означает «длинный», и léxis, что означает «дикция». Следовательно, дисмакрилексия — это трудности с про-изнесением длинных слов.

Дейв: Никогда о таком не слышал.

Тео: Это потому, что я только что изобрел это слово.

Дейв: Забавно. Что будем считать длинным словом для наших животных?

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

Дейв: Немного странно, но ладно.

Тео: Давай назовем наш мультиметод dysGreet. Его функция отправки возвращает

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

мультиметода.

Листинг 13.18. Мультиметод, использующий функцию отправки

с динамической отправкой

function dysGreetDispatch(animal) {

if(dev()) {

if(!ajv.validate(animalSchema, animal)) {

var errors = ajv.errorsText(ajv.errors);

throw ("dysGreet called with invalid arguments:

" + errors);

}

}

var hasLongName = animal.name.length > 5;

344

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

return [animal.type, hasLongName];

};

var dysGreet = multi(dysGreetDispatch);

Дейв: Кажется, написать метод dysGreet не так и сложно.

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

диаграммы намокли! К счастью, у Дейва была припасена дополнительная салфетка, и она-то все еще чистая. Он нарисовал блок-схему (рис. 13.4), а затем взял свой

ноутбук и напечатал реализацию методов dysGreet.

Рис. 13.4. Логический поток мультиметода dysGreet

Листинг 13.19. Методы dysGreet

function dysGreetDogLong(animal) {

console.log("Woof woof! My name is " + animal.name);

}

dysGreet = method(["dog", true], dysGreetDogLong)(dysGreet); function dysGreetDogShort(animal) {

console.log("Woof woof!");

}

dysGreet = method(["dog", false], dysGreetDogShort)(dysGreet); function dysGreetCatLong(animal) {

console.log("Meow! I am " + animal.name);

}

dysGreet = method(["cat", true], dysGreetCatLong)(dysGreet);

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

345

function dysGreetCatShort(animal) {

console.log("Meow!");

}

dysGreet = method(["cat", false], dysGreetCatShort)(dysGreet); function dysGreetCowLong(animal) {

console.log("Moo! Call me " + animal.name);

}

dysGreet = method(["cow", true], dysGreetCowLong)(dysGreet); function dysGreetCowShort(animal) {

console.log("Moo!");

}

dysGreet = method(["cow", false], dysGreetCowShort)(dysGreet); Тео проверил, что код работает так, как положено. Он похвалил Дейва не только за

успешную реализацию метода, но и за то, что он предусмотрительно захватил из

кухни дополнительную салфетку.

Листинг 13.20. Тестирование dysGreet

dysGreet(myDog);

dysGreet(myCow);

dysGreet(myCat);

//"Woof woof!"

//"Moo! Call me Clarabelle"

//"Meow!"

Тео: Отличная работа, друг мой! Наше исследование мультиметодов подошло

к концу. Я думаю, мне пора ехать обратно, чтобы успеть домой до темноты и

избежать пробок в час пик.

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

языках программирования, кроме JavaScript.

Тео: Это вопрос к Джо.

Дейв: Как ты думаешь, можно позвонить ему прямо сейчас?

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

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

Дейв: Мне тоже всё понравилось, особенно наши дискуссии об этимологии. У нас, кажется, еще остались апельсины, возьми несколько домой, съешь попозже.

Тео: Здорово! Моя жена с удовольствием их попробует.

После того как Тео ушел, Дейв отправил Джо электронное письмо. Несколько

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

346

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

ПОДДЕРЖКА МУЛЬТИМЕТОДОВ НА РАЗНЫХ ЯЗЫКАХ

В Python есть библиотека под названием Multimethods (https://github.com/weissjeffm/

multimethods); у Ruby такая библиотека называется М Ruby multimethods (https://

github.com/psantacl/ruby-multimethods). Обе в общем и целом работают так же, как

библиотека arrows/multimethod для JavaScript.

В Java существует библиотека Java Multimethod Framework (http://igm.univ-mlv.fr /~forax/

works/jmmf), а C# нативно поддерживает мультиметоды с помощью ключевого слова

dynamic. Однако как в Java, так и в C# мультиметоды работают только со статическими

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

Поддержка универсальной

Язык URL

структуры данных

JavaScript https://github.com/caderek/arrows/tree/master/

Да

packages/multimethod

Java

http://igm.univ-mlv.fr/~forax/works/jmmf/

Нет

C# Нативная

поддержка

Нет

Python

https://github.com/weissjeffm/multimethods

Да

Ruby

https://github.com/psantacl/ruby-multimethods

Да

13.5. Интеграция мультиметодов:

производство

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

Этот приятный момент прерывается телефонным звонком от Нэнси из Klafim.

Нэнси: Как у тебя дела?

Тео: Нормально. Я возвращаюсь из сельской местности.

Нэнси: Круто. Ты готов поговорить о работе?

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

Нэнси: Я бы хотела добавить в каталог одну крошечную фишечку.

Когда Нэнси ранее называла что-нибудь «крошечным», Тео пугался, потому что

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

в соответствии с принципами ДОП «крошечные фишечки» Нэнси можно довольно

легко реализовать.

Тео: Что за фишечка?

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

Тео: Это не очень и сложно.

Нэнси: А еще мне нужно немного отформатировать текст.

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

347

Тео: Какое именно нужно форматирование?

Нэнси: В зависимости от количества книг, написанных одним автором, их название

должно быть выделено жирным шрифтом или курсивом.

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

Я взгляну на них завтра утром.

Нэнси: Идёт. Хорошей поездки!

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

вдруг подумал, что никогда не смотрел этимологию самого слова «этимология»!

Он поискал термин «этимология» в Интернете и узнал, что это слово происходит от

греческого слова étumon, что означает «истинный смысл», и суффикса logia, озна-чающего «учение о чем-либо». Ночью Тео снятся собаки, кошки и коровы, про-граммирующие на своих ноутбуках в травянистом поле.

Когда Тео на следующий день пришел в офис, он открыл электронное письмо Нэнси с подробной информацией о новой фишке с форматированием текста. Все детали приведены в табл. 13.1.

Таблица 13.1. Форматирование текста для имен авторов

в соответствии с количеством написанных ими книг

Количество книг

Курсив

Жирный шрифт

10 или меньше

Да

Нет

Между 11 и 50

Нет

Да

51 и больше

Да

Да

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

Дейв решил, что самая сложная часть форматирования текста заключается в реализации функции Author.myName(author, format), которая получает два аргумента: данные об авторе и формат текста. Он размышляет над тем, можно ли реализовать эту

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

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

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

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

формат в виде карты с полем type, как Тео вчера делал для языков, но сейчас Дейву

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

в листинге 13.21. Схему для автора он уже писал на прошлой неделе с Тео (она

в листинге 13.22).

348

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

Листинг 13.21. Схема формата текста

var textFormatSchema = {

"name": {"type": "string"},

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

};

Листинг 13.22. Схема для автора

var authorSchema = {

"type": "object",

"required": ["name", "bookIsbns"],

"properties": {

"name": {"type": "string"},

"bookIsbns": {

"type": "array",

"items": {"type": "string"}

}

}

};

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

Дейв вспомнил, как легко Тео позволил себе придумать слово «дисмакрилексия», и решил тоже изобрести собственный неологизм, «плодовиция» (англ. prolificity), вместо общепринятого слова «плодовитость» (англ. prolificness). Он посчитал полезным использовать вспомогательную функцию Author.prolificityLevel, которая

возвращает уровень «плодовиции» автора: low, medium или high (низкий, средний, высокий). Итак, он пишет функцию authorNameDispatch.

Листинг 13.23. Инициализация мультиметода Author.myName

Author.prolificityLevel = function(author) {

var books = _.size(_.get(author, "bookIsbns"));

Назад: Глава 10. Операции с базой данных
Дальше: Итоги