Re[12]: Каких программ вам не хватает?
От: Sinclair Россия https://github.com/evilguest/
Дата: 06.04.25 06:09
Оценка: 11 (2) +1
Здравствуйте, Miroff, Вы писали:

M>Можно это как-то обосновать?

Можно. Когда я делаю GET на ресурс, я получаю его представление. Крайне желательно, чтобы это представление зависело от состояния ресурса, а не от состояния пользователя, который его получает.
Например, можно воткнуть reverse proxy перед сервером приложения, и тогда он сможет снять значительную часть нагрузки с сервера.
Как только мы начинаем менять представление объекта в зависимости от пользователя, о кэшировании можно забыть.

M>Мне кажется, мы о разных вещах говорим. Есть ресурс, есть набор связанных ресурсов с которыми можно проделывать какие-то операции.

Тут всё верно. Ресурсы обычно связаны друг с другом не искусственными линками, а естественным образом, спрятанным в представлении.
Возьмём, к примеру, ресурс Payment.
Вот как выглядит представление этого ресурса через PayPal API:
{
  "id": "0VF52814937998046",
  "status": "CREATED",
  "amount": {
    "value": "10.99",
    "currency_code": "USD"
  },
  "invoice_id": "INVOICE-123",
  "seller_protection": {
    "status": "ELIGIBLE",
    "dispute_categories": [
      "ITEM_NOT_RECEIVED",
      "UNAUTHORIZED_TRANSACTION"
    ]
  },
  "payee": {
    "email_address": "merchant@example.com",
    "merchant_id": "7KNGBPH2U58GQ"
  },
  "expiration_time": "2017-10-10T23:23:45Z",
  "create_time": "2017-09-11T23:23:45Z",
  "update_time": "2017-09-11T23:23:45Z",
  "links": [
    {
      "rel": "self",
      "method": "GET",
      "href": "https://api-m.paypal.com/v2/payments/authorizations/0VF52814937998046"
    },
    {
      "rel": "capture",
      "method": "POST",
      "href": "https://api-m.paypal.com/v2/payments/authorizations/0VF52814937998046/capture"
    },
    {
      "rel": "void",
      "method": "POST",
      "href": "https://api-m.paypal.com/v2/payments/authorizations/0VF52814937998046/void"
    },
    {
      "rel": "reauthorize",
      "method": "POST",
      "href": "https://api-m.paypal.com/v2/payments/authorizations/0VF52814937998046/reauthorize"
    }
  ]
}

Этот ресурс, судя по всему, связан с другим ресурсом — invoice. Но никаких намёков на то, как получить информацию об этом ресурсе, внутри ресурса нет. И линков тоже нет. Зато есть линки про то, как изменить состояние этого же ресурса. Так что это никакой не REST.
В настоящем хардкорном RMM3 REST вместо атрибута invoice_id был бы атрибут invoice со значением "https://www.paypal.com/invoice/details/INVOICE-123".

M>HATEOS позволяет НЕ ВЫЗЫВАЯ операций предсказать, будут они доступны клиенту или нет.

Нет, ничего подобного HATEOAS не предполагает. Нет никакой гарантии, что операции будут доступны. Нет никакой гарантии, что никаких других операций нет.

M>Например, в списке сотрудников рядом с именем Sinclair либо показывать кнопку "объявить выговор" или не показывать. И чтобы это реализовать без HATEOAS часть логики авторизации, кому можно налагать взыскания и на кого, вынужденно скопируется из сервиса в его клиента.

Нет конечно. Зачем? Это крайне контрпродуктивная идея.
Во-первых, "объявление выговора" в REST-системе — это не операция с "сотрудником с именем Синклер". Это создание ресурса "выговор". А точнее — создание ресурса "приказ об объявлении выговора".
В котором могут фигурировать, к примеру, несколько сотрудников. И права на создание этого выговора весьма косвенно связаны с перечисленными там сотрудниками.
Во-вторых, построение безопасности путём скрытия действий в UI — это популярная, но плохая идея. В основном — потому, что она неимоверно бесит пользователей. Вот я сижу перед списком пользователей, хочу объявить выговор. Поискал в меню — нет такой команды. Поискал в свойствах сотрудника — нет такой кнопки. Полез в инструкцию — сказано "для объявления выговора нажмите на кнопку "Объявить выговор". И вот я уже полчаса думаю, то ли я дурак, то ли инструкция устарела, то ли программа глючит. Внезапно, "некрасивый" способ — дать мне нажать на кнопку, а потом вывести "403 у вас недостаточно прав для объявления выговора этому сотруднику" — в разы гуманнее по отношению к пользователю, чем предлагаемая вами идея.

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

Ничего никуда не расползается. Зачем? Микросервис просто делает свою работу. Поймите, нельзя строить микросервис на том, что "я прочитал ресурс — если нет ссылки, значит нельзя рефандить и надо идти по ветке А, а если ссылка есть — то надо идти по ветке Б и выполнять рефанд". Возможно, настройки прав или ешё что-то поменялось между чтением ресурса и переходом по ссылке; всегда можно получить в ответ "403". Так что вместо вот этой вот двухшаговой наркомании "если ссылка, то идём по ветке Б, и если получили 403, то переключаемся обратно на ветку А". Просто идём и пытаемся сделать рефанд, не делая никаких предположений о правах там и всём остальном.

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


Это очень, очень плохая архитектура. В первую очередь потому, что вы инвертируете зависимости. Теперь у вас ресурс user (и его микросервис) обязан знать всё о турах (которые вообще-то обрабатываются другим микросервисом).
А ресурс "тур", ясное дело, должен знать всё о пользователях. Поздравляю, вы получили не просто монолит, а трудноразвиваемый монолит.

M>Опять же, можешь раскрыть тему? Пейджинг это простая штука если перестать думать о нем, как о RPC и начать думать как об отдельном ресурсе. Ты СОЗДАЕШЬ отдельный объект "поисковый запрос" и в ответ получаешь список страниц с результатами этого запроса. И, внезапно, у тебя уже нет проблем со стабильностью, сортировки, изменению состава страниц, кэшированию и т.п. Результат фиксируется в момент создания запроса и далее до нового поиска уже не меняется.

И это — тоже очень, очень плохая архитектура. Смотрите как это работает: "поисковый запрос" — это эфемерный ресурс. Последнее, что мы хотим — это хранить его. В первую очередь по соображениям масштабирования. Сколько пользователей вы ожидаете ежедневно? Двух? Как только у вас появляется хотя бы несколько сотен тысяч запросов в сутки, хранение (даже в течение ограниченного времени) их эфемерных запросов становится неподъёмным.
Как раз наоборот — пользователь, который получил в ответ на поиск информацию о том, что нашлось 100000 страниц, скорее всего выполнит новый запрос, потому что этот оказался недостаточно точным.
Нет, бывают такие ситуации, когда поиск сам по себе очень дорогой, и мы как отдельную фичу предлагаем "сохранение запросов", да ещё и иногда с оповещениями об изменениях. Но в простых гражданских случаях это не работает, и поиск делается поверх реалтаймовых данных, а не поверх какого-то снапшота, полученного неведомо когда.
И вот в такой обстановке "традиционный" пейджинг в стиле "дай мне позиции с 120 по 139" — тупиковая ветвь проектировщицкой мысли. Единственное его достоинство — простота реализации.
Зато недостатки у него принципиальные — в частности, невозможность восстановить целостную картину.

Есть несколько способов решить эту проблему. Один из них — примерно то, что вы предлагаете, "думать о запросе как об отдельном ресурсе", только правильно реализованный.
Правильно — значит иметь один ресурс "поисковый запрос". Несмотря на его эфемерность, он вполне реален. То есть не два разных ресурса "пользователи на букву А с 0 по 99" и "пользователи на букву А с 100 по 199", а один ресурс "пользователи на букву А". Если этот ресурс слишком велик для прожёвывания его одним запросом, то мы добавляем к запросу range header и тащим комфортными для нас частями.
А непротиворечивость гарантируется тем, что последующие запросы мы дооборудуем хидером if-match/if-none-match, что позволяет нам заметить момент, когда данные на серверной стороне поменялись, и закешированную версию нужно выкинуть.

M>Нет. POST refund это создание ДРУГОГО ресурса. Другого потому что у него другой URI. Нужно думать о рефанде не как об изменении платежа, а как ос создании нового объекта — сторнирующей операции, потому что сам платеж изменять не позволяет принцип двойной бухгалтерии известный с 17 века: сделанная проводка не может быть удалена.

Отлично, давайте зайдём с этого конца. Хотя там, если покопать в API, будет пачка методов по изменению именно того же объекта (https://developer.paypal.com/docs/api/payments/v2/#authorizations_reauthorize).
В нормальном REST избегают POST методов, поскольку в случае потери результата POST крайне сложно понять, чем там кончилось дело, и что делать теперь.
К сожалению, авторы PayPal API книжек по REST-у не читали. Поэтому у них нет ничего идемпотентного.

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

Простите, но вы рассказываете какие-то небылицы. Что такое "пречеки"? Мы пробежались по всем нужным ресурсам, и убедились, что все нужные нам "действия" торчат в виде линков?
Ну так это ничего не значит. Посмотрите в тот же PayPal API — там очень редко бывает так, что достаточно POST с пустым телом. То есть — результат будет зависеть от параметров запроса. А их вы никаким пречеком не проверите (если в API нет специального метода precheck). Поэтому — да, либо rollback (если вдруг у нас есть детерминированный способ сделать rollback), либо rollforward. Чудес не бывает.
Если вы собрались изображать в любой архитектуре, хоть микросервисной, хоть монолитной, композитный сценарий вроде "запросили кредит — перевели деньги на расчётный счёт — расплатились за покупку товара", то он всегда может сфейлиться на каждом этапе. В прошлом веке для этого применяли протокол двухфазного коммита. Сейчас у нас два варианта:
— реализовать двухфазный коммит (каждый из этапов на самом деле делается в виде двух отдельных методов типа "подготовить ресурс" и "использовать ресурс"; при этом подготовленный ресурс после некоторого таймаута саморассасывается.
— понимать, что мы можем застрять посреди процесса — например, с выданным кредитом, но без дефицитной игровой приставки, которую успели купить.
Никакие "пречеки" тут не помогут, если нет реального резервирования ресурсов.

M>Людям вообще нравится RPC, даже по этой теме видно. Но RPC не дает необходимой для микросервисов изоляции и довольно быстро превращает приложение в распределенный монолит.

RPC людям нравится не за то, что у него есть преимущества, а за то, что они не сразу видят его недостатки.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.