2 added
1 removed
Original
2026-01-01
Modified
2026-02-26
1
<p><strong>Разработчики часто неверно понимают концепцию передачи состояния представления (REST). Большинство ошибок связаны с трактовкой архитектурного ограничения HATEOAS. В этой статье мы разберем популярные заблуждения, связанные с REST, и подробно остановимся на HATEOAS. В конце текста на примере имитации конечного автомата - кухонного тостера - рассмотрим, как гипермедиа может использоваться в REST API для управления состояниями.</strong></p>
1
<p><strong>Разработчики часто неверно понимают концепцию передачи состояния представления (REST). Большинство ошибок связаны с трактовкой архитектурного ограничения HATEOAS. В этой статье мы разберем популярные заблуждения, связанные с REST, и подробно остановимся на HATEOAS. В конце текста на примере имитации конечного автомата - кухонного тостера - рассмотрим, как гипермедиа может использоваться в REST API для управления состояниями.</strong></p>
2
<p><em>Примечание: Это адаптированный перевод статьи<a>Designing a True REST State Machine</a>Билла Доррфельда, технического журналиста и специалиста по API. Повествование ведётся от лица автора оригинала.</em></p>
2
<p><em>Примечание: Это адаптированный перевод статьи<a>Designing a True REST State Machine</a>Билла Доррфельда, технического журналиста и специалиста по API. Повествование ведётся от лица автора оригинала.</em></p>
3
<h2>Содержание</h2>
3
<h2>Содержание</h2>
4
<ul><li><a>История REST и гипермедиа</a></li>
4
<ul><li><a>История REST и гипермедиа</a></li>
5
<li><a>Что такое REST</a></li>
5
<li><a>Что такое REST</a></li>
6
<li><a>Пример конечного автомата: IoT-тостер</a></li>
6
<li><a>Пример конечного автомата: IoT-тостер</a></li>
7
<li><a>Заключение</a></li>
7
<li><a>Заключение</a></li>
8
-
</ul><p>Концепция гипермедиа сформировалась в 1941 году, когда аргентинский писатель Хорхе Луис Борхес написал "Сад расходящихся тропок" - рассказ, в котором страницы текста ссылаются друг на друга. Вероятно, это первый в истории пример гипертекста (<em>на самом деле<a>впервые</a>гипертекст использовался в романе Джеймса Джойса "Улисс", - прим. редакции</em>). Сейчас в массовой культуре есть множество примеров реляционных связей - от сюжета видеоигры<a>Bioshock</a>до серии детских романов-ужасов Роберта Стайна<a>Goosebumps</a>, - но во времена Борхеса такой прием был беспрецедентным.</p>
8
+
</ul><h2>История REST и гипермедиа</h2>
9
+
<p>Концепция гипермедиа сформировалась в 1941 году, когда аргентинский писатель Хорхе Луис Борхес написал "Сад расходящихся тропок" - рассказ, в котором страницы текста ссылаются друг на друга. Вероятно, это первый в истории пример гипертекста (<em>на самом деле<a>впервые</a>гипертекст использовался в романе Джеймса Джойса "Улисс", - прим. редакции</em>). Сейчас в массовой культуре есть множество примеров реляционных связей - от сюжета видеоигры<a>Bioshock</a>до серии детских романов-ужасов Роберта Стайна<a>Goosebumps</a>, - но во времена Борхеса такой прием был беспрецедентным.</p>
9
<p><em>Гипермедиа - система организации информации, элементы которой взаимосвязаны и, кроме самого гипертекста, включают в себя видео, картинки, аудио и другие типы контента.</em></p>
10
<p><em>Гипермедиа - система организации информации, элементы которой взаимосвязаны и, кроме самого гипертекста, включают в себя видео, картинки, аудио и другие типы контента.</em></p>
10
<p>Затем произошло несколько событий, которые помогли REST сформироваться в концепцию с четкой структурой:</p>
11
<p>Затем произошло несколько событий, которые помогли REST сформироваться в концепцию с четкой структурой:</p>
11
<ul><li>1963:<a>Тед Нельсон</a>ввел термины гипертекста и гипермедиа.</li>
12
<ul><li>1963:<a>Тед Нельсон</a>ввел термины гипертекста и гипермедиа.</li>
12
<li>1968: Исследователь человеко-машинного интерфейса(Human-Machine Intelligence, HMI) Дуглас Энгельбарт на презентации, которую позже окрестили "матерью всех демонстраций", представил систему NLS - oN-Line System и первую компьютерную мышь. Этот момент можно назвать началом обработки текста в современном виде.</li>
13
<li>1968: Исследователь человеко-машинного интерфейса(Human-Machine Intelligence, HMI) Дуглас Энгельбарт на презентации, которую позже окрестили "матерью всех демонстраций", представил систему NLS - oN-Line System и первую компьютерную мышь. Этот момент можно назвать началом обработки текста в современном виде.</li>
13
<li>1987: Сотрудник Apple Билл Аткинсон создал HyperCard - первую успешную реализацию гипермедиа до появления Всемирной паутины.</li>
14
<li>1987: Сотрудник Apple Билл Аткинсон создал HyperCard - первую успешную реализацию гипермедиа до появления Всемирной паутины.</li>
14
<li>1989: Тим Бернерс-Ли из ЦЕРНа создал<a>Всемирную паутину</a>и представил первую успешную реализацию протокола передачи гипертекста (HTTP) между клиентом и сервером.</li>
15
<li>1989: Тим Бернерс-Ли из ЦЕРНа создал<a>Всемирную паутину</a>и представил первую успешную реализацию протокола передачи гипертекста (HTTP) между клиентом и сервером.</li>
15
<li>2000: Рой Филдинг, соавтор спецификации HTTP и URI, написал докторскую диссертацию<a>"Архитектурные стили и проектирование сетевых архитектур программного обеспечения"</a>, где он описал передачу репрезентативного состояния, или REST. Именно Филдинг считается создателем концепции REST в ее нынешнем виде.</li>
16
<li>2000: Рой Филдинг, соавтор спецификации HTTP и URI, написал докторскую диссертацию<a>"Архитектурные стили и проектирование сетевых архитектур программного обеспечения"</a>, где он описал передачу репрезентативного состояния, или REST. Именно Филдинг считается создателем концепции REST в ее нынешнем виде.</li>
16
</ul><h2>Что такое REST</h2>
17
</ul><h2>Что такое REST</h2>
17
<p>Все технические аспекты, которые определяют REST, трудно раскрыть в виде статьи в блоге - это слишком объемная тема. Поэтому предлагаю пойти от обратного: рассмотреть четыре распространенных заблуждения - они помогут нам понять, чем REST точно не является.</p>
18
<p>Все технические аспекты, которые определяют REST, трудно раскрыть в виде статьи в блоге - это слишком объемная тема. Поэтому предлагаю пойти от обратного: рассмотреть четыре распространенных заблуждения - они помогут нам понять, чем REST точно не является.</p>
18
<h3>Заблуждение №1: REST - это просто CRUD</h3>
19
<h3>Заблуждение №1: REST - это просто CRUD</h3>
19
<p>CRUD - акроним, который обозначает четыре базовые функции при работе с персистентными хранилищами данных: "создание, чтение, редактирование, удаление". CRUD соответствует действиям в SQL, но он плохо соотносится с методами HTTP.</p>
20
<p>CRUD - акроним, который обозначает четыре базовые функции при работе с персистентными хранилищами данных: "создание, чтение, редактирование, удаление". CRUD соответствует действиям в SQL, но он плохо соотносится с методами HTTP.</p>
20
<p>Хотя GET и DELETE в REST и CRUD совпадают, POST, PUT и PATCH отвечают за разные операции. Например, в REST POST означает не только "создать" - это универсальный метод. С его помощью туннелируется весь протокол SOAP при использовании с HTTP.</p>
21
<p>Хотя GET и DELETE в REST и CRUD совпадают, POST, PUT и PATCH отвечают за разные операции. Например, в REST POST означает не только "создать" - это универсальный метод. С его помощью туннелируется весь протокол SOAP при использовании с HTTP.</p>
21
<p>Поскольку HTTP-методы не соответствуют CRUD, теоретик и популяризатор REST Асбьёрн Ульсберг утверждает, что создатели различных API должны подумать, как они могут описывать API иным способом: "Не ограничивайте себя CRUD при разработке REST API. Прочитайте спецификацию и поймите семантику каждого метода, а затем правильно используйте ее".</p>
22
<p>Поскольку HTTP-методы не соответствуют CRUD, теоретик и популяризатор REST Асбьёрн Ульсберг утверждает, что создатели различных API должны подумать, как они могут описывать API иным способом: "Не ограничивайте себя CRUD при разработке REST API. Прочитайте спецификацию и поймите семантику каждого метода, а затем правильно используйте ее".</p>
22
<p>Если перефразировать, смысл этой цитаты сводится к тому, что REST - это стиль архитектуры, а не протокол. Таким образом, ошибочно называть "RESTful" HTTP API с операциями CRUD.</p>
23
<p>Если перефразировать, смысл этой цитаты сводится к тому, что REST - это стиль архитектуры, а не протокол. Таким образом, ошибочно называть "RESTful" HTTP API с операциями CRUD.</p>
23
<h3>Заблуждение №2: некоторые конструкции URI "более RESTful", чем другие</h3>
24
<h3>Заблуждение №2: некоторые конструкции URI "более RESTful", чем другие</h3>
24
<p>Унифицированные идентификаторы ресурсов (URI) - основа концепции REST. Они позволяют определять ресурсы и действия с ними. Однако многие разработчики ошибочно считают, что могут отличить качество построения REST API просто на основе того, как структурирован URI. Проведем эксперимент: сможете ли вы сказать, какой URI "более RESTful"?</p>
25
<p>Унифицированные идентификаторы ресурсов (URI) - основа концепции REST. Они позволяют определять ресурсы и действия с ними. Однако многие разработчики ошибочно считают, что могут отличить качество построения REST API просто на основе того, как структурирован URI. Проведем эксперимент: сможете ли вы сказать, какой URI "более RESTful"?</p>
25
<ol><li><a>http://hexlet.io/authors/contributor?author=doerrfeld</a></li>
26
<ol><li><a>http://hexlet.io/authors/contributor?author=doerrfeld</a></li>
26
<li><a>http://api.hexlet.io/blogpost/getPostById?id=47</a></li>
27
<li><a>http://api.hexlet.io/blogpost/getPostById?id=47</a></li>
27
<li><a>http://api.hexlet.io/blogpost/47/edit-form</a></li>
28
<li><a>http://api.hexlet.io/blogpost/47/edit-form</a></li>
28
<li><a>http://api.hexlet.io/blogpost/47</a></li>
29
<li><a>http://api.hexlet.io/blogpost/47</a></li>
29
<li><a>http://api.hexlet.io/128ndoels-8asdf-12d5-39d3</a></li>
30
<li><a>http://api.hexlet.io/128ndoels-8asdf-12d5-39d3</a></li>
30
</ol><p>Тот факт, что мы указали URI, метод и описание для каждого вызова API, еще не значит, что мы создали REST API - мы просто задокументировали наши URI, как если бы мы задокументировали операции удаленного вызова процедур (RPC). Ульсберг отмечал, что такой подход усложняет работу и лишает сервис гибкости.</p>
31
</ol><p>Тот факт, что мы указали URI, метод и описание для каждого вызова API, еще не значит, что мы создали REST API - мы просто задокументировали наши URI, как если бы мы задокументировали операции удаленного вызова процедур (RPC). Ульсберг отмечал, что такой подход усложняет работу и лишает сервис гибкости.</p>
31
<h3>Заблуждение №3: API-интерфейсам REST нужны версии</h3>
32
<h3>Заблуждение №3: API-интерфейсам REST нужны версии</h3>
32
<p>Представим, что у нас есть таблица базы данных под названием "Referer". После нескольких лет использования мы заметили, что имя базы написано с ошибкой и решили изменить его на "Referrer". Клиенты уже взаимодействуют со старым именем таблицы в своих SQL-операторах, поэтому обновить имя базы будет крайне сложно - прежде придется обновить все клиенты.</p>
33
<p>Представим, что у нас есть таблица базы данных под названием "Referer". После нескольких лет использования мы заметили, что имя базы написано с ошибкой и решили изменить его на "Referrer". Клиенты уже взаимодействуют со старым именем таблицы в своих SQL-операторах, поэтому обновить имя базы будет крайне сложно - прежде придется обновить все клиенты.</p>
33
<p>То же самое с API: если мы решим обновить один из /blogposts/ в URI из примеров, указанных выше, обновления потребуют все клиенты. Это приведет к созданию второй версии - то есть к необходимости обновить документацию и все клиенты. Резюмируя, можно сказать, что строго запрограммированное управление версиями в URI - это боль.</p>
34
<p>То же самое с API: если мы решим обновить один из /blogposts/ в URI из примеров, указанных выше, обновления потребуют все клиенты. Это приведет к созданию второй версии - то есть к необходимости обновить документацию и все клиенты. Резюмируя, можно сказать, что строго запрограммированное управление версиями в URI - это боль.</p>
34
<h3>Заблуждение №4. Гипермедиа необязательна для REST API</h3>
35
<h3>Заблуждение №4. Гипермедиа необязательна для REST API</h3>
35
<p><em>В интервью Майку Амудсену в 2014 году Филдинг сказал следующее: "Гипермедиа как механизм управления состоянием приложения - это ограничение REST. Это не одна из опций и не идеал, к которому нужно стремиться. Гипермедиа - это данность и ограничение. Вы либо принимаете их, либо не занимаетесь REST".</em></p>
36
<p><em>В интервью Майку Амудсену в 2014 году Филдинг сказал следующее: "Гипермедиа как механизм управления состоянием приложения - это ограничение REST. Это не одна из опций и не идеал, к которому нужно стремиться. Гипермедиа - это данность и ограничение. Вы либо принимаете их, либо не занимаетесь REST".</em></p>
36
<p>REST состоит из 6 основных ограничений - так называемых ограничений Филдинга. Он выглядит так:</p>
37
<p>REST состоит из 6 основных ограничений - так называемых ограничений Филдинга. Он выглядит так:</p>
37
<ul><li>Клиент-сервер</li>
38
<ul><li>Клиент-сервер</li>
38
<li>Отсутствие состояния</li>
39
<li>Отсутствие состояния</li>
39
<li>Кэшируемость</li>
40
<li>Кэшируемость</li>
40
<li>Многоуровневая система</li>
41
<li>Многоуровневая система</li>
41
<li>Код по требованию</li>
42
<li>Код по требованию</li>
42
<li>Единообразие интерфейса</li>
43
<li>Единообразие интерфейса</li>
43
</ul><p>При этом последний пункт - "Единообразие интерфейса" - состоит из еще нескольких подпунктов:</p>
44
</ul><p>При этом последний пункт - "Единообразие интерфейса" - состоит из еще нескольких подпунктов:</p>
44
<ol><li>Определение ресурсов</li>
45
<ol><li>Определение ресурсов</li>
45
<li>Управление ресурсами через представление</li>
46
<li>Управление ресурсами через представление</li>
46
<li>Самодостаточные сообщение</li>
47
<li>Самодостаточные сообщение</li>
47
<li>Гипермедиа как механизм управления состоянием приложения (HATEOAS)</li>
48
<li>Гипермедиа как механизм управления состоянием приложения (HATEOAS)</li>
48
</ol><p>Среди них HATEOAS, вероятно, самое важное и уникальное, но наименее понятное условия REST.</p>
49
</ol><p>Среди них HATEOAS, вероятно, самое важное и уникальное, но наименее понятное условия REST.</p>
49
<p><em>По сути, HATEOAS или гипермедиа как механизм управления состоянием, - это подход к описанию ресурсов API. С его помощью можно установить инверсию контроля за ресурсами, которые клиент может вызвать, вместо того, чтобы просто перечислять каждый из них и указывать список функций, которые можно с ними выполнить. Именно благодаря этому сервер может отвечать за состояние ресурса.</em></p>
50
<p><em>По сути, HATEOAS или гипермедиа как механизм управления состоянием, - это подход к описанию ресурсов API. С его помощью можно установить инверсию контроля за ресурсами, которые клиент может вызвать, вместо того, чтобы просто перечислять каждый из них и указывать список функций, которые можно с ними выполнить. Именно благодаря этому сервер может отвечать за состояние ресурса.</em></p>
50
<p>Чтобы лучше понять эту идею, Ульсберг советует воспользоваться методом, предложенным Дональдом Норманом в книге "Дизайн повседневных вещей". Так же, как чашка сделана для того, чтобы ее взяли за ручку и подняли, а кнопка - чтобы быть нажатой, гипермедиа хочет сообщить вам, что делать с тем или иным ресурсом. Гипермедиа - это ссылки и метаданные для операций, которые помогают разработчикам или машинам выполнять дополнительные действия.</p>
51
<p>Чтобы лучше понять эту идею, Ульсберг советует воспользоваться методом, предложенным Дональдом Норманом в книге "Дизайн повседневных вещей". Так же, как чашка сделана для того, чтобы ее взяли за ручку и подняли, а кнопка - чтобы быть нажатой, гипермедиа хочет сообщить вам, что делать с тем или иным ресурсом. Гипермедиа - это ссылки и метаданные для операций, которые помогают разработчикам или машинам выполнять дополнительные действия.</p>
51
<p>Асбьёрн Ульсберг описывает гипермедиа так: "Если вы посмотрите на гипермедиа как на рецепт того, как должен выглядеть следующий запрос, вы поймете, что такое гипермедиа".</p>
52
<p>Асбьёрн Ульсберг описывает гипермедиа так: "Если вы посмотрите на гипермедиа как на рецепт того, как должен выглядеть следующий запрос, вы поймете, что такое гипермедиа".</p>
52
<h2>Пример конечного автомата: IoT-тостер</h2>
53
<h2>Пример конечного автомата: IoT-тостер</h2>
53
<p>Разберем концепцию гипермедиа как механизма состояния приложения на примере простого конечного автомата - тостера, подключенного к интернету вещей (IoT), которым можно управлять через интернет.</p>
54
<p>Разберем концепцию гипермедиа как механизма состояния приложения на примере простого конечного автомата - тостера, подключенного к интернету вещей (IoT), которым можно управлять через интернет.</p>
54
<p>Конечный автомат - это абстрактная модель, которая содержит конечное количество состояний чего-либо. В нашем случае тостер запускается после включения - переходит в состояние нагрева. Затем он достигает верхнего предела температуры и переходит в состояние ожидания, снижая температуру. Этот цикл продолжается до тех пор, пока кусок хлеба не поджарится достаточно хорошо - после этого тостер отключается.</p>
55
<p>Конечный автомат - это абстрактная модель, которая содержит конечное количество состояний чего-либо. В нашем случае тостер запускается после включения - переходит в состояние нагрева. Затем он достигает верхнего предела температуры и переходит в состояние ожидания, снижая температуру. Этот цикл продолжается до тех пор, пока кусок хлеба не поджарится достаточно хорошо - после этого тостер отключается.</p>
55
<p>Как управлять тостером с помощью REST и гипермедиа? Филдинг писал, что каждая страница в сети представляет собой отдельное состояние одного ресурса, которое можно получить с помощью вызова GET (этот процесс показан в таблице выше). То есть вы можете получить какие-либо данные с помощью идентификатора в URI.</p>
56
<p>Как управлять тостером с помощью REST и гипермедиа? Филдинг писал, что каждая страница в сети представляет собой отдельное состояние одного ресурса, которое можно получить с помощью вызова GET (этот процесс показан в таблице выше). То есть вы можете получить какие-либо данные с помощью идентификатора в URI.</p>
56
<p><em>Попробуем сделать это: для начала вызовем наш тостер с помощью GET:</em></p>
57
<p><em>Попробуем сделать это: для начала вызовем наш тостер с помощью GET:</em></p>
57
<p>Ответ будет выглядеть примерно так:</p>
58
<p>Ответ будет выглядеть примерно так:</p>
58
<p>В ответе содержится идентификатор, который показывает, с каким ресурсом мы работаем. Следом отображается текущее состояние ресурса, а ниже - список возможных операций, которые с ним можно совершить.</p>
59
<p>В ответе содержится идентификатор, который показывает, с каким ресурсом мы работаем. Следом отображается текущее состояние ресурса, а ниже - список возможных операций, которые с ним можно совершить.</p>
59
<p>Теперь попробуем изменить состояние тостера. Для этого отправим HTTP-запрос PUT:</p>
60
<p>Теперь попробуем изменить состояние тостера. Для этого отправим HTTP-запрос PUT:</p>
60
<p>Ответ будет выглядеть примерно так:</p>
61
<p>Ответ будет выглядеть примерно так:</p>
61
<p>Хорошо, теперь мы включили тостер. Однако в ответе видно, что его мощность по-прежнему на нуле - он все еще не нагревается. Попробуем вызвать изменение мощности:</p>
62
<p>Хорошо, теперь мы включили тостер. Однако в ответе видно, что его мощность по-прежнему на нуле - он все еще не нагревается. Попробуем вызвать изменение мощности:</p>
62
<p>Мы увеличили мощность - теперь в ответе видно, что тостер нагревается.</p>
63
<p>Мы увеличили мощность - теперь в ответе видно, что тостер нагревается.</p>
63
<p>Теперь мы можем перевести тостер в режим ожидания и снова увеличить мощность. Однако вместо этого отправим еще один вызов GET на идентификатор устройства:</p>
64
<p>Теперь мы можем перевести тостер в режим ожидания и снова увеличить мощность. Однако вместо этого отправим еще один вызов GET на идентификатор устройства:</p>
64
<p>С момента нашего последнего запроса тостер перешел в состояние ожидания. В ответе видно, что мы все еще можем отключить его или отрегулировать температуру, усилив мощность нагрева.</p>
65
<p>С момента нашего последнего запроса тостер перешел в состояние ожидания. В ответе видно, что мы все еще можем отключить его или отрегулировать температуру, усилив мощность нагрева.</p>
65
<p>Если подождать еще несколько минут, следующий запрос к конечному автомату, скорее всего, приведет его к состоянию "выключение" или "выключено" - то есть в то состояние, с которого мы начинали.</p>
66
<p>Если подождать еще несколько минут, следующий запрос к конечному автомату, скорее всего, приведет его к состоянию "выключение" или "выключено" - то есть в то состояние, с которого мы начинали.</p>
66
<h2>Заключение</h2>
67
<h2>Заключение</h2>
67
<p>Сеть функционирует как набор идей, связанных друг с другом гипертекстом. Ульсберг считает, что веб-API должны имитировать этот принцип, а важным аспектом в реализации API является гипермедиа. Для тех, кто только начинает работать с REST, создание HATEOAS-совместимого API - это огромный шаг вперед по сравнению с API-интерфейсами в стиле RPC.</p>
68
<p>Сеть функционирует как набор идей, связанных друг с другом гипертекстом. Ульсберг считает, что веб-API должны имитировать этот принцип, а важным аспектом в реализации API является гипермедиа. Для тех, кто только начинает работать с REST, создание HATEOAS-совместимого API - это огромный шаг вперед по сравнению с API-интерфейсами в стиле RPC.</p>
68
<p>"Если вы используете гипермедиа, вы можете добавлять отношения, ссылки и операции к ресурсам, не нарушая работу существующих клиентов, и наделяя новых клиентов новыми функциями", - писал Ульсберг.</p>
69
<p>"Если вы используете гипермедиа, вы можете добавлять отношения, ссылки и операции к ресурсам, не нарушая работу существующих клиентов, и наделяя новых клиентов новыми функциями", - писал Ульсберг.</p>
69
<p>Такой подход подразумевает переосмысление традиционного подхода к управлению версиями. Мнения Ульсберга и Филдинга в этом вопросе сходятся: управление версиями в REST - плохая идея. Когда вы в последний раз видели номер версии на веб-сайте? HTML не требует контроля версий, и JSON тоже в нем не нуждается.</p>
70
<p>Такой подход подразумевает переосмысление традиционного подхода к управлению версиями. Мнения Ульсберга и Филдинга в этом вопросе сходятся: управление версиями в REST - плохая идея. Когда вы в последний раз видели номер версии на веб-сайте? HTML не требует контроля версий, и JSON тоже в нем не нуждается.</p>
70
<p>Уделяя больше внимания ресурсам как конечному автомату, мы можем сообщать потребителям текущее состояние, с помощью которого мы совершаем операции. Такой подход снижает степень связанности и в бизнес-доменах, где состояние приложения состоит из множества сложных и взаимозависимых факторов, и значительно упрощает работу клиента. Именно в этом сила REST и гипермедиа.</p>
71
<p>Уделяя больше внимания ресурсам как конечному автомату, мы можем сообщать потребителям текущее состояние, с помощью которого мы совершаем операции. Такой подход снижает степень связанности и в бизнес-доменах, где состояние приложения состоит из множества сложных и взаимозависимых факторов, и значительно упрощает работу клиента. Именно в этом сила REST и гипермедиа.</p>