Comments 57
object.method("param", "param").method2("param").method3("param")...
и на то есть свои причины. В частности несколько подобных строк, идущих друг за другом ухудшают читабельность, а сам подход тяжело тестировать (если, конечно, это не фассадные методы к более простому интерфейсу, но я сомневаюсь что это так).
Такой подход далек от кошерного ООП и больше походит на процедурный стиль программирования, где все действия выполняются в виде одного длинного алгоритма. BackboneJS же позволяет разделить логику обращения к REST API на небольшие компоненты (объекты модели), которые проще тестировать и расширять.
Другими словами предлагаемое мной решение позволит вам использовать объектную модель для работы с REST API, вы же предлагаете использовать некую точку входа. Это как сравнивать Data Mapper и Table Gateway, понимаете разницу?
Раскройте пожалуйста термин кошерного ООП ?
Предложенное автором решение ближе к инфраструктуре, нежели к модели (я это прекрасно понимаю), но используемый интерфейс (столь нелюбимые мной цепочки вызовов) меня сильно смущает, ибо усложняет читабельность. Мне пришлось перечитать несколько раз примеры автора, чтоб хоть немного въехать в то, что они делают.
А теперь сравните с реализацией на BackboneJS:
var bookList = new BookList;
bookList.fetch();
var book = bookList.findWhere({name: 'Use Backbone'});
book.set('name', 'No use Backbone';
book.save();
var newBook = new Book({name: 'Use XHR'});
bookList.add(newBook);
//предполагается что name используется в качестве id
//иначе что в этом примере, что в вашем, надо разруливать
//возможные совпадения названий у двух и более книг
var bookRes = api.books('Use another-rest-client');
var book = bookRes.get();
book.name = 'Always use another-rest-client!';
bookRes.put(book);
var newBook = api.books.post({name: 'Don't use pure XHR'});
Как видно, по коду значимых отличий нет. Разумеется, есть отличия, вызванные другой парадигмой (у вас есть неявный локальный кеш), но объём кода в случае another-rest-client абсолютно такой же. Что до понятности… Ну, вообще, если бы в моём случае были бы шоткаты для методов на полученном объекте, код был бы ещё понятней, хотя он и сейчас вроде не выглядит чем-то странным.
var book = api.books('Use another-rest-client').get();
book.name = 'Always use another-rest-client!';
book.$.put(); //вот такой магии пока нет и не факт что будет, хотя я думал о ней и до этой публикации
Из возможных отличий:
Как вы решаете проблему преобразования нестандартного ответа сервера в стандартные объекты?
Предполагается ли расширение коллекций и объектов логикой или это только простые хранилища данных (по аналогии с Data Gateway)?
Что подразумевается под нестандартным ответом? Если это какой-то кастомный Content-Type, можно зарегистрировать свой encoder/decoder, в readme это описано. Если ничего подобного нет, возвращается простая строка, с которой можно делать всё что душе угодно. Если сервер отвечает со статусом не 200, 201 или 204, Promise реджектится с инстансом xhr, с которым, опять же, можно делать что угодно.
Хранилищ данных там никаких нет — если только вы не подразумеваете под ними возвращаемые из промиз объекты. Последние — это просто преобразованные в удобоваримый вид ответы сервера, которые библиотекой вообще никак не запоминаются и не используются.
Что подразумевается под нестандартным ответом?
На пример ответ приходит не ввиде массива сущностей, а в таком виде:
{
collection: [
{сущность},
...
]
}
Последние — это просто преобразованные в удобоваримый вид ответы сервера, которые библиотекой вообще никак не запоминаются и не используются
Пример: что делать, если коллекция bookList должна включать нестандартный метод?
Ну, например:
var books = (await api.books.get()).collection;
Какой ещё нестандартный метод? Вроде бы в Rest API никаких нестандартных методов быть не может.
Какой ещё нестандартный метод? Вроде бы в Rest API никаких нестандартных методов быть не может
В REST API то не может, но может быть на уровне JS, на пример такой:
var bookList = new BookList;
var book = bookList.findFromAuthor('Name');
На самом деле это хорошая идея — дополнительный обработчик для определённых ресурсов. Впрочем, над этим ещё нужно поразмыслить. Любые методы и дополнительные обработчики — это уже ответственность data layer, а не простого клиента. И да, я понимаю к чему вы клоните — используй BackboneJS, Amareis, велосипеды не нужны (несложно было догадаться, учитывая что вы сказали это прямым текстом несколько комментов назад :)! Но, как я уже говорил, это разные уровни архитектуры и разные цели. Я написал простую обёртку для XHR, предназначенную для упрощения кода взаимодействия с определённым типом API. Конкретно для этого тот же бэкбон будет явным оверкиллом — в конце концов, я мог и jQuery для того плагина подтащить, не так уж это и страшно.
Объектная модель — это, безусловно, хорошо. Проблема в том, что это уже скорее тот самый data layer, нежели простой клиент. Я могу (и я подумываю об этом) сделать another-data-layer, который вполне может использовать another-rest-client в качестве бэкенда, но это будет уже совсем другая история.
А ещё, цепочка вызовов здесь оправдана тем, что она, по сути, является отражением итогового URL.
api.games(15).players(2).pet(4).get()
Превратится в:
GET http://example.com/api/v1/games/15players/2/pet/4
Отображение практически один в один, но при этом абсолютно не замусорено лишними символами и позволяет легко спрятать используемую несколько раз часть цепочки под алиас:
let me = api.games(15).players(2);
me.pets(4).get();
me.friends(17).delete();
В моих задачах этого пока более чем достаточно. Если будет мало — действительно, можно и свой data layer над этим надстроить, но цель этой библиотеки совсем другая.
Если будет мало — действительно, можно и свой data layer над этим надстроить
Вот я вам и предлагаю BackboneJS ) Конечно вы можете отказаться, ведь дело ваше.
Во-первых, это красиво… :)
Собственно, мне просто не нравится конструировать строки руками. Это выглядит хуже. Впрочем...
api.res('games/15/players/2/pet/4').get();
Такой код вполне легитимен, хотя библиотека на него и не рассчитана (внутри созданные ресурсы кешируются, так что если использовать такой стиль, можно однажды обнаружить что вкладка малость раздулась в памяти). Если вам это не нравится, скорее всего вам нужен rest, в котором как раз такой подход.
Мне вот ваш вариант со строчкой нравится больше, хотя бы потому, что я ее могу скопировать в REST client и выполнить не заморачиваясь перекодированием из часть().часть().часть() нотации в часть/часть/часть нотацию. Да и если ваш код придется читать кому-то, кто его первый раз видит — будет гораздо проще понять куда же вы ходите за данными, опять же поиск по коду можно делать.
Конструирование строк руками вы так и так делаете только в одном случае вы результат можете использовать как минимум двумя способами, а в другом только одним (переиспользование не только кода, но и артифактов кода). Ну и наверняка реализация будет проще без заворачивания строк в объекты.
Кстати если захочется выполнять какую-нибудь ODATA, то заварачивание выражений в объекты — это верный путь к костылям.
Ну, положим, просто так скопировать её у вас не получится потому что вообще-то она будет выглядеть так:
api.res('games/' + game.id + '/players/' + player.id +'/pet/' + pet.id);
//ну или так, что сути особо не меняет
api.res(`games/${game.id}/players/${player.id}/pet/${pet.id}`);
Все эти кавычки и прочие скобочки-плюсики — лишний визуальный мусор, который затрудняет чтение и понимание кода. Добавьте сюда усложнение алиасов — в них надо будет хранить строки и вручную прибавлять их в начало формируемой строки. Собственно, всё это ручное конструирование мне и не понравилось настолько, что я отказался от использования rest и взял restful.js.
* api.games(game.id).players(player.id).pet(pet.id).get()
* api.res(`games/${game.id}/players/${player.id}/pet/${pet.id}`)
но может ведь быть еще
* api.res(`games(${game.id})/players(${player.id})/pet(${pet.id})`)
что на сервере получается из предыдущего примера с помощью нехитрых настроек, так же как видите в случае строки — легким и непринужденным изменением строки, но вот когда у вас все завернуто в объекты, то вы уже вынуждены делать серьезный рефакторинг.
Но это уже будет не Rest API, так что использовать для него Rest client будет неразумно ;)
Честно говоря, я немного растерян. Ладно что я слышу об ODATA впервые, но все, абсолютно все API которые я когда-либо видел, делал или щупал, использовали традиционный путь со слешами! Это настолько базовая вещь что я действительно удивлён что она не эксклюзивна… Хотя постойте, ODATA делали в майкрософт?.. Хорошо, теперь я удивлён чуть поменьше.
Но всё равно считаю что это тот случай, когда спасуют все универсальные решения — тот же Backbone, например (хотя вот rest… Rest с этим справится, да); так что я готов с чистой совестью признать что тут мой велосипед не проедет. Хотя вы, конечно, всё ещё можете запихнуть сырую строку в метод res
, но я уже предупредил что это нецелевое использование, которое может вызывать undefined behavior, рак мозга и случайные чёрные дыры на орбите Земли.
Используйте на ваш страх и риск.
Но всё равно считаю что это тот случай, когда спасуют все универсальные решения — тот же Backbone, например
Backbone по умолчанию сам формирует url для запроса данных из API, но если вам его подход не нравится, переопределить алгоритм формирования url в Backbone (если я не путаю) не составляет особого труда.
(кстати перевод статьи промптом сумашедший — куда проще понять, что автор хотел сказать тут http://martinfowler.com/eaaCatalog/queryObject.html )
- get('/games/15/players/2/pet/4')…
- api.games(15).players(2).pet(4).get()…
В одном случае реализация не зависит от того как автор API структурировал свои адреса (например /games(15)/ или /games/15/ или вообще /games?gamesId=15), в другом зависит и как решать неоднозначности вроде /games/(15) не понятно.
Если считать скорость набора текста — во втором варианте писать больше надо.
Если размер приложения, опять же надо больше кода для реализации.
Если с точки зрения эффективности — опять же приложению надо делать обратную трансформацию из дерева выражений в строку.
Если с точки зрения эффективности разработки — строчку с URL легче и найти в коде и прочитать.
Выше я вам уже ответил — вспомните что id ресурсов вовсе даже не хардкодятся (по большей части).
get(`/games/${game.id}/players/${player.id}/pet/${pet.id}`)
api.games(game.id).players(player.id).pet(pet.id).get()
Писать получается меньше, читать — проще (лично мне, по-крайней мере). Насчёт эффективности вы правы — каждый такой вызов клонирует всё поддерево ресурсов, но я не думаю что эту задержку можно заметить в реальной жизни. Ну а насчёт размера приложения… Не знаю даже, вряд ли он будет у вас значимо меньше. У меня в коде вот это поведение занимает 70 строк, ещё около сотни — независимые функции, которые так или иначе придётся реализовывать.
Я не просто так сделал акцент на "client that makes your code lesser and more beautiful than without it" в описании репозитория.
На пример вот так:
var url = api.from('site.ru/api/games')
if(filter.gameId !== undefined)
url.games(filter.gameId);
if(filter.authorId !== undefined)
url.author(filter.authorId);
Вопрос нужна ли эта библиотека в остальных случаях, когда запросы очень даже статичны?
var me = api.humans('me');
var i = await me.get();
console.log(i); //just object, i.e. {id: 1, name: 'Amareis', profession: 'programmer'}
var post = await me.posts.post({site: 'habrahabr.ru', nick: i.name})
console.log(post); //object
Проблема в том, что каждый раз создавать и инициализировать XHR вручную — глупо. Наверняка вы напишете для этого некую обёртку. И тут, сюрприз-сюрприз, вы обнаружите что сами изобрели ещё один Rest API client :)
В общем-то another-rest-client именно так и появился на свет, я это и в статье описал.
Я, например, дольше буду вспоминать весь workflow XHR, чем клиенты суммарно времени потеряют, подтягивая несчастные 4 килобайта, которые весит минифицированный another-rest-client :)
Пусть здесь полежит: https://github.com/swagger-api/swagger-js
Dojo Toolkit: AMD, красиво, работает, ничего лишнего (не считая кучи мелких файлов, подгружаемых при инициализации).
Описываем хранилище моделей:
define([
"dojo/store/JsonRest"
], function(JsonRest){
return new JsonRest({
target: "/api/users/",
_getTarget: function(id){
if (typeof id !== "undefined") {
return this.target + id + "/"; // В Django принято ставить / в конце адреса
}
return this.target;
}
});
});
Где-то в коде:
require([
"dojo",
"stores/users"
], function(
dojo,
users
) {
// Получить список всех пользователей
users.query({
// Пустой объект - получить всё, а можно же и с параметрами
});
// Получить запись с id = 12
users.get(12).then(
// Тут всё стандартно - обработчики promise
// Однако, dojo не терпит пустоты, поэтому на обработчик ошибки ставьте
// dojo.noop, если писать свой лениво
);
// Обновление с перезаписью (можно и PATCH сделать, но это, мягко говоря, не для новичков)
users.put(userModel).then(function(updatedUser){
console.log(updatedUser);
}, dojo.noop);
// Удаление записи
users.delete(5).then();
});
Если кому-то интересно, специально для Django есть несколько строк кода, позволяющих в каждый запрос вставлять CSRF-токены.
https://learn.javascript.ru/fetch
'use strict';
fetch('/article/fetch/user.json')
.then(function(response) {
alert(response.headers.get('Content-Type')); // application/json; charset=utf-8
alert(response.status); // 200
return response.json();
})
.then(function(user) {
alert(user.name); // iliakan
})
.catch( alert );
нативно, просто и быстро, без дурацких велосипедов
А почему не был исследован этот вопрос? Почему ограничились предположением о том, что какой-то дядя «энтерпрайз» пришёл и сделал из хорошей и удобной библиотеки плохую и неудобную?
Может, библиотека развивалась от неудобного к удобному? Может, если пилить велосипед дальше, будет пройден ровно тот же самый путь?
Это хорошо, что вы сделали все сами. Но странно, что не рассмотрели такие варианты:
https://github.com/visionmedia/superagent
https://github.com/mzabriskie/axios
Запросы к Rest API из JavaScript компактно и красиво