1. Как API сползает в RPC-like стиль
Базовая грамматика у нас уже есть: коллекция, отдельный ресурс, GET/POST/PUT/PATCH/DELETE. Но бизнес почти никогда не приходит с формулировкой “добавьте ещё один PATCH”. Он говорит: завершить задачу, назначить исполнителя, прикрепить файл, удалить комментарий. И именно в этот момент API чаще всего снова сползает в каталог команд.
На уровне сервиса это нормально: внутри приложения completeTask() или uploadAttachment() звучат естественно. Проблема начинается тогда, когда те же глаголы становятся публичными путями вроде /completeTask и /uploadFileToTask. Внешний контракт снова описывает кнопки, а не объекты предметной области.
Здесь нас интересует практический вопрос: как перевести язык действий бизнеса в уже знакомую ресурсную грамматику.
RPC-like API: без религии и «священных войн»
Под RPC-like API здесь будем иметь в виду не конкретную технологию, а характерный паттерн контракта: путь отвечает на вопрос “что выполнить?”, а не “с чем работаем?”. Внешне разница обычно выглядит так:
| Ось сравнения | RPC-like подход (команды) | Resource-like подход (ресурсы) |
|---|---|---|
| Главный смысл URI | «Что сделать?» | «С чем работаем?» |
| Как растёт API | Новая фича → новый action endpoint | Новая фича → уточнение ресурса/представления/подресурса |
| Как клиент «держит модель» | Нужно помнить каталог команд | Можно догадаться по структуре ресурсов |
| Самый частый HTTP-метод | Обычно всё превращается в POST | Методы используются по смыслу (GET, POST, PATCH, DELETE…) |
Команды внутри приложения никто не запрещает. Вопрос только в том, должен ли клиент учить ваши имена методов вместо понятной карты ресурсов.
2. Когда API разрастается в каталог команд
Командный стиль почти всегда растёт одинаково: на каждую новую бизнес-просьбу появляется ещё один путь с глаголом. В Task Tracker API это быстро превращается во что-то вроде:
import java.util.List;
// Так выглядит "каталог команд": рост идёт добавлением новых глаголов в пути
List<String> endpoints = List.of(
"/createTask",
"/completeTask",
"/assignTask",
"/addCommentToTask",
"/uploadFileToTask"
);
Проблема не только в количестве маршрутов. Клиенту приходится учить ваш каталог команд, документация распадается на список несвязанных действий, а API перестаёт выглядеть как карта домена. Как только вы это замечаете, следующий шаг почти напрашивается сам: взять каждое бизнес-действие и спросить, какой объект при этом появляется, меняется или читается.
3. Перевод действий в ресурсный мир
Теперь самое полезное: что делать, когда бизнес говорит “нужно действие X”, а мы хотим остаться в ресурсном стиле. Хороший прагматичный ответ почти всегда начинается не с выбора URI, а с простого вопроса: какой объект предметной области при этом появляется или меняется? Если вы честно на него ответили, дальше дизайн обычно сам “складывается”.
Вот маленькая шпаргалка в виде дерева решений. Оно не про идеальную теорию, а про то, чтобы мозг не выключался:
flowchart TD
%% Идея: начинаем не с URI, а с вопроса "что за объект появляется/меняется?"
A["Нам нужно действие (business operation)"] --> B{"Появляется новый объект?"}
B -- Да --> C["Это создание ресурса → коллекция (например, comments/attachments)"]
B -- Нет --> D{"Меняется состояние существующего объекта?"}
D -- Да --> E["Это update ресурса → меняем представление (например, статус/исполнитель)"]
D -- Нет --> F{"Нужно просто получить данные?"}
F -- Да --> G["Это read ресурса/коллекции"]
F -- Нет --> H["Проверь: не пытаемся ли мы спрятать сложный процесс за 'кнопкой'"]
Давайте посмотрим прямо на нашем домене. Вот как “команда” превращается в ресурсное решение без магии:
Представим команду: “Добавить комментарий к задаче”. Если мыслить командами, рука тянется сделать /addCommentToTask. Но если спросить “что появляется?”, ответ простой: появляется новый комментарий. Значит, комментарий — это ресурс (подресурс задачи), и у него есть место в API как у коллекции комментариев внутри задачи.
import java.util.Map;
// Ментальный перевод: "имя операции" -> "куда это ложится в ресурсной карте"
Map<String, String> operationToResource = Map.of(
// Комментарий — новый объект, значит это создание элемента в коллекции comments
"addCommentToTask", "/tasks/{taskId}/comments",
// Вложение — тоже новый объект (метаданные + файл), значит коллекция attachments
"uploadFileToTask", "/tasks/{taskId}/attachments"
);
Команда: “Загрузить файл к задаче”. Появляется новый объект — метаданные вложения. Значит, это тоже создание подресурса: коллекции вложений в контексте задачи.
Команда: “Назначить исполнителя”. Здесь новый объект не появляется, но меняется состояние задачи (например, поле assigneeName). Значит, это изменение существующего ресурса task. В ресурсной модели клиент работает с задачей, а назначение — это всего лишь изменение её состояния, а не “отдельная команда мира”.
Команда: “Завершить задачу” (complete). Если говорить честно, в нашем домене это, скорее всего, перевод статуса в DONE. Опять же: меняется состояние задачи, а не появляется отдельная сущность “Completion”. Значит, задача остаётся главным ресурсом, а “complete” — частный случай изменения её состояния.
Очень важная дисциплина: внутри сервиса вы можете продолжать использовать глагольные методы. Это нормально. Просто внешний API не должен копировать их имена.
class TaskService {
void completeTask(String taskId) {
// Внутри сервиса глагол естественен: это часть реализации бизнес-операции
// Снаружи (в HTTP контракте) мы не хотим превращать это в адрес вида /completeTask
}
void assignTask(String taskId, String assigneeName) {
// Аналогично: внутренняя команда ок
// Снаружи это обычно будет update ресурса /tasks/{taskId} (например, поле assignee)
}
}
Проблема не в глаголах как таковых. Проблема в том, что глагол становится адресом в публичном контракте, и клиент начинает жить в вашем “словаре команд”, а не в предметной области.
4. Прагматичный REST: дисциплина без перфекционизма
Прагматичный REST нужен здесь не для того, чтобы спорить о чистоте стиля, а чтобы не скатиться сразу в две крайности: в каталог команд с одной стороны и в академический перфекционизм — с другой. Если ресурсная модель делает API понятнее клиенту, её стоит держать; если спор об “идеальном REST” ничего не добавляет контракту, он только шумит.
Полезно сравнить три состояния, в которые часто попадают команды:
| Стиль | Как выглядит снаружи | Что происходит внутри проекта |
|---|---|---|
| RPC-like хаос | Много глагольных путей, “action endpoint” на каждую мелочь | Клиент учит команды, API разрастается как список кнопок |
| Догматичный REST | Всё строго “по учебнику”, но иногда чрезмерно сложно и неудобно | Иногда появляются странные решения «ради правильности» |
| Прагматичный REST | Ресурс в центре, действия выражаются через состояние/подресурс, а не через каталог команд | API остаётся читаемым, но не мешает реальной разработке |
И здесь важный момент: иногда в реальной коммерческой разработке встречаются операции, которые трудно выразить без “действия”. Например, “экспортировать отчёт” или “запустить пересчёт” — это может быть отдельный процесс, который живёт как ресурс “job”. Но обратите внимание: даже там хороший дизайн часто превращает действие в ресурс (“экспорт-задача”, “отчёт-экспорт”), а не в /doExportNow.
В Task Tracker API мы сознательно держим модель простой: основная история — tasks, а из вспомогательного — comments и attachments; tags — lookup-список значений. На этом масштабе прагматичный REST почти всегда означает одно: не плодить команды, если смысл можно выразить через ресурс, его состояние или подресурс.
5. Быстрый тест: признаки командного API
Когда вы проектируете API, полезно иметь быстрый инструмент самопроверки — как запах дыма на кухне. Не обязательно сразу понимать, что именно горит, но важно вовремя заметить, что “что-то точно идёт не так”. Для RPC-like-дизайна такой тест можно свести буквально к трём вопросам. И не надо превращать это в бюрократию: это просто короткая пауза перед тем, как вы добавите новый endpoint.
Первый вопрос звучит так: “Могу ли я в одном предложении сказать, какой ресурс адресую?” Если вы говорите “я вызываю completeTask”, то ресурс не прозвучал. Если вы говорите “я работаю с конкретной задачей”, ресурс прозвучал.
Второй вопрос: “Появляется ли новый объект?” Если да, чаще всего это коллекция — создание нового элемента, — и вы почти автоматически приходите к подресурсу или отдельной коллекции. Комментарий — отличный пример: он появляется, у него есть свой id, и он живёт в контексте задачи.
Третий вопрос: “Не добавляю ли я новую кнопку вместо того, чтобы описать изменение состояния?” Если вы делаете /changeStatus и /assignTask, это очень похоже на “кнопки”. Если вместо этого вы говорите “задача имеет статус и исполнителя, и мы меняем состояние задачи”, модель становится естественнее.
Можно даже сделать игрушечную проверку в коде — не ради “правильного алгоритма”, а просто чтобы мозг остановился на секунду:
record ApiCheck(String path) {
boolean looksLikeCommand() {
// Это не "валидатор REST", а грубая эвристика: глагол в начале пути = красный флажок
return path.startsWith("/create")
|| path.startsWith("/update")
|| path.startsWith("/delete")
|| path.startsWith("/complete")
|| path.startsWith("/upload");
}
}
// Пример: путь начинается с команды, значит эвристика срабатывает
ApiCheck a = new ApiCheck("/completeTask");
System.out.println(a.looksLikeCommand()); // true
Ещё раз: это не “детектор REST”. Это напоминалка: если вы видите, что путь начинается с глагола-команды, остановитесь и попробуйте переформулировать вопрос: “какой ресурс тут на самом деле?”
6. Типичные ошибки при попытке “избежать RPC-like”
Ошибка №1: перепутать “RPC-like плохо” с “глаголы запрещены вообще”.
Новички иногда берут правило “не делай /completeTask” и превращают его в догму “в URI не может быть глаголов никогда”. В результате начинается странная игра в эвфемизмы: путь не становится яснее, а просто делается менее честным. В этой лекции важен не запрет самого слова, а вопрос читаемости: виден ли ресурс и его место в модели.
Ошибка №2: заменять один action endpoint другим, не возвращаясь к ресурсу.
Иногда команда понимает, что /uploadFileToTask выглядит плохо, и делает, скажем, /taskFileUpload. Это косметика. Если вы не ответили на вопрос “это подресурс? новый объект? изменение состояния?”, вы просто поменяли вывеску на том же магазине команд.
Ошибка №3: пытаться “сделать REST” через механическое переименование, но оставить командную модель.
Бывает, что путь уже стал “похож на ресурс”, но внутри всё равно живёт команда: всегда POST, всегда разные странные формы входных данных, каждый endpoint живёт своей жизнью. REST — это не только “как назвать путь”, это ещё и про устойчивую модель операций вокруг ресурсов. Мы пока не уходим в детали реализации, но важно помнить: подмена словаря без смены мышления ничего не лечит.
Ошибка №4: воспринимать прагматичность как разрешение на хаос.
“Прагматичный REST” — это не “давайте как быстрее”. Это “давайте держать ресурсную модель и не усложнять её там, где это не даёт ценности”. Если вы под флагом прагматичности начинаете добавлять /doThis, /doThat, /doSomethingElse, вы просто возвращаетесь в RPC-like стиль, только теперь у вас ещё и моральная индульгенция.
Ошибка №5: забыть, что команды внутри приложения — это нормально, а наружу — нет.
Иногда разработчик начинает воевать с собственным кодом: “Раз REST, значит, и внутри нельзя писать completeTask()”. Не надо так. Внутри вы строите реализацию, и там глагольные методы — естественная вещь. Наружу вы строите контракт, и там важнее ресурсная карта и предсказуемость. Когда перед глазами есть компактная ресурсная карта проекта, эта граница становится гораздо заметнее: внутри могут жить команды сервиса, но наружу выходит не каталог кнопок, а карта предметной области.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ