— Секрет успеха поставщика заключается в том, чтобы обеспечить потребителей качественным товаром… ой, то есть сервисом. Ну и еще важно не пускаться во все тяжкие с нарушением обратной совместимости.
Уолтер Уайт Это перевод статьи, описывающей шаблон Consumer-Driven Contracts (CDC).
Оригинал опубликован на сайте Мартина Фаулера за авторством Яна Робинсона. В микросервисной архитектуре зависимости между сервисами являются источником проблем. Шаблон CDC помогает решать эти проблемы способом, устраивающим и разработчиков сервиса, и его потребителей. Фаулер ссылается на Consumer-Driven Contracts в своей ключевой статье по микросервисной архитектуре: Microservices. Статья будет особенно полезна командам, разрабатывающим сервисы для нескольких потребителей в рамках одной организации, и командам-потребителям таких сервисов. В данной статье обсуждаются проблемы, возникающие между разработчиками и потребителями сервисов, например, когда поставщики меняют часть своего контракта, в частности, схемы документов. Описываются две стратегии борьбы с ними: Обе стратегии помогают защитить потребителей от изменений в контракте, но не дают поставщику сервиса понимания: В статье описан шаблон Consumer-Driven Contracts, который позволяет поставщикам лучше понимать своих потребителей и фокусирует развитие сервиса на том, что нужно потребителям. Чтобы проиллюстрировать некоторые проблемы, с которыми мы сталкиваемся при разработке сервисов, рассмотрим простой ProductSearch, который позволяет осуществлять поиск в нашем каталоге продуктов. Результат поиска имеет следующую структуру: Рисунок 1: Схема результата поиска Примерный документ с результатами поиска выглядит следующим образом: Сервис ProductSearch в настоящее время используется двумя приложениями: внутренним маркетинговым приложением и веб-приложением внешнего реселлера. Оба клиента используют XSD для проверки принятых документов до их обработки. Внутреннее приложение использует поля CatalogueID, Name, Price и Manufacturer; внешнее приложение — поля CatalogueID, Name и Price. Ни одно из них не использует поле InStock: хотя оно рассматривалось для маркетингового приложения, оно было выкинуто на раннем этапе разработки. Одним из наиболее распространенных способов развития сервиса является добавление поля в документ для одного или нескольких потребителей. В зависимости от того, как были реализованы клиентская и серверная части, даже простые изменения, подобные этому, могут иметь дорогостоящие последствия для бизнеса и его партнеров. В нашем примере после того, как сервис ProductSearch эксплуатируется в течение некоторого времени, его хочет использовать второй реселлер, но просит добавить поле Description к каждому продукту. Из-за того, как устроены клиенты, это изменение имеет значительные и дорогостоящие последствия как для поставщика, так и для существующих потребителей. Стоимость каждого из них зависит от того, как мы реализуем изменение. Есть как минимум два способа, которыми мы можем распределять стоимость изменений между участниками "сообщества сервиса". Во-первых, мы могли бы изменить нашу исходную схему и потребовать, чтобы каждый потребитель обновил свою копию схемы, чтобы правильно проверить результаты поиска. Стоимость изменения системы здесь распределяется между поставщиком, который, столкнувшись с таким запросом на изменение, всё равно будет делать какие-то изменения; и потребителями, которых не интересует обновленная функциональность. С другой стороны, мы могли бы добавить вторую операцию и схему для нового потребителя и сохранить исходные операцию и схему для существующих потребителей. Все доработки теперь на стороне поставщика, но сложность сервиса и стоимость его поддержки увеличиваются. Главными преимуществами использования сервисов являются: SOA повышает гибкость за счет размещения бизнес-функций в выделенных, переиспользуемых сервисах. Затем эти сервисы используются и оркестрируются для выполнения основных бизнес-процессов. Это снижает стоимость изменений за счет сокращения зависимостей между сервисами, позволяя им быстро перестраиваться и настраиваться в ответ на изменения или незапланированные события. Однако бизнес может полностью реализовать эти преимущества, только если имплементация SOA позволяет сервисам эволюционировать независимо друг от друга. Чтобы повысить независимость, мы создаем контракты сервисов. Несмотря на это, мы вынуждены дорабатывать потребителей также часто, как и поставщика, главным образом потому, что потребители зависят от конкретной версии контракта поставщика. В конце концов, поставщики начинают крайне осторожно подходить к изменению любого элемента контракта; в частности, потому, что они не имеют представления о том, как потребители реализуют этот контракт. В худшем случае, потребители завязываются на поставщика, описывая всю схему документа в рамках своей внутренней логики. Контракты обеспечивают независимость сервисов; парадоксально, что они могут также связывать поставщиков и потребителей нежелательными способами. Не задумываясь о функции и роли контрактов, которые мы реализуем в нашей SOA, мы подвергаем наши сервисы «скрытой» связи. Отсутствие какой-либо информации о том, как потребители сервиса реализуют контракт в коде, а также отсутствие ограничений на реализацию (и для поставщика, и для потребителя), в совокупности подрывают предполагаемые преимущества SOA. Короче говоря, предприятие начинает утомляться от сервисов. Мы можем начать исследование проблем контракта и зависимостей, которые мешают нашему сервису ProductSearch, с версионирования схемы. Группа технической архитектуры WC3 (TAG) описала ряд стратегий управления версиями, которые могут помочь нам разработать схемы для наших сервисов способами, уменьшающих проблемы с зависимостью. Эти стратегии варьируются от чрезмерно либеральной none, которая требует, чтобы сервисы не различали разные версии схемы и принимали все изменения, до чрезвычайно консервативного большого взрыва, что требует от сервиса выдавать ошибку, если он получает неожиданную версию сообщения. Обе крайности создают проблемы, которые не дают получить ценность для бизнеса и увеличивают общую стоимость владения системой. Явные и неявные стратегии «без версий» приводят к системам, которые непредсказуемы в своих взаимодействиях, плохо развиваемы и имеют высокую стоимость доработок. С другой стороны, стратегии "большого взрыва" создают сильно связанные сервисные ландшафты, в которых изменения схемы затрагивают всех поставщиков и потребителей, ухудшая доступность, замедляя развитие и уменьшая возможности для получения прибыли. "Сообщество сервиса" из нашего примера эффективно реализует стратегию большого взрыва. Учитывая затраты на повышение ценности системы для бизнеса, ясно, что поставщики и потребители выиграют от более гибкой стратегии управления версиями — той, что TAG называет стратегией совместимости. Она обеспечивает прямую и обратную совместимость схем. При развитии сервисов схемы с обратной совместимостью позволяют потребителям новых схем принимать экземпляры старой схемы: поставщик, созданный для обработки обратно совместимых новых версий, может, тем не менее, принять запрос в формате старой схемы. С другой стороны, прямая совместимость позволяет потребителям старых схем обрабатывать экземпляр новой схемы. Это ключевой момент для существующих пользователей ProductSearch: если бы схема результатов поиска изначально была сделана с учетом прямой совместимости, потребители могли бы обрабатывать ответы новой версии, не нуждаясь в доработке. Составление схем c обратной и прямой совместимостью — это хорошо понятная задача дизайна, лучше всего выраженная шаблоном расширяемости Must Ignore (см. статьи Дэвида Орчарда и Дейра Обасанжо). Шаблон Must Ignore рекомендует, чтобы схемы включали точки расширения, которые позволяют добавлять элементы к типам и дополнительные атрибуты для каждого элемента. В шаблоне также рекомендуется, чтобы XML описывал модель обработки, которая определяет, что потребителям делать с расширениями. Простейшая модель требует от потребителей игнорировать элементы, которые они не распознают — отсюда и название шаблона. Модель также может потребовать от потребителей обработки элементов, имеющих флаг «Must Understanding», либо выдать ошибку, если они не могут их понять. Вот наша изначальная схема документа с результатами поиска: Давайте теперь откатимся назад во времени, и с самого начала укажем для нашего сервиса совместимую, расширяемую схему: Эта схема включает дополнительный элемент расширения для каждого продукта. Сам элемент расширения может содержать один или несколько элементов из целевого пространства имен:  Рисунок 2: Расширяемая схема результата поиска Теперь, когда мы получаем запрос на добавление описания продукта, мы можем опубликовать новую схему с дополнительным элементом Description, который поставщик вставляет в точку расширения. Это позволяет сервису ProductSearch возвращать результаты, содержащие описания продуктов, и потребители используют новую схему для проверки всего документа. Потребители, использующие старую схему, не сломаются, хотя они не будут обрабатывать поле "Описание". Новый документ с результатом поиска выглядит так: Пересмотренная схема выглядит так: Обратите внимание, что первая версия расширяемой схемы совместима со второй, а вторая обратно совместима с первой. Однако эта гибкость происходит за счет повышения сложности. Расширяемые схемы позволяют нам вносить непредвиденные изменения, но они дают возможности, которые могут быть никогда не востребованы; при этом мы теряем: Далее мы не будем обсуждать расширяемые схемы. Достаточно сказать, что точки расширения позволяют нам делать прямо и обратно совместимые изменения в схемах и документах, не затрагивая поставщиков и потребителей сервиса. Однако расширение схемы не помогает нам управлять эволюцией системы, когда нам нужно сделать изменение, нарушающее контракт.  — Да, наша команда нарушила обратную совместимость в последнем релизе! Просто не смогли удержаться от небольшой оптимизации протокола… Ну не обижайся, пупсик!
Карла Борин Наш сервис ProductSearch включает в результаты поиска поле, указывающее наличие данного товара. Сервис заполняет это поле, используя дорогостоящий вызов в древнюю систему инвентаризации — зависимость, которую дорого поддерживать. Поставщик хотел бы удалить эту зависимость, очистить дизайн и улучшить общую производительность системы. И желательно, без влияния на потребителей. Общаясь с потребителями, команда поставщика выясняет, что ни одно из потребительских приложений на самом деле ничего не делает с этим полем; то есть будучи дорогостоящим, оно избыточно. К сожалению, в текущем положении, если мы удалим поле InStock из нашей расширяемой схемы, мы сломаем существующих потребителей. Чтобы исправить поставщика, мы должны исправить всю систему: когда мы удаляем функциональность от поставщика и публикуем новый контракт, каждое приложение-потребитель нужно будет переустановить с новой схемой, а взаимодействие между сервисами тщательно протестировать. Сервис ProductSearch в этом отношении не может развиваться независимо от потребителей: поставщик и потребители должны "прыгать одновременно". Наше сообщество сервиса разочаровывается в его эволюции, потому что каждый потребитель имеет «скрытую» зависимость, которая отражает весь контракт поставщика во внутренней логике потребителя. Потребители, используя валидацию по XSD и, в меньшей степени, статическую привязку к схеме документа в коде, неявно принимают весь контракт поставщика, независимо от их намерений использовать лишь часть. Дэвид Орчард дает некоторые подсказки относительно того, как мы могли бы избежать этой проблемы, ссылаясь на принцип надежности: «Реализация должна быть консервативной в своем поведении и либеральной в том, что принимает от других». Мы можем усилить этот принцип в контексте развития сервиса, заявив, что получатели сообщений должны выполнять «достаточную» проверку: то есть они должны обрабатывать только используемые ими данные, и должны выполнять явно ограниченную или целенаправленную проверку полученных ими данных — в отличие от неявно неограниченной валидации «все или ничего», присущей обработке XSD. Один из способов, которым мы можем настроить проверку на стороне потребителя — это использование масок или регулярных выражений для путей до элементов документа, возможно, с использованием языка валидации структуры дерева, такого как Schematron. Используя Schematron, каждый пользователь сервиса ProductSearch может задать, что он ожидает найти в результатах поиска: Реализации Schematron обычно преобразуют схему Schematron, такую как эта, в XSLT-преобразование, которое получатель сообщения может применить к документу для проверки на корректность. Обратите внимание, что эта схема не содержит предположений об элементах в исходном документе, которые не нужны приложению-потребителю. Таким образом, явно описана валидация только требуемых элементов. Изменения схемы исходного документа не будут отвергнуты при валидации, если они не нарушают явные ожидания, описанные в схеме Schematron, даже если это удаление ранее обязательных элементов. Вот относительно легкое решение наших проблем с контрактом и зависимостью, и это не требует от нас добавления неявных метаинформационных элементов в документ. Итак, давайте снова откатимся назад во времени и восстановим простую схему, описанную в начале статьи. Но на этот раз мы также будем настаивать на том, что потребители свободны в своем поведении и валидируют и обрабатывают только ту информацию, которая им необходима (используя схемы Schematron, а не XSD для проверки принятых сообщений). Теперь, когда поставщик добавляет описание для продукта, он может опубликовать пересмотренную схему, не затрагивая существующих потребителей. Аналогичным образом, обнаружив, что поле InStock не проверяется или не обрабатывается ни одним из потребителей, сервис может пересмотреть схему результатов поиска — снова, не затрагивая потребителей. В конце этого процесса схема ответа ProductSearch выглядит так: Использование Schematron в приведенном выше примере приводит к некоторым интересным выводам о контрактах между поставщиками и потребителями, выходящим за рамки проверки документов. В этом разделе мы выделяем и обобщаем некоторые из этих идей и выражаем их в терминах шаблона, который мы называем Контракты потребителей. Первое, что нужно отметить — схемы документов являются лишь частью того, что поставщик сервиса должен предлагать потребителям, чтобы они могли использовать его функциональность. Мы называем эту предназначенную для внешних потребителей информацию контрактом поставщика. Контракт поставщика описывает функциональность сервиса в виде набора экспортируемых элементов, необходимых для использования этой функциональности. С точки зрения развития сервиса, контракт представляет собой контейнер для набора экспортируемых элементов, описывающих бизнес-функции. Список этих элементов включает: Это определение контракта немного шире, чем то, которое мы обычно подразумеваем, говоря о сервисах, но оно полезно с точки зрения развития сервиса. Мы не должны считать его исчерпывающим в отношении типов элементов, которые может содержать контракт поставщика: это просто логичный набор элементов, которые являются кандидатами на включение в стратегию развития сервиса. С логической точки зрения, этот набор элементов-кандидатов открыт, но на практике внутренние или внешние факторы, такие как требования к совместимости или ограничения платформы, могут ограничивать тип элементов, которые может содержать контракт. Например, контракт сервиса, который соответствует профилю WS-Basic, не будет содержать политик. Невзирая на любые подобные ограничения, область контракта определяется просто связностью его элементов. Контракт может содержать много элементов и быть широким по охвату, или сосредоточиться лишь на немногих, до тех пор, пока этого достаточно для описания его функциональности. Как мы решаем, включать ли элемент в контракт поставщика? Мы делаем это, спрашивая себя, может ли любой из наших потребителей сформулировать (например, в виде тестов) одно или несколько ожиданий в отношении функциональности, описываемой элементом. Мы уже видели из нашего примера сервиса, как потребители могут проявлять интерес к частям схемы документа, экспортируемой сервисом, и как они могут утверждать, что их ожидания относительно этого контрактного элемента по-прежнему выполняются. Поэтому схема документа является частью нашего контракта поставщика. Контракты поставщиков имеют следующие характеристики: Раз мы решили учитывать ожидания потребителей относительно схем, которые мы раскрываем при разработке нашего сервиса — и считаем, что наш поставщик знает о них — тогда нам нужно импортировать эти потребительские ожидания в поставщика. Утверждения Schematron в нашем примере очень похожи на такие тесты. Будучи использованы поставщиком, тесты могут помочь гарантировать, что поставщик продолжает выполнять свои обязательства перед потребителями. Внедряя эти тесты, поставщик получает лучшее представление о том, как он может развивать формат сообщений, которые он создает, не "ломая" функциональность, используемую сообществом сервиса. И там, где предлагаемое изменение затрагивает одного или нескольких потребителей, поставщик будет иметь непосредственное представление об этой проблеме и ему будет проще обсуждать её со всеми заинтересованными сторонами, в зависимости от бизнес-факторов либо учитывая их требования, либо стимулируя их доработки. В нашем примере мы можем сказать, что набор утверждений/тестов, порожденных всеми потребителями, выражает обязательную структуру сообщений в течение некоторого периода (то есть пока тесты актуальны). Если поставщик обладал этим набором утверждений, он мог бы гарантировать, что каждое отправленное им сообщение валидно для каждого потребителя, поскольку набор утверждений является валидным и полным. Обобщая эту структуру, мы можем отличить то, что мы уже назвали контрактом поставщика, от индивидуальных контрактных обязательств, которые возникают между поставщиком и потребителем, которые мы теперь будем называть контрактами потребителей. Когда поставщик принимает разумные ожидания, выраженные потребителем, он заключает контракт с потребителем. Рисунок 3: Контракты потребителей Контракты потребителей имеют следующие характеристики: Контракты потребителей позволяют нам думать о ценности для бизнеса в любой момент жизненного цикла поставщика. Выражая ожидания от контракта поставщика, контракты потребителей эффективно определяют, какие части контракта поставщика в настоящее время имеют ценность для бизнеса, а какие нет. Это приводит нас к предположению, что сервисные сообщества могут извлечь выгоду из того, что они сразу будут специфицировать контракт с точки зрения потребителей. С этой точки зрения, контракты поставщиков возникают для удовлетворения ожиданий и требований потребителей. Чтобы отразить производный характер этой новой контрактной договоренности, мы называем такие контракты поставщиков Consumer-Driven Contracts или производными контрактами. Производный характер этих контрактов-поставщиков-для-потребителей, добавляет гетерогенный аспект к отношениям между поставщиком сервиса и его потребителем. То есть, поставщики подчиняются обязательству, которое возникает за пределами их границ. Это никоим образом не влияет на принципиально автономный характер их реализации; он просто делает явным тот факт, что сервисы успешны, только когда их используют. Consumer-Driven Contracts имеют следующие характеристики: В следующей таблице приведены характеристики трёх типов контрактов, описанных в этой статье: Шаблон Consumer-Driven Contracts рекомендует создавать сообщества сервисов, используя потребительские контракты и Consumer-Driven Contracts. Однако в шаблоне не указывается, какие формы или структуры должны принимать эти контракты, а также не определяется, как ожидания/тесты потребителей сообщаются поставщику и проверяются в его жизненном цикле. Контракты могут быть выражены и структурированы несколькими способами. В своей простейшей форме потребительские ожидания могут быть зафиксированы в электронной таблице или подобном документе и реализованы на этапах проектирования, разработки и тестирования приложения-поставщика. Пойдя немного дальше и сделав unit-тесты, которые подтверждают каждое ожидание, мы можем гарантировать, что контракты описываются и проверяются в автоматическом режиме с каждой сборкой. В более сложных реализациях ожидания могут быть выражены как утверждения Schematron или через WS-Policy, и оцениваются во время выполнения во входных и выходных обработчиках сервиса. Как и в случае со структурой контрактов, у нас есть несколько вариантов, когда речь идет о способе коммуникации между поставщиками и потребителями. Так как шаблон Consumer-Driven Contracts не привязан к реализации, мы могли бы, если это позволяет организационная структура, передавать ожидания, просто общаясь с другими командами или используя электронную почту. Если число ожиданий и/или потребителей слишком велико, мы можем рассмотреть автоматизацию этого процесса. Каким бы ни был механизм, скорее всего, эти коммуникации будут проводиться до любых разговоров о бизнес-функциях сервиса. Consumer-Driven Contracts предлагают две существенные выгоды, когда речь заходит о развитии сервисов. Во-первых, они фокусируют спецификацию и реализацию сервиса вокруг пользы для бизнеса. Сервис имеет ценность для бизнеса только в той степени, в которой он потребляется. Consumer-driven contracts связывают развитие сервиса с пользой для бизнеса, обозначая ценность элементов контракта, экспортируемых потребителями сервисов — того, что потребители требуют от поставщиков, чтобы выполнять свою работу. В результате поставщики выставляют "экономный" контракт, который четко соответствует бизнес-целям, реализуемым потребителями. Изменение — развитие сервиса — возникает только тогда, когда потребители выражают явную потребность. Разумеется, возможность начинать с минимального набора требований и развивать наш сервис по запросу потребителей, предполагает что мы в состоянии контролируемым и эффективным образом дорабатывать и развертывать сервис и управлять им. Здесь шаблон Consumer-Driven Contracts обеспечивает второе ключевое преимущество. Consumer-driven contracts поставщика дают нам тонкое понимание и быструю обратную связь, необходимые для планирования изменений и оценки их воздействия на приложения, которые уже эксплуатируются. На практике это позволяет нам ориентировать отдельных потребителей и предоставлять им стимулы, чтобы отказаться от ожиданий, которые мешают нам сделать изменения, нарушающие прямую и обратную совиместимость, когда эти изменения назрели. Строя сервис на основе контрактов потребителей, мы наполняем его репозиторием знаний и механизмом обратной связи, на которые мы можем полагаться во время эксплуатации сервиса. В этой статье мы определили мотив для введения Consumer-Driven Contracts в сервисный ландшафт, а затем описали, как шаблон Consumer-Driven Contracts учитывает факторы, определяющие развитие сервиса. В завершение обсудим область применимости шаблона, а также некоторые проблемы, которые могут возникнуть при реализации потребительских контрактов и CDC. Шаблон Consumer-Driven Contracts применим в контексте как одной организации, так и закрытого, тесно связанного сообщества сервисов: конкретнее, в среде, где поставщики могут оказывать определенное влияние на то, как потребители заключают с ними контракты. Независимо от того, насколько облегчен механизм передачи и представления ожиданий и обязательств, поставщики и потребители должны знать, принимать и использовать согласованный набор каналов и конвенций. Это неизбежно добавляет уровень сложности и зависимости от протокола к уже и без того сложной сервисной инфраструктуре. Мы предположили, что системы, основанные на Consumer-Driven Contracts, способны лучше управлять изменениями, нарушающими совместимость. Но мы не собираемся предполагать, что шаблон — это панацея для проблемы с нарушением совместимости: когда все сказано и сделано, несовместимость все еще является несовместимостью. Однако мы считаем, что шаблон дает понимание того, что на самом деле представляет собой это нарушение, и это понимание может служить основой для стратегии управления версионированием сервиса. Более того, как мы уже обсуждали, сервисные сообщества, которые реализуют шаблон, лучше прогнозируют последствия изменений сервиса. В частности, команда поставщика может эффективнее планировать свои стратегии изменений — возможно, помечая элементы контракта как deprecated в течение определенного периода, одновременно стимулируя потребителей к переходу на новые версии контракта. Consumer-Driven Contracts необязательно уменьшают зависимости между сервисами. Слабосвязанные сервисы относительно независимы друг от друга, но тем не менее, остаются связанными. Однако, что шаблон делает, так это выставляет на свет некоторые из этих остаточных «скрытых» зависимостей, чтобы поставщики и потребители могли лучше вести переговоры и управлять такими зависимостями. Мы обсудили, как потребительские контракты и Consumer-Driven Contracts выражают ценности бизнеса. Но мы должны четко указать, что не рассматриваем такие контракты как индекс или показатель бизнес-ценности — они не являются бизнес-метрикой — и, несмотря, на некоторые поверхностные сходства с такими спецификациями, как WS-Agreement и WSLA, они не предназначены для выражения SLA. Основное допущение здесь заключается в том, что сервисы сами по себе не представляют никакой ценности для бизнеса; ценно их использование. Определяя сервисы как можно ближе к месту использования — силами потребителей — мы стремимся создавать пользу для бизнеса в минималистичной, "точно в срок" манере. Наконец, мы должны отметить риск: позволяя контрактам потребителей управлять спецификацией поставщика сервиса, мы можем подорвать концептуальную целостность сервиса. Сервисы заключают в себе дискретные, идентифицируемые, переиспользуемые функции, чья целостность не должна быть скомпрометирована необоснованными требованиями, выходящими за пределы их назначения. architect