Русский
Русский
English
Статистика
Реклама

Rest

Перевод Наилучшие практики создания REST API

17.07.2020 10:14:14 | Автор: admin
Всем привет!

Предлагаемая вашему вниманию статья, несмотря на невинное название, спровоцировала на сайте Stackoverflow столь многословную дискуссию, что мы не смогли пройти мимо нее. Попытка объять необъятное внятно рассказать о грамотном проектировании REST API по-видимому, удалась автору во многом, но не вполне. В любом случае, надеемся потягаться с оригиналом в градусе обсуждения, а также на то, что пополним армию поклонников Express.

Приятного чтения!


REST API одна из наиболее распространенных разновидностей веб-сервисов, доступных сегодня. С их помощью различные клиенты, в том числе, браузерные приложения, могут обмениваться информацией с сервером через REST API.

Следовательно, очень важно правильно проектировать REST API, чтобы по ходу работы не возникало проблем. Требуется учитывать вопросы безопасности, производительности, а также удобство использования API с точки зрения потребителя.

В противном случае мы спровоцируем проблемы для клиентов, пользующихся нашими API а это неприятно и раздражает. Если не следовать общепринятым соглашениям, то мы только запутаем тех, кто будет поддерживать наш API, а также клиентов, поскольку архитектура будет отличаться от той, которую каждый ожидает увидеть.

В этой статье будет рассмотрено, как проектировать REST API таким образом, чтобы они были просты и понятны для каждого, кто их потребляет. Обеспечим их долговечность, безопасность и скорость, так как данные, передаваемые клиентам через такой API, могут быть конфиденциальными.

Поскольку существует множество причин и вариантов отказа сетевого приложения, мы должны убедиться, что ошибки в любом REST API будут обрабатываться изящно и сопровождаться стандартными HTTP-кодами, которые помогут потребителю разобраться с проблемой.

Принимаем JSON и выдаем JSON в ответ

REST API должны принимать JSON для полезной нагрузки запроса, а также отправлять отклики в формате JSON. JSON это стандарт передачи данных. К его использованию приспособлена практически любая сетевая технология: в JavaScript есть встроенные методы для кодирования и декодирования JSON либо через Fetch API, либо через другой HTTP-клиент. В серверных технологиях используются библиотеки, позволяющие декодировать JSON практически без вмешательства с вашей стороны.

Существуют и другие способы передачи данных. Язык XML как таковой не очень широко поддерживается во фреймворках; обычно требуется преобразование данных в более удобный формат, а это обычно JSON. На стороне клиента, особенно в браузере, не так легко обращаться с этими данными. Приходится выполнять массу дополнительной работы всего лишь для того, чтобы обеспечить нормальную передачу данных.

Формы удобны для передачи данных, особенно если мы собираемся пересылать файлы. Но для передачи информации в текстовом и числовом виде можно обойтись без форм, поскольку в большинстве фреймворков допускается передача JSON без дополнительной обработки достаточно взять данные на стороне клиента. Это наиболее прямолинейный способ обращения с ними.

Чтобы гарантировать, что клиент интерпретирует JSON, полученный с нашего REST API, именно как JSON, следует установить для Content-Type в заголовке отклика значение application/json после того, как будет сделан запрос. Многие серверные фреймворки приложений устанавливают заголовок отклика автоматически. Некоторые HTTP-клиенты смотрят Content-Type в заголовке отклика и разбирают данные в соответствии с указанным там форматом.

Единственное исключение возникает, когда мы пытаемся отправлять и получать файлы, пересылаемые между клиентом и сервером. Тогда требуется обрабатывать файлы, полученные в качестве отклика, и посылать данные форм с клиента на сервер. Но это тема уже для другой статьи.

Также следует убедиться, что в отклике от наших конечных точек нам приходит именно JSON. Во многих серверных фреймворках данная возможность является встроенной.
Рассмотрим в качестве примера API, принимающий полезную нагрузку в формате JSON. В данном примере используется бэкендовый фреймворк Express для Node.js. Можно использовать в качестве промежуточного ПО программу body-parser для разбора тела запроса JSON, а затем вызвать метод res.json с объектом, который мы хотим вернуть в качестве отклика JSON. Это делается так:

const express = require('express');const bodyParser = require('body-parser');const app = express();app.use(bodyParser.json());app.post('/', (req, res) => {  res.json(req.body);});app.listen(3000, () => console.log('server started'));


bodyParser.json() разбирает строку с телом запроса в JSON, преобразуя ее в объект JavaScript, а затем присваивает результат объекту req.body.

Установим для заголовка Content-Type в отклике значение application/json; charset=utf-8 без каких-либо изменений. Метод, показанный выше, применим и в большинстве других бэкендовых фрейморков.

В названиях путей к конечным точкам используем имена, а не глаголы

В названиях путей к конечным точкам следует использовать не глаголы, а имена. Такое имя представляет объект с конечной точки, который мы оттуда извлекаем, либо которым мы манипулируем.

Дело в том, что в названии нашего метода HTTP-запроса уже содержится глагол. Ставить глаголы в названиях путей к конечной точке API нецелесообразно; более того, имя получается излишне длинным и не несет никакой ценной информации. Глаголы, выбираемые разработчиком, могут ставиться просто в зависимости от его прихоти. Например, кому-то больше нравится вариант get, а кому-то retrieve, поэтому лучше ограничиться привычным глаголом HTTP GET, сообщающим, что именно делает конечная точка.

Действие должно быть указано в названии HTTP-метода того запроса, который мы выполняем. В названиях наиболее распространенных методов содержатся глаголы GET, POST, PUT и DELETE.
GET извлекает ресурсы. POST отправляет новые данные на сервер. PUT обновляет имеющиеся данные. DELETE удаляет данные. Каждый из этих глаголов соответствует одной из операций из группы CRUD.

Учитывая два принципа, рассмотренных выше, для получения новых статей мы должны создавать маршруты вида GET /articles/. Аналогично, используем POST /articles/ для обновления новой статьи, PUT /articles/:id для обновления статьи с заданным id. Метод DELETE /articles/:id предназначен для удаления статьи с заданным ID.

/articles это ресурс REST API. Например, можно воспользоваться Express, чтобы выполнять со статьями следующие операции:

const express = require('express');const bodyParser = require('body-parser');const app = express();app.use(bodyParser.json());app.get('/articles', (req, res) => {  const articles = [];  // код для извлечения статьи...  res.json(articles);});app.post('/articles', (req, res) => {  // код для добавления новой статьи...  res.json(req.body);});app.put('/articles/:id', (req, res) => {  const { id } = req.params;  // код для обновления статьи...  res.json(req.body);});app.delete('/articles/:id', (req, res) => {  const { id } = req.params;  // код для удаления статьи...  res.json({ deleted: id });});app.listen(3000, () => console.log('server started'));


В вышеприведенном коде мы определили конечные точки для манипуляций над статьями. Как видите, в именах путей нет глаголов. Только имена. Глаголы употребляются только в названиях HTTP-методов.

Конечные точки POST, PUT и DELETE принимают тело запроса в формате JSON и возвращают отклик также в формате JSON, включая в него конечную точку GET.

Коллекции называем существительными во множественном числе

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

Множественное число используется, в частности, и для единообразия с принципами именования в базах данных. Как правило, в таблице содержится не одна, а много записей, и именуется таблица соответствующим образом.

При работе с конечной точкой /articles мы пользуемся множественным числом при именовании всех конечных точек.

Вложение ресурсов при работе с иерархическими объектами

Путь конечных точек, имеющих дело со вложенными ресурсами, должен составляться так: добавляем вложенный ресурс как имя пути, следующее вслед за именем родительского ресурса.
Необходимо убедиться, что в коде вложение ресурсов полностью совпадает с вложением информации в наших таблицах баз данных. Иначе возможна путаница.

Например, если мы хотим на определенной конечной точке получать комментарии к новой статье, то должны прикрепить путь /comments к концу пути /articles. В данном случае предполагается, что мы считаем сущность comments дочерней для article в нашей базе данных.

Например, это можно сделать при помощи следующего кода в Express:

const express = require('express');const bodyParser = require('body-parser');const app = express();app.use(bodyParser.json());app.get('/articles/:articleId/comments', (req, res) => {  const { articleId } = req.params;  const comments = [];  // код для получения комментариев по articleId  res.json(comments);});app.listen(3000, () => console.log('server started'));


В вышеприведенном коде можно использовать метод GET в пути '/articles/:articleId/comments'. Мы получаем комментарии comments к статье, которой соответствует articleId, а затем возвращаем ее в ответ. Мы добавляем 'comments' после сегмента пути '/articles/:articleId', чтобы указать, что это дочерний ресурс /articles.

Это логично, поскольку comments являются дочерними объектами articles и предполагается, что у каждой статьи свой набор комментариев. В противном случае данная структура может запутать пользователя, поскольку обычно применяется для доступа к дочерним объектам. Тот же принцип действует при работе с конечными точками POST, PUT и DELETE. Все они используют одно и то же вложение структур при составлении имен путей.

Аккуратная обработка ошибок и возврат стандартных кодов ошибок

Чтобы избавиться от путаницы, когда на API происходит ошибка, нужно обрабатывать ошибки аккуратно и возвращать коды отклика HTTP, указывающие, какая именно ошибка произошла. Таким образом те, кто поддерживает API, получают достаточную информацию для понимания возникшей проблемы. Недопустимо, чтобы ошибки обваливали систему, поэтому и без обработки их оставлять нельзя, и заниматься такой обработкой должен потребитель API.
Коды наиболее распространенных ошибок HTTP:

  • 400 Bad Request (Плохой Запрос) означает, что ввод, полученный с клиента, не прошел валидацию.
  • 401 Unauthorized (Не авторизован) означает, что пользователь не представился и поэтому не имеет права доступа к ресурсу. Обычно такой код выдается, когда пользователь не прошел аутентификацию.
  • 403 Forbidden (Запрещено) означает, что пользователь прошел аутентификацию, но не имеет права на доступ к ресурсу.
  • 404 Not Found (Не найдено) означает, что ресурс не найден
  • 500 Internal server error (Внутренняя ошибка сервера) это ошибка сервера, которую, вероятно, не следует выбрасывать явно.
  • 502 Bad Gateway (Ошибочный шлюз) означает недействительное ответное сообщение от вышестоящего сервера.
  • 503 Service Unavailable (Сервис недоступен) означает, что на стороне сервера произошло нечто непредвиденное например, перегрузка сервера, отказ некоторых элементов системы, т.д.


Следует выдавать именно такие коды, которые соответствуют ошибке, помешавшей нашему приложению. Например, если мы хотим отклонить данные, пришедшие в качестве полезной нагрузки запроса, то, в соответствии с правилами Express API, должны вернуть код 400:

const express = require('express');const bodyParser = require('body-parser');const app = express();// существующие пользователиconst users = [  { email: 'abc@foo.com' }]app.use(bodyParser.json());app.post('/users', (req, res) => {  const { email } = req.body;  const userExists = users.find(u => u.email === email);  if (userExists) {    return res.status(400).json({ error: 'User already exists' })  }  res.json(req.body);});app.listen(3000, () => console.log('server started'));


В вышеприведенном коде мы держим в массиве users список имеющихся пользователей, у которых известна электронная почта.

Далее, если мы пытаемся передать полезную нагрузку со значением email, уже присутствующим в users, то получаем отклик с кодом 400 и сообщение 'User already exists', означающее, что такой пользователь уже существует. Располагая этой информацией, пользователь может поправиться заменить адрес электронной почты на тот, которого пока нет в списке.

Коды ошибок всегда нужно сопровождать сообщениями, достаточно информативными для устранения ошибки, но не настолько подробными, чтобы этой информацией могли воспользоваться и злоумышленники, намеренные украсть нашу информацию или обвалить систему.

Всякий раз, когда нашему API не удается правильно завершить работу, мы должны аккуратно обработать отказ, отправив информацию об ошибке, чтобы пользователю было проще исправить ситуацию.

Разрешать сортировку, фильтрацию и разбивку данных на страницы

Базы, расположенные за REST API, могут сильно разрастаться. Иногда данных бывает настолько много, что все их невозможно вернуть за один раз, так как это замедлит систему или вообще обрушит ее. Следовательно, нам нужен способ фильтрации элементов.

Также необходимы способы разбивки данных на страницы (пагинации), чтобы за один раз мы возвращали всего несколько результатов. Мы не хотим занимать ресурсы слишком надолго, пытаясь вытянуть все запрошенные данные разом.

Как фильтрация, так и пагинация данных позволяют повысить производительность, сократив использование серверных ресурсов. Чем больше данных накапливается в базе, тем более важными становятся две эти возможности.

Вот небольшой пример, в котором API может принимать строку запроса с различными параметрами. Давайте отфильтруем элементы по их полям:

const express = require('express');const bodyParser = require('body-parser');const app = express();// информация о сотрудниках в базе данныхconst employees = [  { firstName: 'Jane', lastName: 'Smith', age: 20 },  //...  { firstName: 'John', lastName: 'Smith', age: 30 },  { firstName: 'Mary', lastName: 'Green', age: 50 },]app.use(bodyParser.json());app.get('/employees', (req, res) => {  const { firstName, lastName, age } = req.query;  let results = [...employees];  if (firstName) {    results = results.filter(r => r.firstName === firstName);  }  if (lastName) {    results = results.filter(r => r.lastName === lastName);  }  if (age) {    results = results.filter(r => +r.age === +age);  }  res.json(results);});app.listen(3000, () => console.log('server started'));


В вышеприведенном коде у нас есть переменная req.query, позволяющая получить параметры запроса. Затем мы можем извлечь значения свойств путем деструктуризации отдельных параметров запроса в переменные; для этого в JavaScript предусмотрен специальный синтаксис.
Наконец, мы применяем filter с каждым значением параметра запроса, чтобы найти те элементы, которые хотим вернуть.

Справившись с этим, возвращаем results в качестве отклика. Следовательно, при выполнении запроса GET к следующему пути со строкой запроса:

/employees?lastName=Smith&age=30

Получаем:
[    {        "firstName": "John",        "lastName": "Smith",        "age": 30    }]


в качестве возвращенного ответа, поскольку фильтрация производилась по lastName и age.
Аналогично, можно принять параметр запроса page и вернуть группу записей, занимающих позиции от (page - 1) * 20 до page * 20.

Также в строке запроса можно указать поля, по которым будет производиться сортировка. В таком случае мы можем отсортировать их по этим отдельным полям.
Например, нам может понадобиться извлечь строку запроса из URL вида:

http://example.com/articles?sort=+author,-datepublished

Где + означает вверх, а вниз. Таким образом, мы сортируем по имени автора в алфавитном порядке и по datepublished от новейшего к наиболее давнему.

Придерживаться проверенных практик обеспечения безопасности

Коммуникация между клиентом и сервером должна быть в основном приватной, так как зачастую мы отправляем и получаем конфиденциальную информацию. Следовательно, использование SSL/TLS для обеспечения безопасности обязательное условие.

Сертификат SSL не так сложно загрузить на сервер, а сам этот сертификат либо бесплатен, либо стоит очень дешево. Нет никаких причин отказываться от того, чтобы обеспечивать коммуникацию наших REST API по защищенным каналам, а не по открытым.

Человеку нельзя открывать доступ к большему объему информации, чем он запросил. Например, рядовой пользователь не должен получать доступа к информации другого пользователя. Также он не должен иметь возможности посмотреть данные администраторов.
Для продвижения принципа наименьших привилегий необходимо либо реализовать проверку ролей для отдельно взятой роли, либо обеспечить более точную детализацию ролей для каждого пользователя.

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

Кэшировать данные для улучшения производительности

Можно добавить возможность кэширования, чтобы возвращать данные из локального кэша памяти, а не извлекать некоторые данные из базы всякий раз, когда их запрашивают пользователи. Достоинство кэширования заключается в том, что пользователи могут получать данные быстрее. Однако, эти данные могут оказаться устаревшими. Это также бывает чревато проблемами при отладке в продакшен-окружениях, когда что-то пошло не так, а мы продолжаем смотреть на старые данные.

Существуют разнообразные варианты решений для кэширования, например, Redis, кэширование в оперативной памяти и многие другие. По мере надобности можно менять способ кэширования данных.

Например, в Express предусмотрено промежуточное ПО apicache, позволяющее добавить в приложение возможность кэширования без сложной настройки конфигурации. Простое кэширование в оперативной памяти можно добавить на сервер вот так:

const express = require('express');const bodyParser = require('body-parser');const apicache = require('apicache');const app = express();let cache = apicache.middleware;app.use(cache('5 minutes'));// информация о сотрудниках в базе данныхconst employees = [  { firstName: 'Jane', lastName: 'Smith', age: 20 },  //...  { firstName: 'John', lastName: 'Smith', age: 30 },  { firstName: 'Mary', lastName: 'Green', age: 50 },]app.use(bodyParser.json());app.get('/employees', (req, res) => {  res.json(employees);});app.listen(3000, () => console.log('server started'));


Вышеприведенный код просто ссылается на apicache при помощи apicache.middleware, в результате имеем:

app.use(cache('5 minutes'))

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

Версионирование API

У нас должны быть различные версии API на тот случай, если мы вносим в них такие изменения, которые могут нарушить работу клиента. Версионирование может производиться по семантическому принципу (например, 2.0.6 означает, что основная версия 2, и это шестой патч). Такой принцип сегодня принят в большинстве приложений.
Таким образом можно постепенно выводить из употребления старые конечные точки, а не вынуждать всех одновременно переходить на новый API. Можно сохранить версию v1 для тех, кто не хочет ничего менять, а версию v2 со всеми ее новоиспеченными возможностями предусмотреть для тех, кто готов обновиться. Это особенно важно в контексте публичных API. Их нужно версионировать, чтобы не сломать сторонние приложения, использующие наши API.
Версионирование обычно делается путем добавления /v1/, /v2/, т.д., добавляемых в начале пути к API.

Например, вот как это можно сделать в Express:

const express = require('express');const bodyParser = require('body-parser');const app = express();app.use(bodyParser.json());app.get('/v1/employees', (req, res) => {  const employees = [];  // код для получения информации о сотрудниках  res.json(employees);});app.get('/v2/employees', (req, res) => {  const employees = [];  // другой код для получения информации о сотрудниках  res.json(employees);});app.listen(3000, () => console.log('server started'));


Мы просто добавляем номер версии к началу пути, ведущего к конечной точке.

Заключение

Важнейший вывод, связанный с проектированием высококачественных REST API: в них необходимо сохранять единообразие, следуя стандартам и соглашениям, принятым в вебе. JSON, SSL/TLS и коды состояния HTTP обязательная программа в современном вебе.
Не менее важно учитывать производительность. Можно увеличить ее, не возвращая слишком много данных сразу. Кроме того, можно задействовать кэширование, чтобы не запрашивать одни и те же данные снова и снова.

Пути к конечным точкам должны именоваться единообразно. В их названиях нужно использовать существительные, поскольку глаголы присутствуют в названиях методов HTTP. Пути вложенных ресурсов должны следовать после пути родительского ресурса. Они должны сообщать, что мы получаем или чем манипулируем, так, чтобы нам не приходилось дополнительно обращаться к документации, чтобы понять, что происходит.
Подробнее..

Структура React REST API приложения TypeScript Styled-Components

09.03.2021 16:05:49 | Автор: admin

Доброго %время_суток, хабровчане!

Какие-либо статьи я еще не писал, опыта в этом нет от слова совсем. Поэтому, пожалуйста, не судите строго и прошу заранее прощения, если где-то совершу ошибки.

Это для меня совершенно новый опыт, мне никогда еще не доводилось делиться своим опытом и наработками с сообществом.

Предисловие

Когда я только начал знакомиться с React, мне приходилось искать, и искать довольно-таки долго, информацию, которая помогла бы мне грамотно организовать структуру моих будущих проектов, но дельной информации было мало.

Кто-то советовал делать плоскую структуру, в том числе и для папки, в которой находятся компоненты, кто-то же советовал просто разделять компоненты на "Молекулы", "Организмы" и т.д., но при этом, почти все, мало внимания уделяли сервисам, хукам, а именно тому, что содержало логику, не говорили о том, где лучше и как лучше их хранить.

НО, были и хорошие, одной из которых я вдохновился. В ней говорилось о том, что всю логику хранить в папке "Core". Ссылку на эту статью, на момент публикации, к сожалению, найти не могу, но как найду - обязательно прикреплю, либо отредактирую пост (если такое будет возможно, то на месте этого абзаца будет ссылка на статью), либо оставлю ссылку в виде комментария.

(!) Прошу вас, на время прочтения этой статьи, держать в голове мысль, что то, что тут предлагаю - это всего-лишь одна из идей того, как можно организовать структуру своего приложения. Не стоит воспринимать мои слова, как догму (это относится к тем, кто только начал свое знакомство с React), ведь я и сам новичок.

Буду очень рад, если вы оставите предложения по улучшению данной структуры, как и вашей конструктивной критике.

Components

Начну, пожалуй, с компонентов.

Компоненты, в данной структуре, разделяются на:

  • Умные (Smart)

  • Обычные (Ordinary)

  • Простые (Simple)

  • UI (UI, как ни странно)

  • Контейнеры (Containers)

  • Страницы (Pages)

Первые четыре группы (Smart, Ordinary, Simple и UI) хранятся в папке Components.

Поговорим немного о них:

  • UI компоненты - это те компоненты, которые заменяют нативные (стандартные) компоненты по типу: button, input, textarea, select и так далее.

    • Данные компоненты не могут использовать свое локальное хранилище и обращаться к глобальному.

  • Simple компоненты - это те компоненты, которые являются простыми, иначе говоря компоненты, в которых нет какой-либо логики, которые просто что-то рендерят.

    • Не могут использовать локальное хранилище и обращаться к глобальному.

    • Не могут использовать хуки, кроме тех, что изначально поставляются с React (за исключением useState).

    • Могут использовать в своей реализации UI компоненты.

  • Ordinary компоненты - это те компоненты, которые могут иметь какую-то логику, для отображения чего-либо.

    • Не могу использовать локальное хранилище, как и обращаться к глобальному.

    • Не могут использовать хуки, кроме тех, что изначально поставляются с React (за исключением useState).

    • Могут использовать в своей реализации Simple и UI компоненты.

  • Smart компоненты - это те компоненты, которые могут использовать относительно серьезную логику, для отображения чего-либо

    • Могут использовать локальное хранилище, как и обращаться к глобальному (не изменяя его)

    • Могут использовать все доступные хуки, кроме тех, что взаимодействуют с сетью

    • Могут использовать в своей реализации Ordinary, Simple и UI компоненты.

Структура папки Componets:

. src/     components/        ordinary        simple        smart        ui     ...

Оставшиеся две группы (Containers и Pages) имеют отдельные папки в корне приложения (папка src).

  • Containers - это те компоненты, которые формируют некие контейнеры, которые, в дальнейшем, используются для формирования страниц, которые, к тому же, содержать в своей реализации компоненты остальных четырех групп и при этом взаимодействовать, каким-либо образом, с сервисами или сетью, если говорить обобщенно.

  • Pages - это те компоненты, которые формируются благодаря контейнерам и компонентам из папки Components, если в этом есть необходимость. Могут, как и контейнеры, взаимодействовать с сервисами.

Структура корневой папки:

. src/     components/        ordinary        simple        smart        ui     containers     pages     ...

Сами компоненты должны иметь отдельные папки, есть 2 (это число не является константой) файла:

  • index.tsx - файл, в котором находится сам компонент

  • styled.ts - файл, в котором находятся стилизованные компоненты (его спокойно можно заменить на styles.sсss, либо же styles.css, в зависимости от того, чем вы пользуетесь для стилизации своих компонентов)

Пример компонента Align. Хотелось бы сказать, что этот компонент попадает под группу "Simple", так как он является глупым (не имеет нужды в локальном хранилище) и не заменяет никакой нативный, браузерный, UI компонент.

// index.tsximport React, { memo } from "react";import * as S from "./styled"; // Импортируем стилизованные компонентыconst Align = memo(({ children, axis, isAdaptable = false }: Readonly<Props>) => {return (<S.Align $axis={axis} $isAdaptable={isAdaptable}>{children}</S.Align>);});export { Align };export interface Props {axis: S.Axis;children?: React.ReactNode;isAdaptable?: boolean;}
// styled.tsimport styled, { css } from "styled-components";const notAdaptableMixin = css`width: 100%;height: 100%;max-height: 100%;max-width: 100%;`;const adaptableMixin = css<AlignProps>`width: ${(props) => !props.$axis.includes("x") && "100%"};height: ${(props) => !props.$axis.includes("y") && "100%"};min-width: ${(props) => props.$axis.includes("x") && "100%"};min-height: ${(props) => props.$axis.includes("y") && "100%"};`;export const Align = styled.div<AlignProps>`display: flex;flex-grow: 1;justify-content: ${(props) => (props.$axis.includes("x") ? "center" : "start")};align-items: ${(props) => (props.$axis.includes("y") ? "center" : "start")};${(props) => (props.$isAdaptable ? adaptableMixin : notAdaptableMixin)};`;export interface AlignProps {$axis: Axis;$isAdaptable?: boolean;}export type Axis = ("y" | "x")[] | "x" | "y";

Теперь, поговорим о самом сладком...

Core

Данная папка является "ядром" вашего приложения. В ней хранится все, для взаимодействия с сервером, глобальное хранилище, тема вашего приложения и т.д.

Эта папка содержит:

  • Config - в данной папке хранятся конфигурационные файлы приложения (например в ней можно хранить данные, необходимы для взаимодействия с бэкендом)

  • Constants - в данной папке находятся все константы, что используются в приложении (например сообщения об ошибках и предупреждениях)

  • Hooks - в данной папке хранятся все хуки кастомные хуки (хуки, что были сделаны вами).

  • Models - в данной папке хранятся модели, что приходят с бэкенда.

  • Schemes - в данной папке хранятся схемы форм, таблиц и т.д.

  • Services - в данной папке хранятся сами сервисы, благодаря которым и происходит общение с бэкендом.

  • Store - в данной папке хранятся схемы глобального хранилища (если Вы используете MobX), если же вы отдаете предпочтение Redux, то в данной папке могут хранится экшены, редьюсеры и т.д.

  • Theme (для Styled-Components) - в данной папке хранятся темы приложения.

  • Types - в данной папке хранятся вспомогательные типы, а также декларации модулей.

  • Utils - в данной папке хранятся вспомогательные, простые, функции, которые могут использоваться в хуках, либо же компонентах.

  • api.ts - в данном файле находится экземпляр HTTP клиента (например axios), который используют сервисы и который какой-то мутирует данные запросы (для передачи каких-либо заголовков, например).

Примеры содержимого папок
// config/api.config.tsexport const serverURI = "http://localhost:8080";export const routesPrefix = '/api/v1';// config/routes.config.tsimport { routesPrefix } from "./api.config";export const productBrowserRoutes = {getOne: (to: string = ":code") => `/product/${to}`,search: (param: string = ":search") => `/search/${param}`,};export const productAPIRoutes = {getOne: (code: string) => `${routesPrefix}/product/code/${code}`,search: () => `${routesPrefix}/product/search`,};

Довольно-таки удобно хранить все роуты в одном файле. Если вдруг со стороны бэкенда изменятся роуты, то их легко можно будет изменить в одном файле, не проходясь, при этом, по всему проекту.

// constants/message.constants.tsexport const UNKNOWN_ERROR = "Неизвестная ошибка";
// hooks/useAPI.ts// Хук для взаимодействия с сервисами/* eslint-disable react-hooks/exhaustive-deps */import { useCallback, useEffect } from "react";import { useLocalObservable } from "mobx-react-lite";import type { API, Schema, Take } from "@core/types";function useAPI<F extends API.Service.Function<API.Response<any>>,R extends Take.FromServiceFunction.Response<F>,P extends Parameters<F>>(service: F, { isPendingAfterMount = false, isIgnoreHTTPErrors = false }: Options = {}) {const localStore = useLocalObservable<Store>(() => ({isPending: {value: isPendingAfterMount,set: function (value) {this.value = value;},},}));const call = useCallback(async (...params: P): Promise<R["result"]> => {localStore.isPending.set(true);try {const { data } = await service(...params);const { result } = data;localStore.isPending.set(false);return result;} catch (error) {if (isIgnoreHTTPErrors === false) {console.error(error);}localStore.isPending.set(false);throw error;}},[service, isIgnoreHTTPErrors]);const isPending = useCallback(() => {return localStore.isPending.value;}, []);useEffect(() => {localStore.isPending.set(isPendingAfterMount);}, [isPendingAfterMount]);return {call,isPending,};}export { useAPI };export interface Options {isPendingAfterMount?: boolean;isIgnoreHTTPErrors?: boolean;}type Store = Schema.Store<{ isPending: boolean }>;
// models/product.model.ts// Описание модели товараexport interface ProductModel {id: number;name: string;code: string;info: {description: string;note: string;};config: {isAllowedForPurchaseIfInStockZero: boolean;isInStock: boolean;};seo: {title: string;keywords: string;description: string;};}
// services/product.service.ts// Сервисы для взаимодействия с товарамиimport { api } from "../api";import { routesConfig } from "../config";import type { ProductModel } from "../models";import type { API } from "../types";export function getOne(code: string) {return api.get<API.Service.Response.GetOne<ProductModel>>(routesConfig.productAPIRoutes.getOne(code));}
// theme/index.ts// Тема приложенияimport { DefaultTheme } from "styled-components";export const theme: DefaultTheme = {colors: {primary: "#2648f1",intense: "#151e27",green: "#53d769",grey: "#626b73",red: "#f73d34",orange: "#fdb549",yellow: "#ffe243",white: "white",},};
// types/index.tsx// Вспомогательные типыimport type { AxiosResponse } from "axios";export namespace API {export namespace Service {export namespace Response {export type Upsert<T> = Response<T | null>;export type GetOne<T> = Response<T | null>;export type GetMany<T> = Response<{rows: T[];totalRowCount: number;totalPageCount: number;}>;}export type Function<T extends API.Response<any>, U extends any[] = any[]> = (...params: U) => Promise<AxiosResponse<T>>;}export type Response<T> = {status: number;result: T;};}
// utils/throttle.tsfunction throttle<P extends any[]>(func: (...params: P) => any, limit: number) {let inThrottle: boolean;return function (...params: P): any {if (!inThrottle) {inThrottle = true;func(...params);setTimeout(() => (inThrottle = false), limit);}};}export { throttle };
// store/index.tsximport { createContext } from "react";import { useLocalObservable } from "mobx-react-lite";import { app, App } from "./segments/app";import { layout, Layout } from "./segments/layout";import { counters, Counters } from "./segments/counters";export const combinedStore = { layout, app, counters };export const storeContext = createContext<StoreContext>(combinedStore);export function StoreProvider({ children }: { children: React.ReactNode }) {const store = useLocalObservable(() => combinedStore);return <storeContext.Provider value={store}>{children}</storeContext.Provider>;}export type StoreContext = {app: App;layout: Layout;counters: Counters;};
// api.ts// Экземпляр AXIOS для взаимодействия с серверомimport axios from "axios";import { apiConfig } from "./config";const api = axios.create({baseURL: apiConfig.serverURI,});api.interceptors.request.use((req) => {return {...req,baseURL: apiConfig.serverURI,};});export { api };

Ух ты! Как же много получилось.

И напоследок...

Есть еще несколько, немаловажных папок, которые также следует упомянуть:

  • Assets - в данной папке хранятся все статичные файлы, такие как: иконки, изображения, шрифты и т.д. (их, конечно же, также стоит группировать и разделять на папки)

  • Routes - в данной папке (либо же файле, кому как больше нравится) хранятся все роуты приложения (пример будет ниже).

  • Styles - в данной папке хранятся все глобальные стили, которые применяются ко всем элементам и документу, в том числе.

// routes/index.tsximport { Switch, Route } from "react-router-dom";// Экспортируем страницыimport { Product } from "../pages/Product";...import { NotFound } from "../pages/NotFound";import { routesConfig } from "../core/config";const Routes = () => {return (<Switch><Route exact path={routesConfig.productBrowserRoutes.getOne()}><Product /></Route>{/* Объявляем как-то роуты */}<Route><NotFound /></Route></Switch>);};export { Routes };

Остается еще 2 файла:

  • app.tsx - компонент приложения

Примерно так он может выглядеть:

// app.tsximport React, { useEffect } from "react";// Импортирует роутыimport { Routes } from "./routes";const App = () => {return (<Routes />);};export { App };
  • index.tsx - входной файл вашего приложения

Он же может выглядеть примерно так:

import React from "react";import ReactDOM from "react-dom";import { BrowserRouter } from "react-router-dom";import { ThemeProvider } from "styled-components";// импортируем нашеimport { App } from "./app";// импортируем глобальные стилиimport { BodyStyles } from "./styles";import { StoreProvider } from "../core/store";// импортируем темуimport { theme } from "../core/theme";import reportWebVitals from "./reportWebVitals";const app = document.getElementById("app");ReactDOM.render(<React.StrictMode><ThemeProvider theme={theme}><BodyStyles /><BrowserRouter><StoreProvider><App /></StoreProvider></BrowserRouter></ThemeProvider></React.StrictMode>,app);reportWebVitals();

И на этом, я думаю, стоит закончить.

Итоговая структура выглядит вот так:

. src/     assets/        fonts        icons     components/        ordinary        simple        smart        ui     containers     core/        config        constants        hooks        models        schemes        services        store        theme        types        utils        api.ts     pages     routes     styles     app.tsx     index.tsx

Заключение

Если вам понравилась эта статья и вы узнали что-то интересное для себя, то я очень этому рад.

Ссылка на репозиторий (за такой скудный ридми, мне еще не по силам разговорный английский).

Всем удачи и огромное спасибо за внимание.

Подробнее..

Код на React и TypeScript, который работает быстро. Доклад Яндекса

12.01.2021 12:17:05 | Автор: admin
Евангелисты Svelte и других библиотек любят показывать примеры тормозящих компонентов на React. React и TypeScript дают много возможностей создавать медленный код. После доклада Виктора victor-homyakov вы сможете писать более производительные компоненты без усложнения кода.

Здравствуйте, меня зовут Виктор, я один из разработчиков страницы поиска Яндекса. На ней каждый день сотни миллионов пользователей вводят свои запросы, получают страницу со ссылками или сразу с правильными ответами. Из-за такого количества запросов нам очень важно, чтобы наш код работал оптимально. И, конечно, я сразу должен затронуть тему преждевременной оптимизации кода.

О преждевременной оптимизации




Многие из вас не один раз слышали эту фразу, а некоторые даже сами ее произносили: Не занимайтесь преждевременной оптимизацией. Фраза родилась уже довольно давно, в то время, когда писали на языках довольно низкого уровня и единственной методикой разработки был waterfall.

Это значило, что чаще всего проект в первый раз собирался только перед сдачей в тестирование и эксплуатацию. Поэтому было очень сложно заранее угадать, какая из тысяч строк кода, какой из сотен модулей потребуют оптимизации. Тогда эта фраза была актуальна.



Но прошли годы и десятилетия. У нас появились новые методологии разработки, появилась концепция MVP. Мы как можно раньше собираем рабочий прототип и отдаем его в тестирование и эксплуатацию. Кроме того, мы можем запускать отдельные компоненты, не дожидаясь всего проекта. Для этого у нас есть и тесты, и Storybook. Но та же самая мантра повторяется до сих пор не занимайтесь преждевременной оптимизацией, когда бы и на каком бы этапе жизни проекта вы бы ни спросили.



В результате современный фронтенд имеет то, что имеет: самые медленные инструменты сборки, самые тормознутые интерфейсы, самые большие размеры собранных файлов. Для Single Page-приложений гигантские бандлы в мегабайты никого не удивляют. И папка node_modules одна из самых жирных во всех проектах. Например, у нас на странице поиска она уже превысила три гигабайта и продолжает расти.



О чем же будет мой доклад? В первую очередь, наши языки, TypeScript и JavaScript, и наши библиотеки и фреймворки подразумевают, что практически у каждой задачи есть несколько вариантов решений. Все они правильные, все дают нужный результат, но не все одинаково эффективны.

Видеть эти варианты заранее и выбирать нужные, а не выбирать заведомо плохой, это не преждевременная оптимизация. Те тривиальные приемы, про которые я расскажу, дают при консистентном использовании до 5% производительности кода. Это по данным реальных проектов, которые переходили со старых стеков на использование React и TypeScript. Первая часть про React.

React


Лишние ререндеры


Самая большая проблема в React это лишние ререндеры. Вообще, библиотека React была создана с упором на то, чтобы как можно чаще и безболезненнее ререндерить всё дерево компонентов. При этом сами компоненты должны отсекать лишние ререндеры там, где пропсы у них не изменились.


Источник

Для этого разработчиками предусмотрены штатные средства и для функциональных компонентов, и для классовых. Но мы очень часто обманываем React и заставляем его делать ререндеры там, где это не нужно.



Один из наиболее частых антипаттернов создание новых объектов и массивов в методе рендера. Как вы понимаете, в случаях, когда нам нужны стиль или значение по умолчанию, свежесозданный объект не равен предыдущему даже если у него внутри полностью такой же набор свойств.

То есть пропсы у вложенных компонентов в таком случае принудительно изменяются, и оптимизации, которые мы рассматривали на предыдущем слайде, не работают. Компонент ререндерится, даже если у него визуально ничего не изменилось.



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

Здесь новая стрелочная функция тоже не равна точно такой же предыдущей, и новый результат bind не равен предыдущему. То есть мы опять обманываем вложенные компоненты и заставляем их ререндериться без нужды.


Источник

Многие хуки, такие как useState и useReducer, возвращают из себя какие-то функции. В данном случае setCount. И очень просто на лету сгенерировать стрелочную функцию, использующую setCount, чтобы передать ее во вложенный компонент.

Мы знаем из предыдущего примера, что эта новая функция заставит вложенный компонент перерендериться. Хотя разработчики React и хуков явно говорят в документации, что функции, которые возвращаются из useState и из useReducer, не меняются при ререндерах. То есть вы можете получить самую первую функцию, запомнить ее и не перегенерировать свои функции и пропсы при новых вызовах useState. Это очень важно, это часто забывают.

Если вы пишете свои хуки, тоже обратите на это внимание, чтобы ваши функции, возвращаемые из хуков, удовлетворяли этому же требованию, чтобы можно было запомнить первую функцию и потом ее переиспользовать, не заставляя ререндериться вложенные компоненты.

const Foo = () => (    <Consumer>{({foo, update}) => (...)}</Consumer>);const Bar = () => (    <Consumer>{({bar, update}) => (...)}</Consumer>);const App = () => (    <Provider value={...}>        <Foo />        <Bar />    </Provider>);

Про контекст. Предположим, у нас небольшое приложение или жесткое ограничение на размер файлов, которые скачиваются на клиент, и мы не хотим втаскивать тяжелую библиотеку типа Redux или других библиотек для управления состоянием то есть мы считаем их слишком тяжелыми или медленными. Тогда мы можем использовать контекст, чтобы прокинуть свойства до глубоко вложенных компонентов.

Минимальный пример выглядит примерно так. При этом мы можем захотеть сэкономить и вместо двух разных контекстов завести один, в котором хранятся все нужные нам свойства.


Ссылка со слайда

В этом есть две потенциальных проблемы. Первая: внутри Context Provider при изменении контекста может перерендериться все, что в него вложено, то есть непосредственно все, что вложено внутри Provider, и те компоненты, которые зависят от контекста, и те, которые не зависят. Очень важно, когда вы пишете такие вещи с использованием контекста, сразу же проверить, чтобы такого не было.

Советуют при этом делать так: выносить провайдер контекста в отдельный компонент, внутри которого не будет ничего кроме children, и уже в этот компонент оборачивать компоненты, куда дальше передавать контекст.


Источник

Вторая потенциальная проблема: у нас два не связанных между собой свойства в контексте, и при изменении одного из них ререндерятся все Consumer, даже те, которых это изменение не должно касаться.


Ссылка со слайда

Разработчиками React и контекста предусмотрен способ, как это предотвратить.

Есть битовые маски. При задании контекста мы указываем функцию, которая указывает в битовой маске, что именно изменилось в контексте. И в конкретном Context Consumer мы можем указать битовую маску, которая будет фильтровать изменения и ререндерить вложенный компонент, только если изменились те биты, которые нам нужны.


Ссылка со слайда

Пакет, который называется Why Did You Render, это однозначный must have для всех, кто борется с лишними ререндерами. Он лежит в NPM, ставится довольно легко и в режиме разработчика позволяет в консоли Developer Tools браузера отследить все компоненты, которые перерендериваются, хотя фактически содержимое props и state у них не изменилось.

Вот пример скриншота. Это тот же антипаттерн, когда мы генерируем на каждый рендер новый объект в атрибуте style. При этом в консоли выведется предупреждение, что props фактически не изменились, а изменились только по ссылке, и вы этого ререндера могли избежать.

Если подвести итог, что у нас есть для борьбы с лишними ререндерами:



  • Пакет Why Did You Render. Это must have в любом проекте, у любого разработчика на React.
  • В Developer Tools браузера Chrome можно включить опцию Paint flashing. Тогда он будет подсвечивать те области экрана, которые перерисовались. Вы визуально заметите, что и как часто у вас ререндерится.
  • Самое убойное средство это в каждый рендер вставить console.log. Это позволяет оценить, сколько вообще у вас ререндеров: и нужных, и ненужных.
  • И еще одна вещь: часто забываемый второй параметр в React.memo. Это функция, которая позволит вручную написать код сравнения props с предыдущими и самому возвращать true/false, то есть дополнительно к сравнению по ссылке сравнивать какое-то содержимое. Функция аналогична методу shouldComponentUpdate для классовых компонентов.

HTML-комментарии


Следующий интересный момент комментарии в HTML-коде, который сгенерирован на сервере.

ReactDOMServer.renderToString(    <div>{someVar}bar</div>);<div data-reactroot="">foo<!-- -->bar</div>

В местах склейки статического текста и текста из JavaScriptовых переменных React вставляет HTML-комментарий. Это сделано, чтобы безболезненно гидрировать такие места на клиенте.

ReactDOMServer.renderToString(    <div>{`${someVar}bar`}</div>);<div data-reactroot="">foobar</div>

Если вам нужно удалить такой комментарий, то вы склеиваете строки в JS-коде и вставляете в JSX всю склеенную строку, как в этом примере. Почему это важно?


Источник

Представьте, что вы разрабатываете интернет-магазин или список товаров. В строке диапазона цен товара получается целых четыре комментария в местах склейки. Если вы видите на странице список из 100 товаров, то у вас отрендерятся три килобайта HTML-комментариев.

То есть при server-side rendering мы вынуждены потратить лишние ресурсы процессора, лишнюю память и лишнее время на то, чтобы их отрендерить. Мы должны передать на клиент эту лишнюю разметку, а браузер должен эти три килобайта распарсить. И пока страница будет открыта, браузер будет держать их в памяти, потому что они присутствуют в дереве DOM документа.

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

HOC


function withEmptyFc(WrappedComponent) {    return props => <WrappedComponent {...props} />;}function withEmptyCc(WrappedComponent) {    class EmptyHoc extends React.Component {        render() {            return <WrappedComponent {...this.props} />;        }    }    return EmptyHoc;}

Про HOC. Сегодня на Я.Субботнике уже рассказывали про него. Пустой минимальный HOC, который не делает ничего, выглядит примерно так. Вот два примера: в функциональном и в классовом стиле.



Если замерить производительность server-side rendering, то пустая кнопка, классическая кнопка HTML, рендерится 0,9 микросекунды. Если мы ее обернем в пустой HOC, который не делает ничего, то увидим, что это уже добавляет замедление в рендеринг.

А если мы в этот HOC добавим еще и полезной нагрузки (приведен пример реального HOC из нашего проекта), то увидим, что скорость рендеринга замедлилась еще больше. Почему так происходит?



При server side rendering и при первом рендеринге на клиенте HOC всегда делает вызов React.createElement. Это довольно сложная функция, которая выполняет довольно много работы внутри самой библиотеки React. Она не может не занимать дополнительного времени.

Также происходит копирование props. Мы снаружи HOC получили какие-то props и должны сформировать новые props для вложенного в HOC компонента. Это тоже занимает время.



При ререндере у нас никуда не делся React.createElement. Также HOC добавляет обертку в дереве. Сравнение с предыдущим деревом и обход дерева замедляет работу с ним.



В итоге на продакшене это может выглядеть как результат угара по HOC. Только половина разметки в дереве это полезная нагрузка, а оставшаяся половина это context consumer, context provider и разнообразные HOC.

То есть React работает с деревом, которое стало в два раза больше, чем без HOC. Он не может не тратить дополнительное время на обработку этого дерева.



И еще один важный момент. Если мы напишем слишком сложный HOC, то можем наткнуться на полную замену дерева при ререндере вместо update предыдущего. Расскажу про это немножко подробнее.

switch (workInProgress.tag) {  case IndeterminateComponent: {    //     return mountIndeterminateComponent();  }  case FunctionComponent: {    //     return updateFunctionComponent();  }  case ClassComponent: {    //     return updateClassComponent();  }

При ререндере React смотрит: если у нас функциональный компонент или классовый, он производит update, то есть берет новое и старое дерево, сравнивает их, находит между ними минимальный дифф и только эти изменения внедряет в старое дерево.

Но если код получается слишком сложный, то React не понимает, что мы от него хотим, и просто монтирует новое дерево взамен старого. Это можно заметить, если у вас есть компоненты, которые при монтировании выполняют какую-то работу запросы на бэкенд, генерирование uids и т. п. Так что следите за этим.



Если у вас есть выбор, где реализовать вашу функциональность, в HOC или в хуке, то однозначно рекомендую хук. Он по размеру кода меньше, он всего лишь вызов функции, которая не несет смысла внутри библиотеки React, в то время как в HOC, я уже говорил, React.createElement сложная вещь. И в HOC добавляются уровни вложенности и прочая ненужная работа. Если можно ее избежать избегайте.

Изоморфный код



Про изоморфный код. Евангелисты изоморфизма не очень любят углубляться в детали того, как же их изоморфный код работает на наших серверах и наших клиентах. Проблема в том, что мы контролируем наш бэкенд, можем на нем доставить свежую Node.js, которая понимает последний диалект ECMAScript. В то же время на клиенте до сих пор значительная доля древних браузеров, например Internet Explorer 11 или старые Android: четвертый и немножко новее. Поэтому клиентам до сих пор все равно очень часто нужно отдавать ES5.

Поэтому никакими полифилами вы не сможете добавить на клиент понимание нового синтаксиса: стрелочных функций, классов, async await и прочих вещей.

Таким образом, изоморфный код, который нам подсовывают из разных библиотек или который получается с настройками системы сборки по умолчанию, просто не использует все возможности нашей версии Node.js. Это не очень хорошо.

Мы бы хотели, когда пишем изоморфный код на TypeScript, так настроить сборку, чтобы наш TypeScript компилировался в максимально свежий диалект для Node.js. Чтобы именно этот скомпилированный код исполнялся на Node.js при server side rendering. И чтобы для браузеров TypeScript компилировался в подходящий диалект, ES5 или чуть более новый, если вы собираете разные версии кода для старых и новых браузеров.



Если же мы пишем сразу на ECMAScript, то можем нативно писать для Node.js, и в этом случае бонусом будет то, что нам не нужны никакие системы сборки и бандлинга. Мы сразу пишем код, который нативно понимается Node.js. Node.js умеет использовать модульные системы: CommonJS через require, ESM через import. И нам надо только скомпилировать в ES5 для браузеров и собрать в бандлы.

К сожалению, когда рассматривается изоморфный код, об этом часто забывают. В одном из примеров изоморфного кода, который я видел, код вообще компилировался в ES3, и потребители изоморфного кода из этой библиотеки вынуждены были это терпеть на сервере, сжать зубы и выполнять древний код со всеми полифилами для того, что и так уже было в Node.js.

TypeScript


Дизайн языка


Мы плавно перешли к TypeScript. Сначала очень важно упомянуть про дизайн языка. Агрессивная оптимизация производительности скомпилированных программ и система типов, которая позволяет на этапе компиляции доказать, что ваша программа корректна, это все не является целями дизайна TypeScript. Не является приоритетом при его дальнейшем развитии.



Это официально написано в документации TypeScript. Так что, пожалуйста, оставьте надежды, что вы сможете так сильно типизировать ваш код, что он будет за вас проверять правильность вашей программы. И не надейтесь, что компилятор сделает за вас всю грязную работу и напишет оптимальный код в результате компиляции.

Spread operator


О чем я хотел бы сказать в первую очередь, это оператор Spread.


Он очень часто используется в коде на React. Но то, что его легко написать, не означает, что его так же легко выполнять.



Потому что при компиляции такого кода TypeScript запишет в модуль на ES5, во-первых, реализацию метода __assign, а во-вторых, его вызов. То есть фактически воткнет полифил для Object.assign.

И только при компиляции в более новые диалекты он будет использовать сам Object.assign. Проблема в том, что если вы пишете на TypeScript библиотеки и компилируете их проверьте, чтобы в каждом скомпилированном модуле не было понатыкано реализаций этих __assign. То есть не заставляйте потребителя с каждым модулем снова и снова получать код реализации __assign. Для этого есть соответствующие настройки компиляции TypeScript.

И еще одна проблема: Object.assign, если вы знаете, означает клонирование объекта. Клонирование объекта выполняется не за константное время. Чем сложнее объект, чем больше в нем полей, тем больше времени будет занимать клонирование. И с этим связан такой пример фейла.

Это код, который успешно прошел код-ревью, и оказался в продакшене. Казалось бы, здесь ничего сложного нет, все должно красиво работать.



Проблема в том, что на каждой итерации мы выполняем клонирование предыдущего объекта. И соответственно, на N+1 итерации мы вынуждены будем склонировать объект, в котором уже N полей.

Те, кто разбирается в алгоритмах, понимают, что сложность этого алгоритма O(N2). То есть чем больше исходный массив, тем с квадратичной зависимостью медленнее будет работать такой простенький код. Легко написать, сложно выполнить, как я уже говорил.

Бывает еще вот такой фейл при использовании spread с массивами.



Здесь вы сразу заметили квадратичную сложность вместо линейной. И здесь легче увидеть еще одну проблему: при каждой итерации мы выделяем память для нового массива, потом копируем в него содержимое старого массива и освобождаем его память.

А если массив начинает занимать гигабайт? Представьте: во-первых, постоянно занято 3 ГБ одновременно (1 ГБ исходный массив, 1 ГБ предыдущая копия и 1 ГБ следующая). Во-вторых, на каждой итерации мы копируем из предыдущего расположения массива в следующее 1 ГБ плюс 1 элемент, 1 ГБ плюс 2 элемента и т. д. Ваша задача заметить такое на код-ревью и не пустить в продакшен.



Также надо заметить, что порядок расположения spread и остальных полей объекта влияет на то, какой код будет сгенерирован. Например, при таком расположении будет сгенерирован один вызов assign.

// TS:res = {...obj, a: 1};// компилируется в ES5:res = __assign(__assign({}, obj), {a: 1});// хотелось бы:res = __assign({}, obj, {a: 1});// илиres = __assign({}, obj);res.a = 1;

Если же порядок поменяется, это будет означать уже два вложенных вызова assign. Хотя мы хотели бы один вызов или вообще запись поля a в объект результата. Почему так происходит? Напоминаю, что генерация оптимального кода не цель написания и развития языка TypeScript. Он просто обязан учитывать гипотетические крайние случаи: например, когда в объекте есть getter и поэтому он строит универсальный код, который в любых случаях работает правильно, но медленно.



Справедливости ради нужно сказать, что в TSX оптимально компилируется похожий случай, когда есть два объекта props и вы передаете их в компонент таким образом. Здесь будет всего один вызов assign и компилятор понимает, что надо делать эффективно.

Rest operator


Двоюродный родственник Spread-оператора это Rest. Те же три точечки, но по-другому.


У нас в коде это чаще всего используется в деструктурировании. Вот один из примеров. Здесь под капотом, чтобы получить объект otherProps, надо выполнить следующую нетривиальную работу: из объекта props скопировать в новый объект otherProps все поля, название которых не равно prop1, prop2 или prop3.

Чувствуете, к чему я клоню? При компиляции в ES5 получается примерно такой код:

var blackList = ['prop1', 'prop2', 'prop3'];var otherProps = {};// Цикл по всем полямfor (var p in props)    if (        hasOwnProperty(props, p) &&        // Вложенный цикл  поиск в массиве indexOf(p)        blackList.indexOf(p) < 0    )        otherProps[p] = props[p];

Мы итерируемся по всем полям исходного объекта и внутри выполняем поиск каждого объекта по массиву, который происходит за время, зависящее от размера массива blackList. То есть мы можем получить квадратичную сложность на, казалось бы, простой операции деструктурирования. Чем сложнее деструктурирование, чем больше полей в нем упоминается, тем медленнее оно будет работать, с квадратичной зависимостью.

Нативная поддержка Rest в новых Node.js и новых браузерах не спасает. Вот пример бенчмарка (к сожалению, сейчас сайт jsperf.com лежит), который показывает, что даже примитивная реализация Rest с помощью вспомогательных функций чаще всего работает не медленнее, а даже быстрее нативного кода, который сейчас реализован в Node.js и браузерах.



Второй вариант использования Rest в аргументах. Мы хотим красиво описать аргументы, дать им имена и типы. Но бывает так, что потом мы их собираем и передаем в следующую функцию без изменения, в таком же порядке.

// хотелось бы ES5:Component.prototype.fn1 = function(path) {    utils.fn2.apply(utils, arguments);};

Мы бы хотели, чтобы TypeScript понимал такие кейсы и генерировал вызов apply, передавая в него arguments.

// получаем замедление в ES5:Component.prototype.fn1 = function(path) {    var vars = [];    for (var _i = 1; _i < arguments.length; _i++) {        vars[_i - 1] = arguments[_i];    }    utils.fn2.apply(utils, __spreadArrays([path], vars));};

Но опять же, TypeScript действует максимально надежно и медленно. Он копирует часть аргументов в промежуточный массив. Потом создает еще один массив из первого аргумента и сливает их в один новый массив, делая кучу ненужной работы и замедляя ваш код.

Если вы пишете библиотеки, смотрите на скомпилированный код внимательно. Такие случаи желательно расписать руками максимально эффективно, вместо того чтобы надеяться на компилятор TypeScript.

=> вместо bind


В относительно свежих диалектах языка появилась интересная фича стрелочная функция в методах классов. Выглядит это примерно так.



Мы можем описать метод стрелочной функцией, и у него автоматически будет привязан контекст каждого экземпляра при вызове. То есть нам не надо явно вызвать bind. Казалось бы, это хорошо. На самом деле это тянет за собой очень много минусов.


Источник

Под капотом такая конструкция означает вот что: в конструкторе объекта создается поле onClick, где записывается стрелочная функция, привязанная к контексту. То есть в прототипе метод onClick не существует!



  • Самый очевидный минус: каждый конструктор тратит время на создание этой новой функции.
  • Ее код не шарится между экземплярами. Он существует в стольких же экземплярах, сколько у вас создано экземпляров MyComponent.
  • Вместо N вызовов одной функции вы получаете по одному вызову N функций в каждом из независимых экземпляров. То есть оптимизатор на такую функцию внимания не обращает, не хочет ее инлайнить или оптимизировать. Она выполняется медленно.

Это только минусы в производительности. Но я еще не закончил.



С наследованием такого кода появляются проблемы:

  • Если в классе-потомке мы создадим метод onClick, он будет затерт в конструкторе предка.
  • Если мы все-таки как-то создадим метод, то все равно не сможем вызвать super.onClick, потому что на прототипе метода не существует.
  • Хоть как-то переопределить onClick в классе-потомке, опять же, можно только через стрелочную функцию.

Это еще не все минусы.


Источник

Так как в прототипе метод не существует, то писать тесты на него, использовать mock и spy невозможно. Надо вручную ловить создание конкретного экземпляра, и только на конкретном экземпляре можно будет как-то шпионить за этим методом.

Не используйте стрелочные функции для методов. Это единственный совет, который можно дать.

@boundMethod вместо bind


Хорошо, тогда разработчики говорят: у нас есть декораторы. В частности, такой интересный декоратор @boundMethod, который вместо нас магически привязывает контекст к нашему методу.

import {boundMethod} from 'autobind-decorator';class Component {    @boundMethod    method(): number {        return this.value;    }}

Выглядит красиво, но под капотом этот декоратор делает следующие вещи:

const boundFn = fn.bind(this);Object.defineProperty(this, key, {    get() {        return boundFn;    },    set(value) {        fn = value;        delete this[key];    }});

Он все равно вызывает bind. И в придачу определяет getter и setter с именем вашего метода. Можно сразу сказать, что getter и setter никогда не работали быстрее, чем обычное чтение и запись поля.

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

class Base extends Component {    @boundMethod    method() {}}class Child extends Base {    method = debounce(super.method, 100);}

Кроме того, очень легко выстрелить себе в ногу и организовать утечку памяти, всего лишь вызвав в классе-потомке debounce для нашего метода.



В DevTools это выглядит примерно так. Мы видим, что в памяти накапливаются старые экземпляры компонента Child. И если посмотреть в одном экземпляре, как у него выглядит этот метод, то мы увидим целую цепочку из bind-function-debounced-bind-function-debounced- и так далее. И в каждом из этих debounced в замыканиях содержатся предыдущие экземпляры Child. Вот вам утечка памяти на ровном месте, когда можно было ее избежать.


Ссылка со слайда

Задним числом хотелось бы сказать: перед тем, как вы решили использовать эту библиотеку в продакшене, хотелось бы посмотреть на то, как работает ее код. Одного знания, что ее код вместо одного вызова bind делает такие вещи, как getter и setter, было бы достаточно, чтобы не хотеть ее использовать.

Мы хотели бы посмотреть на коммиты: как часто они делаются, когда был последний коммит. Хотели бы посмотреть на тесты, насколько вменяемо они написаны. И проанализировать открытые баги насколько оперативно они исправляются. Этот баг с утечкой памяти, к сожалению, существует до сих пор. Ему уже два года, он скоро пойдет в детский садик, и до сих пор автор не торопится его исправлять.

Не используйте этот декоратор как минимум до тех пор, пока баг не будет исправлен.

TL;DR


Мой рассказ подходит к концу. Вот что я хотел бы еще раз для вас повторить:

  • Если вы заранее думаете над кодом и не выбираете заведомо неудачные варианты, которые работают плохо и медленно, это не преждевременная оптимизация. Это, наоборот, хорошо.
  • Когда вы делаете осознанный выбор, скорость или красота кода, это тоже хорошо, если ваш выбор осознан.
  • Очень плохо, если вы в принципе не умеете делать выбор, потому что не видите разных вариантов решения или не знаете, как писать производительный код, или не понимаете разницу в скорости работы вашего кода.

У меня все. Вот ссылка на документ со всеми упомянутыми материалами.
Подробнее..

Тривиальная и неправильная облачная компиляция

28.01.2021 00:21:03 | Автор: admin


Введение


Данная статья не история успеха, а скорее руководство как не надо делать. Весной 2020 для поддержания спортивного тонуса участвовал в студенческом хакатоне (спойлер: заняли 2-е место). Удивительно, но задача из полуфинала оказалась более интересной и сложной чем финальная. Как вы поняли, о ней и своём решении расскажу под катом.


Задача


Данный кейс был предложен Deutsche Bank в направлении WEB-разработка.
Необходимо было разработать онлайн-редактор для проекта Алгосимулятор тестового стенда для проверки работы алгоритмов электронной торговли на языке Java. Каждый алгоритм реализуется в виде наследника класса AbstractTradingAlgorythm.


AbstractTradingAlgorythm.java
public abstract class AbstractTradingAlgorithm {    abstract void handleTicker(Ticker ticker) throws Exception;    public void receiveTick(String tick) throws Exception {        handleTicker(Ticker.parse(tick));    }    static class Ticker {        String pair;        double price;       static Ticker parse(String tick) {           Ticker ticker = new Ticker();           String[] tickerSplit = tick.split(",");           ticker.pair = tickerSplit[0];           ticker.price = Double.valueOf(tickerSplit[1]);           return ticker;       }    }}

Сам же редактор во время работы говорит тебе три вещи:


  1. Наследуешь ли ты правильный класс
  2. Будут ли ошибки на этапе компиляции
  3. Успешен ли тестовый прогон алгоритма. В данном случае подразумевается, что "В результате вызова new <ClassName>().receiveTick(RUBHGD,100.1) отсутствуют runtime exceptions".


Ну окей, скелет веб-сервиса через spring накидать дело на 5-10 минут. Пункт 1 работа для регулярных выражений, поэтому даже думать об этом сейчас не буду. Для пункта 2 можно конечно написать синтаксический анализатор, но зачем, когда это уже сделали за меня. Может и пункт 3 получится сделать, использовав наработки по пункту 2. В общем, дело за малым, уместить в один метод, ну например, компиляцию исходного кода программы на Java, переданного в контроллер строкой.


Решение


Здесь и начинается самое интересное. Забегая вперёд, как сделали другие ребята: установили на машину джаву, отдавали команды на ось и грепали stdout. Конечно, это более универсальный метод, но во-первых, нам сказали слово Java, а во-вторых...



у каждого свой путь.


Естественно, Java окружение устанавливать и настраивать всё же придётся. Правда компилировать и исполнять код мы будем не в терминале, а, как бы это ни звучало, в коде. Начиная с 6 версии, в Java SE присутствует пакет javax.tools, добавленный в стандартный API для компиляции исходного кода Java.
Теперь привычные понятия такие, как файлы с исходным кодом, параметры компилятора, каталоги с выходными файлами, сообщения компилятора, превратились в абстракции, используемые при работе с интерфейсом JavaCompiler, через реализации которого ведётся основная работа с задачами компиляции. Подробней о нём можно прочитать в официальной документации. Главное, что оттуда сейчас перейдёт моментально в текст статьи, это класс JavaSourceFromString. Дело в том, что, по умолчанию, исходный код загружается из файловой системы. В нашем же случае исходный код будет приходить строкой извне.


JavaSourceFromString.java
import javax.tools.SimpleJavaFileObject;import java.net.URI;public class JavaSourceFromString extends SimpleJavaFileObject {    final String code;    public JavaSourceFromString(String name, String code) {        super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);        this.code = code;    }    @Override    public CharSequence getCharContent(boolean ignoreEncodingErrors) {        return code;    }}

Далее, в принципе уже ничего сложного нет. Получаем строку, имя класса и преобразуем их в объект JavaFileObject. Этот объект передаём в компилятор, компилируем и собираем вывод, который и возвращаем на клиент.
Сделаем класс Validator, в котором инкапсулируем процесс компиляции и тестового прогона некоторого исходника.


public class Validator {    private JavaSourceFromString sourceObject;    public Validator(String className, String source) {        sourceObject = new JavaSourceFromString(className, source);    }}

Далее добавим компиляцию.


public class Validator {    ...    public List<Diagnostic<? extends JavaFileObject>> compile() {        // получаем компилятор, установленный в системе        var compiler = ToolProvider.getSystemJavaCompiler();        // компилируем        var compilationUnits = Collections.singletonList(sourceObject);        var diagnostics = new DiagnosticCollector<JavaFileObject>();        compiler.getTask(null, null, diagnostics, null, null, compilationUnits).call();        // возворащаем диагностику        return diagnostics.getDiagnostics();    }}

Пользоваться этим можно как-то так.


public void MyMethod() {        var className = "TradeAlgo";        var sourceString = "public class TradeAlgo extends AbstractTradingAlgorithm{\n" +                "@Override\n" +                "    void handleTicker(Ticker ticker) throws Exception {\n" +                "       System.out.println(\"TradeAlgo::handleTicker\");\n" +                "    }\n" +                "}\n";        var validator = new Validator(className, sourceString);        for (var message : validator.compile()) {            System.out.println(message);        }    }

При этом, если компиляция прошла успешно, то возвращённый методом compile список будет пуст. Что интересно? А вот что.

На приведённом изображении вы можете видеть директорию проекта после завершения выполнения программы, во время выполнения которой была осуществлена компиляция. Красным прямоугольником обведены .class файлы, сгенерированные компилятором. Куда их девать, и как это чистить, не знаю жду в комментариях. Но что это значит? Что скомпилированные классы присоединяются в runtime, и там их можно использовать. А значит, следующий пункт задачи решается тривиально с помощью средств рефлексии.
Создадим вспомогательный POJO для хранения результата прогона.


TestResult.java
public class TestResult {    private boolean success;    private String comment;    public TestResult(boolean success, String comment) {        this.success = success;        this.comment = comment;    }    public boolean success() {        return success;    }    public String getComment() {        return comment;    }}

Теперь модифицируем класс Validator с учётом новых обстоятельств.


public class Validator {    ...    private String className;    private boolean compiled = false;    public Validator(String className, String source) {        this.className = className;        ...    }    ...    public TestResult testRun(String arg) {        var result = new TestResult(false, "Failed to compile");        if (compiled) {            try {                // загружаем класс                var classLoader = URLClassLoader.newInstance(new URL[]{new File("").toURI().toURL()});                var c = Class.forName(className, true, classLoader);                // создаём объект класса                var constructor = c.getConstructor();                var instance = constructor.newInstance();                // выполняем целевой метод                c.getDeclaredMethod("receiveTick", String.class).invoke(instance, arg);                result = new TestResult(true, "Success");            } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | ClassNotFoundException | RuntimeException | MalformedURLException | InstantiationException e) {                var sw = new StringWriter();                e.printStackTrace(new PrintWriter(sw));                result = new TestResult(false, sw.toString());            }        }        return result;    }}

Возвращаясь к предыдущему примеру использования, можно дописать туда такие строчки.


public void MyMethod() {        ...        var result = validator.testRun("RUBHGD,100.1");        System.out.println(result.success() + " " + result.getComment());    }

Вставить этот код в реализацию API контроллера задача нетрудная, поэтому подробности её решения можно опустить.


Какие проблемы?


  1. Ещё раз напомню про кучу .class файлов.


  2. Поскольку опять же идёт работа с компиляцией некоторых классов, есть риск отказа в записи любого непредвиденного .class файла.


  3. Самое главное, наличие уязвимости для инъекции вредосного кода на языке программирования Java. Ведь, на самом деле, пользователь может написать что угодно в теле вызываемого метода. Это может быть вызов полного перегруза системы, затирание всей файловой системы машины и тому подобное. В общем, исполняемую среду нужно изолировать, как минимум, а в рамках хакатона этим в команде никто естественно не занимался ввиду нехватки времени.



Поэтому делать в точности как я не надо)


P.S. Ссылка на гитхаб с исходным кодом из статьи.

Подробнее..

Перспективы разработчика в автоматизации тестирования ПО

15.02.2021 10:17:19 | Автор: admin

Как и многим, нам в Veeam нужны разработчики автотестов. Обычно на эту позицию претендуют специалисты с опытом в ручном тестировании ПО, но зачастую у них может не быть навыков программирования. А программисты, как правило, нечасто стремятся в разработку автотестов. Между тем у нас в компании есть автоматизаторы, которые изначально были не тестировщиками, а программистами, и они очень хорошо себя проявили в автоматизации. В статье я расскажу почему такой вариант развития карьеры может быть интересен для разработчиков.

Лирическое отступление

Это только присказка, сказка впереди

В начале 2000-x я работал в IT-компании, которая выполняла несколько аутсорсинговых проектов для компании Integrated Genomics. Проекты были связаны с расшифровкой геномов простейших организмов. К примеру, одна из утилит искала фрагменты (праймеры) с определенными свойствами в геноме кишечной палочки. На входе утилиты была последовательность ДНК, загружаемая из публичной базы геномов ERGO и состоящая из азотистых оснований. На выходе таблица фрагментов и их позиция в цепочке ДНК. Далее эти фрагменты использовались биологами для синтеза геномов. Задача была сравнительно простой. Нужно было лишь позаботиться о том, чтобы программа не выжирала всю оперативную память довольно слабых машин, которые были у нас на тот момент. Сложность других проектов заключалась в том, что они находились на стыке трех дисциплин: биологии, математики и информатики. В тех случаях, когда алгоритм задачи был понятен, его реализация в программном коде не представляла трудности. Но когда сама задача была неопределенной, и не находилось никого кто мог бы ее формализовать, это был серьезный вызов.

Выяснилось, что для того, чтобы успешно решать такие задачи, нужны фундаментальные знания по биологии и высшей математике. Нас, молодых и горячих, это не остановило. Мы нашли англоязычную книгу по биоинформатике профессора Павла Певзнера и приступили к изучению. Поначалу повествование было легким и непринужденным. Во вступлении Павел рассказывал о том, как выживал в Москве в студенческие годы, и это было поистине приятное и расслабляющее чтение. Далее в первых главах речь шла про азотистые основания нуклеотидов ДНК аденин, гуанин, тимин и цитозин и про комплиментарность нуклеиновых кислот, а именно: как основания нуклеотидов способны формировать парные комплексы аденинтимин и гуанинцитозин при взаимодействии цепей нуклеиновых кислот. Было понятно, что такое свойство нуклеотидов играет ключевую роль в репликации ДНК. Я помню, что испытывал подъем и состояние потока, читая это объяснение, и мысленно представлял, как мы сейчас быстренько все это освоим (подумаешь, биология) и сможем брать более серьезные задачи. Мы даже думали о том, что сможем написать свой геномный ассемблер и взяться за расшифровку простейших геномов. Продолжаю читать книгу, и тут бах система уравнений на полстраницы без каких-либо объяснений. Из контекста подразумевалось, что эта система проще пареной репы, и любое объяснение будет оскорблением для читателя. Не было даже сноски для факультативного чтения. Я решил эту страницу пропустить, вернуться к ней позже и пока что продолжить читать дальше вдруг станет понятно. Перелистываю страницу а там еще одна система уравнений уже на всю страницу и скупое описание, часть слов из которого мы не нашли в словаре. Стало понятно, что эту область знаний на стыке геномики и математики нахрапом не возьмешь. Также стало понятно, почему подавляющее большинство коллег, с которыми мы взаимодействовали, имели биологическое и/или математическое образование. В конечном итоге, шаг за шагом погружаясь в основы геномики, нам удалось создать несколько программных продуктов, и тогда я в первый раз всерьез задумался о том насколько результативной и интересной может быть деятельность на стыке нескольких дисциплин.

С тех пор еще не раз мне довелось сменить сферу деятельности, и в каждом случае навыки разработчика служили трамплином для решения новых задач. Так произошло и с автоматизацией тестирования ПО.

Перспективы разработчика в автоматизации тестирования ПО

Перспективы разработчика в этой области трудно переоценить. Как и биоинформатика, разработка автотестов находится на стыке двух дисциплин, и навыки программиста здесь все чаще выступают на первый план.

Исторически сложилось так, что в автоматизацию приходят специалисты с опытом в ручном тестировании ПО. В какой-то момент они осознают, что рутинные операции можно и нужно автоматизировать, либо просто желают попробовать себя в новом качестве. Опыт, привносимый ручными тестировщиками в проект автоматизации, бывает архиполезным. Никто лучше них не знает возможности и слабые места продукта, наиболее трудоемкие сценарии для тестирования, окружение, в котором работает продукт, а также пожелания пользователей и планы на следующие релизы. Как правило, хороший ручной тестировщик четко понимает, что именно он хочет автоматизировать, но с написанием автотестов порой возникают сложности. Почему? Потому что автотесты это такой же программный продукт, как и продукт, который они призваны тестировать. Нужно продумать архитектуру системы автотестов, механизмы их запуска (Continuous Integration, CI) и самое главное нужно писать хороший код. По факту, для ручного тестировщика это зачастую оказывается непросто. Ведь здесь требуется создать полноценный проект, который можно интегрировать в CI, изменять, расширять и переиспользовать. Для этого нужны способности к программированию, накопленный опыт и набитые шишки.

Ручной тестировщик с глубоким пониманием продукта и методик тестирования и разработчик с опытом программирования отлично дополняют друг друга. Первый силен в постановке задачи (use case -> test case), второй в ее реализации. Встает вопрос: почему среди автоматизаторов встречается много ручных тестировщиков? На это есть несколько причин:

Путь из ручных тестировщиков в автоматизаторы был более понятен, а требования по части написания кода могли быть более мягкими.

Для разработчиков переход в автоматизаторы в некотором смысле разрыв шаблона: Вот я разрабатывал продукт, а теперь буду тестировщиком, который пишет автотесты какой же я теперь разработчик? Иными словами, есть опасение потерять квалификацию, превратиться в ручного тестировщика, а также нежелание глубоко погружаться в теорию и практику тестирования.

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

Опасение потерять в карьерном росте и зарплате.

Разберем эти моменты.

Автотесты это такой же продукт разработки, как и тот продукт, для которого эти автотесты создаются. Если разработчик идет на позицию автоматизатора в выделенную группу разработки автотестов, он остается на стезе написания программного кода. Да, ему надо иметь представление о тестировании ПО, но базовые знания можно сравнительно быстро получить, пролистав какое-либо руководство по тестированию ПО (например, вот эту книгу). Безусловно, автоматизатору нужно понимать продукт, для которого он пишет автотесты. Но освоить продукт на приемлемом уровне зачастую оказывается легче, чем научиться писать хороший код. Знания о продукте и методиках тестирования будут расширяться по мере ознакомления с багами, написанными на продукт, и общения с ручными тестировщиками. Разработчик не перестанет быть разработчиком, не превратится в ручного тестировщика. Это не его путь. Путь разработчика писать хорошие автотесты.

Задачи у автоматизатора интересные и зачастую сложные. В первую очередь стоит вспомнить про знаменитую пирамиду Фаулера. Модульные, интеграционные, end-to-end тесты подразумевают вдумчивый подход к структуре тестов и выбору инструментов в соответствии с функциональностью продуктов, для которых пишем автотесты. Если говорить о продуктах, разрабатываемых в Veeam, то автоматизатору понадобится работать с REST, WebDriver, Microsoft SQL Server, Amazon Web Services, Microsoft Azure, VMware vCenter, Hyper-V список не исчерпан. У каждого из облаков и гипервизоров свой API и свои скелеты в шкафу. Порой приходится писать код на различных языках программирования, использовать заглушки, семафоры, создавать свои обертки и т.п.

Одну и ту же задачу можно решить по-разному, и автоматизатор ищет наиболее эффективное решение. Вот лишь один из примеров сценарий, реализованный для продукта Veeam ONE. Один из компонентов продукта Business View, который позволяет группировать элементы виртуальной инфраструктуры по различным критериям. Критериев и вариантов их комбинирования очень много, поэтому проверка этой функциональности вручную занимает много времени. Написание автотестов в лоб с имитацией действий ручных тестировщиков было бы неэффективным: тесты для графического интерфейса десктопных приложений, как правило, сложны и трудоемки в разработке, являются хрупкими, их тяжело модифицировать, и выполняются они долго. Мы нашли другое решение: поскольку действия пользователя в UI интерполируются в SQL-запросы к базе данных, мы используем SQL-запросы для создания категорий и групп. Это позволило нам в разумные сроки покрыть автотестами все свойства и операторы, задействованные в Business View.

Как сделать так, чтобы тесты, запускаемые параллельно ради уменьшения общего времени запуска, не мешали друг другу? Как измерить покрытие продукта автотестами? Анализ покрытия кода? Анализ покрытия пользовательских сценариев? Как отслеживать падения сервисов? Как интегрировать автотесты с репортинговыми фреймворками? Как интегрировать автотесты со сборкой билдов в CI? Как прогонять тесты по pull-реквестам? И многое-многое другое.

Нужно обратить внимание на качество самих автотестов. Кто сторожит сторожей? Какому автотесту можно доверять? Автотест должен быть эффективным по критерию количество затраченных на него усилий / полученный результат, автономным, стабильным (нехрупким), быстрым, надежным (никаких false positive и false negative). В автотесте должен быть понятный, хороший код с точки зрения возможностей языка программирования, чтобы этот код можно было легко расширять и изменять.

Начинающий автоматизатор приступает к написанию простых тестов в соответствии с согласованными планами/подходами/инструментами. Решает самые простые задачи, учится справляться с трудностями, находить решения, обрастает знаниями и навыками. Задачи становятся все более серьезными, кредит доверия автоматизатору растет. Если автоматизатор не останавливается в своем росте, то спустя какое-то время он дорастает до разработки комплексных технических решений и более глубокого участия в обсуждении стратегии и приоритетов в разработке автотестов. В один прекрасный день он принимает под свое крыло начинающих автоматизаторов, которые хотят приносить пользу и расти.

Что в Veeam?

В компании Veeam мы будем рады новым боевым товарищам с опытом программирования на C# (например, web-интерфейсы, десктопные приложения, консольные утилиты и т.п.). У нас в Veeam есть много продуктов. Технологии в них могут различаться. В автотестах для нескольких продуктов мы опираемся на REST и WebDriver. Если у вас нет опыта с этими технологиями, но вы уверенно себя чувствуете в написании кода на C# и питаете интерес к автоматизации тестирования, то, возможно, мы также найдем точки соприкосновения.

Мы будем рады вашему резюме и паре абзацев о том, что вас привлекает в автоматизации, о ваших сильных сторонах и профессиональных планах. Пишите нам на ящик qa@veeam.com внимательно прочитаем. Если укажете в теме письма [Хабр] (например, [Хабр] Позиция автоматизатора), будет плюс в карму =)

Да пребудет с Вами Сила.

Подробнее..

Взаимодействия. RPC vs REST vs MQ

15.02.2021 14:04:42 | Автор: admin

По работе мне довелось провести ряд собеседований на позицию Backend-разработчика. Особо важным для оценки архитектурных навыков мне кажется следующий вопрос:


Если вам необходимо спроектировать взаимодействие двух систем, в каких случаях вы выберете RPC, в каких REST, а в каких MQ?


Если ответ на этот вопрос у вас вызывает затруднения, загляните в мой небольшой ЛикБез на тему.




Для начала, нужно определиться концептуально, какие вообще бывают взаимодействия (в самом общем виде).


Во-первых, вызовы могут быть синхронные и асинхронные. Сам по себе вопрос об этом выборе интересен. И я думаю, что очень многие всегда будут отдавать предпочтение асинхронному async/await без конкретной причины тоже хороший вопрос для интервью.


Во-вторых, модель взаимодействий может быть однонаправленная (one-way) и вызовы вида запрос-ответ. Если вы исповедуете CQS, соблюдаете требование идемпотентности, то скорее всего вызовы будут однонаправленными.


В-третьих, приложения, которые взаимодействуют между собой, могут иметь разную архитектуру, а именно строение доменной логики.
complexity domain logic


Выбор


Если предполагается асинхронное взаимодействие, а ответ не требуется - это идеальный случай использовать брокер сообщений. Положительной стороной такого решения становиться устойчивость к сбоям и повышенным нагрузкам.


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


Сценарий транзакции


Организует бизнес-логику в процедуры, которые управляют каждая своим запросом.
TS


Данная форма доменной логики типичный сценарий хранимой процедуры, который за одну транзакцию выполняет ряд действий над данными. Иногда такую же форму может иметь микросервис декомпозированный по бизнес-возможности или глаголу. API подобных процедур нередко может представлять собой набор параметров, часть из которых может передаваться пустыми.


Исходя из определения, данного Мартином Фаулером, вызывать необходимо определённый сценарий и в определённой последовательности. RPC подход появился именно отсюда. Т.е. подойдут такие протоколы как: Sockets, WebSockets, gRPC, SOAP и другие.


Обработчик таблицы


Одна сущность обрабатывает всю бизнес-логику для всех строк таблицы БД или представления.
TM


Для данной формы организации доменной логики характерна работа над отдельными таблицами, с помощью репозиториев, реализующих CRUD-операции. Сервис строится с использованием API-Controller адаптера к репозиторию реализующий удалённый вызов CRUD-процедур с использование протокола HTTP. Таким образом, если ваше приложение базируется на БД с отдельными репозиториями, вам наиболее подходит REST протокол. В ряде случаев, особенно полезным становится использование протокола OData, расширяющего REST.


Модель предметной области


Объектная модель домена, объединяющая данные и поведение.
DDD


Это такая форма организации доменной логики, которая взаимодействуют с кластером доменных сущностей агрегатом. Данное взаимодействие для соблюдения инвариантов агрегата должно происходить через методы агрегата. Содержание (устройство агрегата) делает доменную модель значительно более похожей на сценарий транзакции чем на модуль таблицы. Ниже пример агрегата из книги Эванса.


Агрегат


Шаблон доменная модели, как можно видеть, очень похож на сценарий транзакции, но (1) имеет очерченные границы (Bounded Context), и (2) связан с доменной сущностью (агрегатом). Структура данных при этом сокрыта за абстракцией и может быть реляционном виде, а ещё проще когда в нереляционном.


Если сервис делается по принципам предметно-ориентированного проектирования, то есть несколько сложностей. В работе со своей командой, я столкнулся с тем, что для соблюдения инвариантов сущности, получения больших возможностей масштабирования, эффективнее всего будет создавать версии агрегата. При этом, если вы корректно оперируете агрегатом через его методы, классический REST уже не подходит. В частности, для работы с версиями модели, необходимо соблюдать требование идемпотентности одна бизнес-операция может изменить соответственно только последнюю версию агрегата. Корректно в данном случае использовать только глагол POST, с разными dto разных операций, или конкретным указанием через маршрутизацию контроллера. Согласитесь это не REST.


Совершенно по-новому в этом отношении смотрится gRPC замена Windows Comunication Foundation. С последним очень часто бывают существенные проблемы соразвития, особенно если интеграция происходит с командой, интеграция с которой оставляет желать лучшего (т.е. худшие варианты карты контекстов тут не подходят). В рамках же смыслового ядра считаю технологию оправданной. А сам RPC подход был бы наиболее верным.


Отдельные возможности открываются для брокеров сообщений как средство получения ответа от сервиса, ведь доменная модель идеально подходит для получения очень чистых событий предметной области, потребителями которых могут быть любые другие сервисы, а сама ШИНА ДОМЕННХ СОБТИЙ может стать превосходным средством масштабирования. Организуя архитектуру определяемую событиями, важно не забывать про возможность циклического вызова, про маркеры корреляции.




Вполне ясно что можно скрестить ужа с ежом, добавить много особых кейсов. Но в самых типовых случаях, правильно обратиться к классификации бизнес-логики Фаулера, а потом уже к технологиям и протоколам. И всё же, если считаете что есть кейс который нужно сюда внести, напишите об этом в комментарии. Думайте про архитектуру!


Просто подумайте




Что почитать


Подробнее..

Перевод Использование Google Protocol Buffers (protobuf) в Java

25.02.2021 20:10:38 | Автор: admin

Привет, хабровчане. В рамках курса "Java Developer. Professional" подготовили для вас перевод полезного материала.

Также приглашаем посетить открытый вебинар на тему gRPC для микросервисов или не REST-ом единым.


Недавно вышло третье издание книги "Effective Java" (Java: эффективное программирование), и мне было интересно, что появилось нового в этой классической книге по Java, так как предыдущее издание охватывало только Java 6. Очевидно, что появились совершенно новые разделы, связанные с Java 7, Java 8 и Java 9, такие как глава 7 "Lambdas and Streams" (Лямбда-выражения и потоки), раздел 9 "Prefer try-with-resources to try-finally" (в русском издании 2.9. Предпочитайте try-с-ресурсами использованию try-finally) и раздел 55 "Return optionals judiciously" (в русском издании 8.7. Возвращайте Optional с осторожностью). Но я был слегка удивлен, когда обнаружил новый раздел, не связанный с нововведениями в Java, а обусловленный изменениями в мире разработки программного обеспечения. Именно этот раздел 85 "Prefer alternatives to Java Serialization" (в русском издании 12.1 Предпочитайте альтернативы сериализации Java) и побудил меня написать данную статью об использовании Google Protocol Buffers в Java.

В разделе 85 "Prefer alternatives to Java Serialization" (12.1 Предпочитайте альтернативы сериализации Java) Джошуа Блох (Josh Bloch) выделяет жирным шрифтом следующие два утверждения, связанные с сериализацией в Java:

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

Нет никаких оснований для использования сериализации Java в любой новой системе, которую вы пишете.

После описания в общих чертах проблем с десериализацией в Java и, сделав эти смелые заявления, Блох рекомендует использовать то, что он называет кроссплатформенным представлением структурированных данных (чтобы избежать путаницы, связанной с термином сериализация при обсуждении Java). Блох говорит, что основными решениями здесь являются JSON (JavaScript Object Notation) и Protocol Buffers (protobuf). Мне показалось интересным упоминание о Protocol Buffers, так как в последнее время я немного читал о них и игрался с ними. В интернете есть довольно много материалов по использованию JSON (даже в Java), в то время как осведомленность о Protocol Buffers среди java-разработчиков гораздо меньше. Поэтому я думаю, что статья об использовании Protocol Buffers в Java будет полезной.

На странице проекта Google Protocol Buffers описывается как не зависящий от языка и платформы расширяемый механизм для сериализации структурированных данных. Также там есть пояснение: Как XML, но меньше, быстрее и проще. И хотя одним из преимуществ Protocol Buffers является поддержка различных языков программирования, в этой статье речь пойдет исключительно про использование Protocol Buffers в Java.

Есть несколько полезных онлайн-ресурсов, связанных с Protocol Buffers, включая главную страницу проекта, страницу проекта protobuf на GitHub, proto3 Language Guide (также доступен proto2 Language Guide), туториал Protocol Buffer Basics: Java, руководство Java Generated Code Guide, API-документация Java API (Javadoc) Documentation, страница релизов Protocol Buffers и страница Maven-репозитория. Примеры в этой статье основаны на Protocol Buffers 3.5.1.

Использование Protocol Buffers в Java описано в туториале "Protocol Buffer Basics: Java". В нем рассматривается гораздо больше возможностей и вещей, которые необходимо учитывать в Java, по сравнению с тем, что я расскажу здесь. Первым шагом является определение формата Protocol Buffers, не зависящего от языка программирования. Он описывается в текстовом файле с расширением .proto. Для примера опишем формат протокола в файле album.proto, который показан в следующем листинге кода.

album.proto

syntax = "proto3";option java_outer_classname = "AlbumProtos";option java_package = "dustin.examples.protobuf";message Album {    string title = 1;    repeated string artist = 2;    int32 release_year = 3;    repeated string song_title = 4;}

Несмотря на простоту приведенного выше определения формата протокола, в нем присутствует довольно много информации. В первой строке явно указано, что используется proto3 вместо proto2, используемого по умолчанию, если явно ничего не указано. Две строки, начинающиеся с option, указывают параметры генерации Java-кода (имя генерируемого класса и пакет этого класса) и они нужны только при использовании Java.

Ключевое слово "message" определяет структуру "Album", которую мы хотим представить. В ней есть четыре поля, три из которых строки (string), а одно целое число (int32). Два из них могут присутствовать в сообщении более одного раза, так как для них указано зарезервированное слово repeated. Обратите внимание, что формат сообщения определяется независимо от Java за исключением двух option, которые определяют детали генерации Java-классов по данной спецификации.

Файл album.proto, приведенный выше, теперь необходимо скомпилировать в файл исходного класса Java (AlbumProtos.java в пакете dustin.examples.protobuf), который можно использовать для записи и чтения бинарного формата Protocol Buffers. Генерация файла исходного кода Java выполняется с помощью компилятора protoc, соответствующего вашей операционной системе. Я запускаю этот пример в Windows 10, поэтому я скачал и распаковал файл protoc-3.5.1-win32.zip. На изображении ниже показан мой запущенный protoc для album.proto с помощью команды protoc --proto_path=src --java_out=dist\generated album.proto

Перед запуском вышеуказанной команды я поместил файл album.proto в каталог src, на который указывает --proto_path, и создал пустой каталог build\generated для размещения сгенерированного исходного кода Java, что указано в параметре --java_out.

Сгенерированный Java-класс AlbumProtos.java содержит более 1000 строк, и я не буду приводить его здесь, он доступен на GitHub. Среди нескольких интересных моментов относительно сгенерированного кода я хотел бы отметить отсутствие выражений import (вместо них используются полные имена классов с пакетами). Более подробная информация об исходном коде Java, сгенерированном protoc, доступна в руководстве Java Generated Code. Важно отметить, что данный сгенерированный класс AlbumProtos пока никак не связан с моим Java-приложением, и сгенерирован исключительно из текстового файла album.proto, приведенного ранее.

Теперь исходный Java-код AlbumProtos надо добавить в вашем IDE в перечень исходного кода проекта. Или его можно использовать как библиотеку, скомпилировав в .class или .jar.

Прежде чем двигаться дальше, нам понадобится простой Java-класс для демонстрации Protocol Buffers. Для этого я буду использовать класс Album, который приведен ниже (код на GitHub).

Album.java

package dustin.examples.protobuf;import java.util.ArrayList;import java.util.List;/** * Music album. */public class Album {    private final String title;    private final List < String > artists;    private final int releaseYear;    private final List < String > songsTitles;    private Album(final String newTitle, final List < String > newArtists,        final int newYear, final List < String > newSongsTitles) {        title = newTitle;        artists = newArtists;        releaseYear = newYear;        songsTitles = newSongsTitles;    }    public String getTitle() {        return title;    }    public List < String > getArtists() {        return artists;    }    public int getReleaseYear() {        return releaseYear;    }    public List < String > getSongsTitles() {        return songsTitles;    }    @Override    public String toString() {        return "'" + title + "' (" + releaseYear + ") by " + artists + " features songs " + songsTitles;    }    /**     * Builder class for instantiating an instance of     * enclosing Album class.     */    public static class Builder {        private String title;        private ArrayList < String > artists = new ArrayList < > ();        private int releaseYear;        private ArrayList < String > songsTitles = new ArrayList < > ();        public Builder(final String newTitle, final int newReleaseYear) {            title = newTitle;            releaseYear = newReleaseYear;        }        public Builder songTitle(final String newSongTitle) {            songsTitles.add(newSongTitle);            return this;        }        public Builder songsTitles(final List < String > newSongsTitles) {            songsTitles.addAll(newSongsTitles);            return this;        }        public Builder artist(final String newArtist) {            artists.add(newArtist);            return this;        }        public Builder artists(final List < String > newArtists) {            artists.addAll(newArtists);            return this;        }        public Album build() {            return new Album(title, artists, releaseYear, songsTitles);        }    }}

Теперь у нас есть data-класс Album, Protocol Buffers-класс, представляющий этот Album (AlbumProtos.java) и мы готовы написать Java-приложение для "сериализации" информации об Album без использования Java-сериализации. Код приложения находится в классе AlbumDemo, полный код которого доступен на GitHub.

Создадим экземпляр Album с помощью следующего кода:

/** * Generates instance of Album to be used in demonstration. * * @return Instance of Album to be used in demonstration. */public Album generateAlbum(){   return new Album.Builder("Songs from the Big Chair", 1985)      .artist("Tears For Fears")      .songTitle("Shout")      .songTitle("The Working Hour")      .songTitle("Everybody Wants to Rule the World")      .songTitle("Mothers Talk")      .songTitle("I Believe")      .songTitle("Broken")      .songTitle("Head Over Heels")      .songTitle("Listen")      .build();}

Класс AlbumProtos, сгенерированныйProtocol Buffers, включает в себя вложенный класс AlbumProtos.Album, который используется для бинарной сериализации Album. Следующий листинг демонстрирует, как это делается.

final Album album = instance.generateAlbum();final AlbumProtos.Album albumMessage    = AlbumProtos.Album.newBuilder()        .setTitle(album.getTitle())        .addAllArtist(album.getArtists())        .setReleaseYear(album.getReleaseYear())        .addAllSongTitle(album.getSongsTitles())        .build();

Как видно из предыдущего примера, для заполнения иммутабельного экземпляра класса, сгенерированного Protocol Buffers, используется паттерн Строитель (Builder). Через ссылку экземпляр этого класса теперь можно легко преобразовать объект в бинарный вид Protocol Buffers, используя метод toByteArray(), как показано в следующем листинге:

final byte[] binaryAlbum = albumMessage.toByteArray();

Чтение массива byte[] обратно в экземпляр Album может быть выполнено следующим образом:

/** * Generates an instance of Album based on the provided * bytes array. * * @param binaryAlbum Bytes array that should represent an *    AlbumProtos.Album based on Google Protocol Buffers *    binary format. * @return Instance of Album based on the provided binary form *    of an Album; may be {@code null} if an error is encountered *    while trying to process the provided binary data. */public Album instantiateAlbumFromBinary(final byte[] binaryAlbum) {    Album album = null;    try {        final AlbumProtos.Album copiedAlbumProtos = AlbumProtos.Album.parseFrom(binaryAlbum);        final List <String> copiedArtists = copiedAlbumProtos.getArtistList();        final List <String> copiedSongsTitles = copiedAlbumProtos.getSongTitleList();        album = new Album.Builder(                copiedAlbumProtos.getTitle(), copiedAlbumProtos.getReleaseYear())            .artists(copiedArtists)            .songsTitles(copiedSongsTitles)            .build();    } catch (InvalidProtocolBufferException ipbe) {        out.println("ERROR: Unable to instantiate AlbumProtos.Album instance from provided binary data - " +            ipbe);    }    return album;}

Как вы заметили, при вызове статического метода parseFrom(byte []) может быть брошено проверяемое исключение InvalidProtocolBufferException. Для получения десериализованного экземпляра сгенерированного класса, по сути, нужна только одна строка, а остальной код это создание исходного класса Album из полученных данных.

Демонстрационный класс включает в себя две строки, которые выводят содержимое исходного экземпляра Album и экземпляра, полученного из бинарного представления. В них есть вызов метода System.identityHashCode() на обоих экземплярах, чтобы показать, что это разные объекты даже при совпадении их содержимого. Если этот код выполнить с примером Album, приведенным выше, то результат будет следующим:

BEFORE Album (1323165413): 'Songs from the Big Chair' (1985) by [Tears For Fears] features songs [Shout, The Working Hour, Everybody Wants to Rule the World, Mothers Talk, I Believe, Broken, Head Over Heels, Listen]AFTER Album (1880587981): 'Songs from the Big Chair' (1985) by [Tears For Fears] features songs [Shout, The Working Hour, Everybody Wants to Rule the World, Mothers Talk, I Believe, Broken, Head Over Heels, Listen]

Здесь мы видим, что в обоих экземплярах соответствующие поля одинаковы и эти два экземпляра действительно разные. При использовании Protocol Buffers, действительно, нужно сделать немного больше работы, чем припочти автоматическом механизме сериализации Java, когда надо просто наследоваться от интерфейса Serializable, но есть важные преимущества, которые оправдывают затраты. В третьем издании книги Effective Java (Java: эффективное программирование) Джошуа Блох обсуждает уязвимости безопасности, связанные со стандартной десериализацией в Java, и утверждает, что Нет никаких оснований для использования сериализации Java в любой новой системе, которую вы пишете.


Узнать подробнее о курсе "Java Developer. Professional".

Смотреть открытый вебинар на тему gRPC для микросервисов или не REST-ом единым.

Подробнее..

Из песочницы Что такое REST API

14.08.2020 18:11:43 | Автор: admin
Если говорить отдаленно, то REST API нужен для создания сайта. Но ведь для этого много чего нужно (скажете Вы) в какой же части REST API?

Frontend и Backend


Начнем по порядку. Что такое frontend и backend? У сайта есть две стороны: лицевая и внутренняя соответственно. Первая обычно отвечает за визуальное расположение объектов на странице (где какие картинки, где какой текст и где какие кнопочки). Вторая отвечает за действия. Обычно это нажатия на те самые кнопочки или на другие штуки на сайте. Например, Вы заходите на страницу вашей любимой социальной сети и для начала Вам нужно войти в аккаунт. С лицевой стороны (frontend) Вы вводите логин и пароль и нажимаете кнопку Войти. В это время запрос отправляется в базу данных для проверки наличия такого пользователя, и в случае успеха, Вы попадаете в социальную сеть под своим аккаунтом, а в противном случае, Вы увидите сообщение об ошибке. В данном случае, за отправку запроса в базу данных по сути и отвечает backend сторона. Обычно ее разделяют на три подчасти:

  1. Web API для приема запросов
  2. Бизнес-логика для обработки запросов
  3. Взаимодействие с базой данных

В этой статье мы поговорим в основном про API или Application Programming Interface и немного про бизнес-логику. Но для начала создадим сервер.

Создание собственного сервера


Так выглядит простейшее серверное приложение на python с использованием фреймворка flask:

from flask import Flaskapp = Flask(__name__)@app.route("/")def index():   return "Hello, World!"app.run()

Здесь уже есть один пустой роут (/) и если запустить это приложение и открыть браузер на странице 127.0.0.1:5000, то Вы увидете надпись Hello, World!. Со стороны сервера, Вы увидите такое сообщение:

127.0.0.1 - [07/Aug/2020 20:32:16] GET / HTTP/1.1 200 Таким образом, переходя в браузере (клиенте) по данной ссылке, мы делаем GET-запрос на наш сервер и попадаем в функцию index отсюда и берется Hello, World!. Можно добавлять и другие запросы (гораздо более сложные) по другим роутам (или необязательно). Как я сказал, в данном случае у нас использовался GET-запрос стандартный по умолчанию. Но существует и множество других, самые популярные из которых POST, PUT, DELETE. Но зачем это нужно?

Create Read Update Delete


Во-первых, REST расшифровывается как REpresentational State Transfer (или, по-простому, РЕпрезентативная передача состояния). По факту само определение REST не так важно, но его обычно связывают с другой аббревиатурой CRUD Create Read Update Delete. В самом начале я приводил пример, связанный с базой данных и эти четыре операции неотъемлемая часть работы с ней (ну или просто с данными).

Во-вторых, REST или RESTfull API должны поддерживать обработку этих четырех действий. Здесь нам как раз и пригодятся методы GET, POST, PUT, DELETE. Как правило (не обязательно!) метод POST используется для добавления новых данных (Create), GET для чтения (Read), PUT для обновления существующих данных (Update) и DELETE соответственно для удаления (Delete). Например, то же самое приложение на flask можно переделать так:

from flask import Flask, requestapp = Flask(__name__)@app.route("/", methods=["POST", "GET", "PUT", "DELETE"])def index():   if request.method == "POST":       # добавить новые данные   if request.method == "GET":       # отдать данные   if request.method == "PUT":       # обновить данные   if request.method == "DELETE":       # удалить данныеapp.run()

Это и есть примитивный REST API. Frontend сторона теперь может посылать запросы и, в зависимости от их типа, мы будем производить дальнейшие действия.

Работа с данными


Наше текущее приложение совсем неинтересное хорошо бы поработать с какими-нибудь данными. Для этого надо подумать, как их передавать. Самый популярный способ JSON-формат (но можно использовать и другие, например, XML). Он представляет из себя аналог словаря в python и очень удобен в использовании. Я буду использовать примитивные данные для примера с авторизацией в социальной сети:

data = {   1: {       "login": "login1",       "password": "Qwerty1"},   2: {       "login": "login2",       "password": "Ytrewq2"}   }

У нас есть data, в которой пока что два пользователя (login1 и login2) и мы будем эту дату CRUDить. Стоит сказать, что все четыре метода редко когда работают на одном и том же роуте и обычно делают так: для методов GET (выдать всех пользователей) и POST используется роут, например, /users, а для методов GET (выдать одного пользователя по его id), PUT и DELETE /users/id. Также необходимо заметить, что для обновления и создания новых пользователей к нам приходят данные о них в теле запроса (request.json). Теперь нашу программу можно переписать следующим образом:

from flask import Flask, requestapp = Flask(__name__)data = {   1: {       "login": "login1",       "password": "Qwerty1"},   2: {       "login": "login2",       "password": "Ytrewq2"}   }@app.route("/users", methods=["POST", "GET"])def work_with_users():   if request.method == "POST":       data[max(data.keys())+1] = request.json       return {"message": "User was created"}, 201   if request.method == "GET":       return data, 200@app.route("/users/<int:user_id>", methods=["GET", "PUT", "DELETE"])def work_with_user_by_id(user_id):   if request.method == "GET":       return data[user_id], 200   if request.method == "PUT":       data[user_id]["login"] = request.json["login"]       data[user_id]["password"] = request.json["password"]       return {"message": "User was updated"}, 200   if request.method == "DELETE":       data.pop(user_id)       return {"message": "User was deleted"}, 200app.run()

Для тестирования запросов существует множество программ (Postman, Fiddler, Insomnia...) и я рекомендую ознакомиться с одной из них (лично мне больше всего нравится Postman). С их помощью можно посмотреть, что приходит в результате запроса и с каким статус-кодом (числа 200/201 в returnах). А также можно инсценировать отправку данных, добавляя их в тело запроса.

Стоит также заметить, что в настоящее время такой подход не используется, а обычно применяют библиотеку flask-restplus (или пришедшую ей на смену flask-restx), но я считаю, что для начала нужно познакомиться с чистым flask. Также необходимо проверять наличие данных и их корректность и предусмотреть возврат ошибки в противных случаях.

Заключение


REST API это просто методы CRUD, к которым обращается клиентская сторона сайта по определенным роутам. На слух и взгляд, возможно, это воспринимается трудно, так что я рекомендую написать собственный сервер по аналогии с примером. Лично я считаю flask одним из самых простых фреймворков для этого, и, если Вы новичок, то я советую попробовать именно его.
Подробнее..
Категории: Python , Api , Rest , Rest api , Flask , Crud restful api

Black Olives Matter раса, криминал и огонь на поражение в США. Часть 1

04.09.2020 04:18:51 | Автор: admin
Дисклеймер

Зная, насколько эта публикация может оказаться воспринятой как "политическая" и насколько разнятся мнения людей по определенным злободневным вопросам, сразу внесу следующие оговорки:

  • Автор публикации не является расистом, не считает, что представителей одних рас должны обладать какими-либо привилегиями или предпочтениями по сравнению с представителями других рас. Для меня все люди - братья!

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

  • Цель публикации - статистический анализ данных из открытых источников и выявление взаимосвязей и закономерностей; широкие выводы предоставляется сделать читателям.

  • Все данные, использованные в статье, взяты их открытых источников, прямо указанных в самом тексте. Каждый из вас может их верифицировать. При этом автор не несет ответственность за валидность данных в самих источниках, принимая их "как есть" и не изменяя никакие исходные данные. Поэтому сомнения в валидности настоящего исследования должны относиться к исходным данным, на которые автор не может повлиять.

  • Я не считаю себя профессиональным Data Scientist и использую самые базовые инструменты анализа данных (при этом, наверное, не всегда наиболее оптимальным способом). Буду благодарен каждому за подсказки, как можно сделать то или иное более эффективно или углубить исследование!

Во времена Советского Союза нашим с вами, уважаемые читатели, папам и мамам, дедушкам и бабушкам неустанно и отовсюду напоминали о том, как "империалисты" притесняли и угнетали представителей иных рас, как уже после отмены крепостного права в Российской Империи американские капиталисты продолжали использовать рабский труд африканцев и их потомков, как и в нынешнем (на то время) двадцатом веке издевательства не прекращаются даже после формального упразднения рабства, выражаясь в самых возмутительных формах апартеида, унижений, расизма и ненависти... Классические романы вроде "Хижины дяди Тома" Гарриет Бичер-Стоу и "Убить пересмешника" Харпер Ли еще сильнее упрочняли негодование борцов за свободу по всему миру. Да, расизм со стороны белых процветал в США до 1960-х - 1970-х. Но и, конечно, эти притеснения были отличным подспорьем для социалистической пропаганды, не щадящей красок в живописании "зверств акул капитализма". С середины 1950-х в США началось сильное движение за борьбу с расовым неравенством, которое было в итоге поддержано властями и кардинально изменило ситуацию с социальными свободами к 1980-м. Обо всем этом можно прочитать хотя бы в Википедии. А что теперь?..

Иллюстрация к роману Г. Бичер-Стоу "Хижина дяди Тома". "Классическое" изображение рабского труда африканцев.Иллюстрация к роману Г. Бичер-Стоу "Хижина дяди Тома". "Классическое" изображение рабского труда африканцев.

Почти все то же, что наши родичи читали со страниц "Правды" в 1960-х, сейчас мы слышим со всех американских СМИ. Расовая несправедливость! Насилие со стороны полиции и иных слуг закона! Как мы все видели, после гибели Джорджа Флойда в США начались массовые протесты, перешедшие местами в беспорядки и погромы под лозунгом Black Lives Matter. Итог общественного мнения в США на сегодняшний день: полиция убивает чернокожих по причине массового расизма со стороны белых.

Цели исследования

Как и многим из вас (я уверен), мне часто хочется самостоятельно разобраться в каком-то вопросе, особенно если:

  • вопрос широко обсуждается и составляет предмет споров

  • освещение почти во всех СМИ носит явно окрашенный характер (т.е. налицо пропаганда той или иной позиции)

  • есть достаточное количество исходных данных, доступных для изучения

Интересно заметить, что эти три пункта связаны между собой: 1) злободневные вопросы почти всегда однобоко освещаются прессой, так как истинно свободной прессы почти нет (да и была ли когда-то?) 2) злободневные темы порождают сообщества активистов, которые начинают собирать и анализировать данные в поддержку своей точки зрения (или во имя справедливости); также данные начинают открывать / предоставлять публике официальные источники (чтобы их нельзя было обвинить в сокрытии оных). Об имеющихся данных поговорим чуть позже, а пока - цели исследования.

Я хотел для себя ответить на несколько вопросов:

  1. Какова статистика применения поражающего огня полицейскими против черных и белых в абсолютном выражении (т.е. количество случаев) и в удельном выражении (на количество представителей обеих рас)? Можно ли сказать, что полицейские убивают черных чаще, чем белых?

  2. Какова статистика совершения преступлений представителями обеих рас (в абсолютном и удельном выражениях)? Представители какой расы статистически чаще совершают преступления?

  3. Имеется ли взаимосвязь между статистикой совершения преступлений и статистикой гибели от рук полиции (в целом по США, а также отдельно для белых и черных)? Можно ли сказать, что полиция стреляет насмерть пропорционально количеству совершаемых преступлений?

  4. Каким образом найденные закономерности (по пунктам 1-3) распределены между отдельными штатами США?

На данный момент это все вопросы, однако, я не исключаю, что могут добавиться и другие в процессе исследования, которое пока выполнено лишь на самом поверхностном уровне.

Оговорки и допущения

Вы ведь прочитали дисклеймер в начале статьи? :) Кроме того, что там написано, вот еще несколько допущений и оговорок, принятых для исследования в основном в целях упрощения:

  • Исследование касается только США и не распространяется на другие страны.

  • Представителей чернокожей расы в США для краткости я могу называть "черными", а представителей белокожей расы - "белыми"; эти краткие наименования не отражают какого-то неуважения, а приняты именно для лаконичности.

  • Представители белокожей расы ("белые") включают латиноамериканцев (проживающих на территории США), но исключают представителей азиатских рас, американских индейцев, гавайцев, эскимосов и представителей смешанных рас, в соответствии с данными по населению в Википедии, взятыми из официальной переписи населения в США.

  • Для настоящего исследования взяты только белая и черная расы; представители иных рас, а также те, чья раса не указана в источниках, не включены в исследование. Это ограничение сделано для упрощения, основываясь на том, что эти две категории составляют совместно более 80% всего населения США. При этом я не исключаю, что на будущих этапах будут добавлены и остальные расовые категории для полной картины.

Источники данных

Теперь поговорим о том, какие данные используются для исследования. Исходя из обозначенных целей нам нужны данные по:

  • совершенным преступлениям с указанием расовой принадлежности, видов преступления и штатов

  • гибели от рук полиции с указанием расовой принадлежности погибших и места события (штата)

  • численности населения по годам с указанием расовой принадлежности (для вычисления удельных показателей)

Для данных по преступлениям использовалась открытая база данных ФБР Crime Data Explorer, обладающая расширенным API и содержащая детальные данные по преступлениям, арестам, жертвам преступлений в США с 1991 по 2018 год.

Для данных по гибели от рук полиции использовалась открытая база данных на сайте Fatal Encounters, поддерживаемая сообществом. На настоящий момент база (доступная для скачивания) содержит более 28 тысяч записей начиная с 2000 года с подробной информацией о каждом погибшем, кратким описанием события, ссылками на СМИ, местом события и т.д. В Интернете есть и другие базы данных с тем же назначением, например, на сайте MappingPoliceViolence (около 8400 записей с 2013 г.) или БД Washington Post (ок. 5600 записей с 2015 г.). Но БД Fatal Encounters (FENC) на текущий момент самая подробная и имеет самый длинный период наблюдений (20 лет), поэтому я использовал ее. Кстати сказать, официальные источники (ФБР) также обещают открыть базу данных применения силы службами порядка, но это наступит только когда наберется представительная выборка данных. Прочитать об этой будущей официальной базе можно по ссылке.

Наконец, данные по общей численности представителей различных рас взяты из Википедии, которая в свою очередь, берет эти данные из официальных источников - Бюро переписи населения США. К сожалению, данные доступны только за промежуток с 2010 по 2018 год. В связи с этим в рамках данного исследования пришлось: 1) ограничить конечную точку наблюдений 2018 годом; 2) для промежутка с 2000 по 2009 год использовать данные по численности населения, смоделированные при помощи простой линейной регрессии (что вполне оправдано учитывая линейную природу прироста населения). Таким образом, мы будем исследовать все данные за период с 2000 г. (начальная точка в БД FENC) по 2018 г. (конечная точка в данных по численности населения). Все результаты будут основаны на наблюдениях за эти 18 лет.

Подготовка данных

Прежде чем приступить к анализу, необходимо загрузить вышеуказанные исходные данные в удобном виде и подготовить их для использования.

С данными по гибели от рук полиции все понятно: просто скачиваем всю БД с сайта и сохраняем как CSV (можно оставить и в XLSX, но я предпочитаю CSV для унификации и экономии). Здесь прямая ссылка на исходный датасет в Google Spreadsheets, здесь уже готовый CSV.

Поля данных (использованные в анализе выделены жирным шрифтом):
  1. Unique ID - ID в БД

  2. Subject's name - имя жертвы

  3. Subject's age - возраст жертвы

  4. Subject's gender - пол жертвы

  5. Subject's race - раса жертвы (официально указанная)

  6. Subject's race with imputations - раса жертвы (официально указанная или заполненная экспертом)

  7. Imputation probability - вероятность экспертной оценки расы

  8. URL of image of deceased - фото жертвы

  9. Date of injury resulting in death (month/day/year) - дата события

  10. Location of injury (address) - адрес события

  11. Location of death (city) - город события

  12. Location of death (state) - штат события

  13. Location of death (zip code) - почтовый индекс адреса события

  14. Location of death (county) - округ события

  15. Full Address - полный адрес события

  16. Latitude - координата широты

  17. Longitude - координата долготы

  18. Agency responsible for death - правоохранительная служба, причинившая смерть

  19. Cause of death - причина смерти

  20. A brief description of the circumstances surrounding the death - краткое описание обстоятельств

  21. Dispositions/Exclusions INTERNAL USE, NOT FOR ANALYSIS - исключения (НЕ ДЛЯ АНАЛИЗА)

  22. Intentional Use of Force (Developing) - применение силы (намеренное)

  23. Link to news article or photo of official document - ссылка на СМИ

  24. Symptoms of mental illness? INTERNAL USE, NOT FOR ANALYSIS - симптомы помешательства жертвы (НЕ ДЛЯ АНАЛИЗА)

  25. Video - видео

  26. Date&Description - дата и описание

  27. Unique ID formula - формула ID

  28. Unique identifier (redundant) - НЕ ИСПОЛЬЗУЕТСЯ

  29. Date (Year) - год события

Данные по численности населения я сохранил с Википедии и при помощи Excel дополнил модельными данными за 2000 - 2009 гг., применив простую регрессию. Здесь можете взять Excel и итоговый CSV.

Поля данных (использованные в анализе выделены жирным шрифтом):
  1. Year - год

  2. Whitepop - численность белых

  3. Blackpop - численность черных

  4. Asianpop - численность азиатов

  5. Native Hawaiianpop - численность гавайцев

  6. American Indianpop - численность индейцев и эскимосов

  7. Unknownpop - численность других рас / без указания расы

Самое интересное - это скачать и подготовить данные по преступлениям с БД ФБР. Для этого я написал программу на Python, которая подключается к публичному API при помощи API-ключа (который я специально получил на том же сайте). API использует REST для запросов к различным имеющимся базам данных и возвращает данные в виде JSON. Программа скачивает и объединяет данные в единый DataFrame, который затем сохраняется в CSV. В тот же файл добавляются и данные по численности населения с вычислением удельных показателей по преступлениям.

Поля данных (использованные в анализе выделены жирным шрифтом):
  1. Year - год

  2. Offense - вид преступления, одно из:

    • All Offenses - все преступления

    • Assault Offenses - нападения

    • Drugs Narcotic Offenses - преступления, связанные с оборотом наркотиков

    • Larceny Theft Offenses - воровство

    • Murder And Nonnegligent Manslaughter - убийство

    • Sex Offenses - преступления на сексуальной почве

    • Weapon Law Violation - нарушение хранения / оборота оружия

  3. Class - классификатор (здесь это раса, но может быть также возраст, пол и т.д.)

  4. Offender/Victim - данные по преступникам или жертвам (в этом анализе речь пока только о преступниках)

  5. Asian - количество преступлений, совершенных азиатами

  6. Native Hawaiian - количество преступлений, совершенных гавайцами

  7. Black - количество преступлений, совершенных черными

  8. American Indian - количество преступлений, совершенных индейцами и эскимосами

  9. Unknown - количество преступлений, совершенных представителями других рас

  10. White - количество преступлений, совершенных белыми

  11. Whitepop - численность белых на соответствующий год

  12. Blackpop - численность черных на соответствующий год

  13. Asianpop - численность азиатов на соответствующий год

  14. Native Hawaiianpop - численность гавайцев на соответствующий год

  15. American Indianpop - численность индейцев и эскимосов на соответствующий год

  16. Unknownpop - численность представителей других рас на соответствующий год

  17. Asian pro capita - удельное количество преступлений, совершенных азиатами (на 1 человека)

  18. Native Hawaiian pro capita - удельное количество преступлений, совершенных гавайцами (на 1 человека)

  19. Black pro capita - удельное количество преступлений, совершенных черными (на 1 человека)

  20. American Indian pro capita - удельное количество преступлений, совершенных индейцами и эскимосами (на 1 человека)

  21. Unknown pro capita - удельное количество преступлений, совершенных представителями других рас (на 1 человека)

  22. White pro capita - удельное количество преступлений, совершенных белыми (на 1 человека)

Инструменты

Весь анализ я провожу с помощью Python 3.8, используя интерактивный Jupyter Notebook. Дополнительные библиотеки:

  • pandas 1.0.3 (для анализа данных)

  • folium 0.11 (для визуализации карт)

Все это "добро" (включая сам Python) доступно мне из дистрибутива WinPython, который я давно использую на Windows из-за его очевидных преимуществ. Вы, конечно, можете использовать любой другой на ваш вкус (например Anaconda) или вообще обойтись просто Python, установив нужные пакеты.

Вообще же, этот же анализ можно с легкостью выполнить с помощью любого другого статистического / математического ПО: R, MatLab, SAS и даже Excel. Как говорится, выбирайте свое оружие :)

В следующей части приступим непосредственно к анализу.

Подробнее..

Black Olives Matter раса, криминал и огонь на поражение в США. Часть 2

04.09.2020 08:20:07 | Автор: admin

В первой части статьи я описал предпосылки для исследования, его цели, допущения, исходные данные и инструменты. Сейчас можно без дальнейших разглагольствований сказать гагаринское...

Поехали!

Импортируем библиотеки и определяем путь к директории со всеми файлами:

import pandas as pd, numpy as np# путь к папке с исходными файламиROOT_FOLDER = r'c:\_PROG_\Projects\us_crimes'

Гибель от рук закона

Начнем с анализа данных по жертвам полиции. Давайте подгрузим файл из CSV в DataFrame:

# Файл с БД Fatal Encounters (FENC)FENC_FILE = ROOT_FOLDER + '\\fatal_enc_db.csv'# грузим в DataFramedf_fenc = pd.read_csv(FENC_FILE, sep=';', header=0, usecols=["Date (Year)", "Subject's race with imputations", "Cause of death", "Intentional Use of Force (Developing)", "Location of death (state)"])

Заметьте сразу, что мы не грузим все поля из БД, а только необходимые нам для анализа: год, расовая принадлежность (с учетом экспертной оценки), причина смерти (здесь пока не используется, но может понадобиться в дальнейшем), признак намеренного применения силы и штат, в котором имело место событие.

Здесь надо пояснить, что такое "экспертная оценка" расовой принадлежности. Дело в том, что официальные источники, откуда FENC собирает данные, не всегда указывают расу жертвы, отсюда получаются пропуски в данных. Для компенсации этих пропусков сообщество привлекает экспертов, оценивающих расу жертвы по другим данным (с определенной погрешностью). Более подробно на эту тему можете почитать на самом сайте Fatal Encounters или загрузив исходный Excel файл (во втором листе).

Переименуем столбцы для удобства и очистим строки с пропущенными данными:

df_fenc.columns = ['Race', 'State', 'Cause', 'UOF', 'Year']df_fenc.dropna(inplace=True)

Теперь нам надо унифицировать наименования расовой принадлежности для того, чтобы в дальнейшем сопоставлять эти данные с данными по преступлениям и численности населения. Классификация рас в этих источниках немного разная. БД FENC, в частности, выделяет латиноамериканцев (Hispanic/Latino), азиатов и уроженцев тихоокеанских территорий (Asian/Pacific Islander) и среднеазиатов (Middle Eastern). Нас же интересуют только белые и черные. Поэтому сделаем укрупнение:

df_fenc = df_fenc.replace({'Race': {'European-American/White': 'White', 'African-American/Black': 'Black',                           'Hispanic/Latino': 'White', 'Native American/Alaskan': 'American Indian',                          'Asian/Pacific Islander': 'Asian', 'Middle Eastern': 'Asian',                          'NA': 'Unknown', 'Race unspecified': 'Unknown'}}, value=None)

Оставляем только данные по белым (теперь с учетом латино) и черным:

df_fenc = df_fenc.loc[df_fenc['Race'].isin(['White', 'Black'])]

Зачем нам поле "UOF" (намеренное использование силы)? Для исследования мы хотим оставить только случаи, когда полиция (или иные правоохранительные органы) намеренно применяли силу против человека. Мы опускаем случаи, когда человек совершил самоубийство (например, в результате осады полицией) или погиб в результате ДТП, преследуемый полицейскими. Это допущение сделано по двум причинам: 1) обстоятельства гибели по косвенным причинам часто не позволяют провести прямую причинно-следственную связь между действиями правоохранительных органов и смертью (пример: полицейский держит на мушке человека, который затем умирает от сердечного приступа; другой пример: при задержании преступник пускает себе пулю в лоб); 2) при рассмотрении действий властей расценивается именно применение силы; так, например, будущая официальная БД по применению силы (которую я упомянул в предыдущей статье) будет содержать именно данные, отражающая намеренное применение смертельной силы против граждан. Итак, оставляем только эти данные:

df_fenc = df_fenc.loc[df_fenc['UOF'].isin(['Deadly force', 'Intentional use of force'])]

Для удобства добавим полные названия штатов. Для этого я приготовил отдельный CSV, который мы и подгрузим в наш датасет:

df_state_names = pd.read_csv(ROOT_FOLDER + '\\us_states.csv', sep=';', header=0)df_fenc = df_fenc.merge(df_state_names, how='inner', left_on='State', right_on='state_abbr')

Отобразим начальные строки командой df_fenc.head(), чтобы получить представление о датасете:

Race

State

Cause

UOF

Year

state_name

state_abbr

0

Black

GA

Gunshot

Deadly force

2000

Georgia

GA

1

Black

GA

Gunshot

Deadly force

2000

Georgia

GA

2

Black

GA

Gunshot

Deadly force

2000

Georgia

GA

3

Black

GA

Gunshot

Deadly force

2000

Georgia

GA

4

Black

GA

Gunshot

Deadly force

2000

Georgia

GA

Нам не нужно разбирать отдельные случаи гибели, давайте агрегируем данные по годам и расовой принадлежности:

# группируем по году и расеds_fenc_agg = df_fenc.groupby(['Year', 'Race']).count()['Cause']df_fenc_agg = ds_fenc_agg.unstack(level=1)# конвертируем численные данные в UINT16 для экономииdf_fenc_agg = df_fenc_agg.astype('uint16')

В итоге получили таблицу с 2 столбцами: White (количество белых жертв) и Black (количество черных жертв), индексированную по годам (с 2000 по 2020). Давайте взглянем на эти данные в виде графика:

# белые и черные жертвы полицейских по годам (кол-во гибелей)plt = df_fenc_agg.plot(xticks=df_fenc_agg.index)plt.set_xticklabels(df_fenc_agg.index, rotation='vertical')plt

Промежуточный вывод:

В количественном (абсолютном) выражении белых жертв больше, чем черных.

Разница между этими данными составляет в среднем 2.4 раза. Напрашивается справедливое заключение о том, что это связано с разницей в численности белых и черных. Что же, давайте посмотрим теперь на удельные показатели.

Подгрузим данные по численности населения (по расам):

# файл CSV с данными по населению (1991 - 2018)POP_FILE = ROOT_FOLDER + '\\us_pop_1991-2018.csv'df_pop = pd.read_csv(POP_FILE, index_col=0, dtype='int64')

Добавим эти данные в наш датасет:

# выбираем только данные по числ-ти белых и черных за 2000 - 2018 гг.df_pop = df_pop.loc[2000:2018, ['White_pop', 'Black_pop']]# объединяем датафреймы, выкидываем строки с пропускамиdf_fenc_agg = df_fenc_agg.join(df_pop)df_fenc_agg.dropna(inplace=True)# конвертируем данные по численности в целочисленный типdf_fenc_agg = df_fenc_agg.astype({'White_pop': 'uint32', 'Black_pop': 'uint32'})

ОК. Осталось создать 2 столбца с удельными значениями, разделив количество жертв на численность и умножив на миллион (количество жертв на 1 млн. человек):

df_fenc_agg['White_promln'] = df_fenc_agg['White'] * 1e6 / df_fenc_agg['White_pop']df_fenc_agg['Black_promln'] = df_fenc_agg['Black'] * 1e6 / df_fenc_agg['Black_pop']

Смотрим, что получилось:

Black

White

White_pop

Black_pop

White_promln

Black_promln

Year

2000

148

291

218756353

35410436

1.330247

4.179559

2001

158

353

219843871

35758783

1.605685

4.418495

2002

161

363

220931389

36107130

1.643044

4.458953

2003

179

388

222018906

36455476

1.747599

4.910099

2004

157

435

223106424

36803823

1.949742

4.265861

2005

181

452

224193942

37152170

2.016112

4.871855

2006

212

460

225281460

37500517

2.041890

5.653255

2007

219

449

226368978

37848864

1.983487

5.786171

2008

213

442

227456495

38197211

1.943229

5.576323

2009

249

478

228544013

38545558

2.091501

6.459888

2010

219

506

229397472

38874625

2.205778

5.633495

2011

290

577

230838975

39189528

2.499578

7.399936

2012

302

632

231992377

39623138

2.724227

7.621809

2013

310

693

232969901

39919371

2.974633

7.765653

2014

264

704

233963128

40379066

3.009021

6.538041

2015

272

729

234940100

40695277

3.102919

6.683822

2016

269

723

234644039

40893369

3.081263

6.578084

2017

265

743

235507457

41393491

3.154889

6.401973

2018

265

775

236173020

41617764

3.281493

6.367473

Последние 2 столбца - наши удельные показатели на миллион человек по каждой из двух рас. Пора посмотреть на графике:

plt = df_fenc_agg.loc[:, ['White_promln', 'Black_promln']].plot(xticks=df_fenc_agg.index)plt.set_xticklabels(df_fenc_agg.index, rotation='vertical')plt

Также выведем основную статистику по этим данным:

df_fenc_agg.loc[:, ['White_promln', 'Black_promln']].describe()

White_promln

Black_promln

count (количество)

19.000000

19.000000

mean (среднее арифм.)

2.336123

5.872145

std (станд. отклонение)

0.615133

1.133677

min (мин. значение)

1.330247

4.179559

25%

1.946485

4.890977

50%

2.091501

5.786171

75%

2.991827

6.558062

max (макс. значение)

3.281493

7.765653

Промежуточные выводы:

1. В среднем от рук полиции погибает 5.9 на 1 млн. черных и 2.3 на 1 млн. белых (черных в 2.6 раз больше).

2. Разброс (отклонение) в данных по черным жертвам в 1.8 раз выше, чем в данных по белым жертвам. (На графике видно, что кривая по белым жертвам гораздо более плавная, без резких скачков.)

3. Максимальное количество жертв среди черных - в 2013 г. (7.7 на миллион); максимальное количество жертв среди белых - в 2018 г. (3.3 на миллион).

4. Жертвы среди белых монотонно растут (в среднем на 0.1 - 0.2 в год), в то время как жертвы среди черных вернулись на уровень 2009 г. после пика в 2011 - 2013 гг.

Итак, на первый поставленный вопрос мы ответили:

- Можно ли сказать, что полицейские убивают черных чаще, чем белых?

- Да, это верный вывод. От рук закона черных гибнет в среднем в 2.6 раз больше, чем белых.

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

Данные по преступлениям

Загружаем наш CSV по преступлениям:

CRIMES_FILE = ROOT_FOLDER + '\\culprits_victims.csv'df_crimes = pd.read_csv(CRIMES_FILE, sep=';', header=0, index_col=0, usecols=['Year', 'Offense', 'Offender/Victim', 'White', 'White pro capita', 'Black', 'Black pro capita'])

Здесь опять-таки используем только необходимые столбцы: год, вид преступления, классификатор и данные по количеству преступлений, совершенных черными и белыми (абсолютные - "White", "Black" и удельные на человека - "White pro capita", "Black pro capita").

Взглянем на данные (`df_crimes.head()`):

Offense

Offender/Victim

Black

White

Black pro capita

White pro capita

Year

1991

All Offenses

Offender

490

598

1.518188e-05

2.861673e-06

1991

All Offenses

Offender

4

4

1.239337e-07

1.914160e-08

1991

All Offenses

Offender

508

122

1.573958e-05

5.838195e-07

1991

All Offenses

Offender

155

176

4.802432e-06

8.422314e-07

1991

All Offenses

Offender

13

19

4.027846e-07

9.092270e-08

Нам пока не нужны данные по жертвам преступлений. Убираем лишние данные и столбцы:

# оставляем только преступников (убираем жертв)df_crimes1 = df_crimes.loc[df_crimes['Offender/Victim'] == 'Offender']# берем исследуемый период (2000-2018) и удаляем лишние столбцыdf_crimes1 = df_crimes1.loc[2000:2018, ['Offense', 'White', 'White pro capita', 'Black', 'Black pro capita']]

Получили такой датасет (1295 строк * 5 столбцов):

Offense

White

White pro capita

Black

Black pro capita

Year

2000

All Offenses

679

0.000003

651

0.000018

2000

All Offenses

11458

0.000052

30199

0.000853

2000

All Offenses

4439

0.000020

3188

0.000090

2000

All Offenses

10481

0.000048

5153

0.000146

2000

All Offenses

746

0.000003

63

0.000002

...

...

...

...

...

...

2018

Larceny Theft Offenses

1961

0.000008

1669

0.000040

2018

Larceny Theft Offenses

48616

0.000206

30048

0.000722

2018

Drugs Narcotic Offenses

555974

0.002354

223398

0.005368

2018

Drugs Narcotic Offenses

305052

0.001292

63785

0.001533

2018

Weapon Law Violation

70034

0.000297

58353

0.001402

Теперь нам надо превратить удельные показатели на 1 человека в удельные на 1 миллион (так как именно эти данные используются во всем исследовании). Для этого просто умножаем на миллион соответствующие столбцы:

df_crimes1['White_promln'] = df_crimes1['White pro capita'] * 1e6df_crimes1['Black_promln'] = df_crimes1['Black pro capita'] * 1e6

Чтобы увидеть целую картину, как соотносится количество преступлений между белыми и черными по видам преступлений (в абсолютном выражении), просуммируем годовые наблюдения:

df_crimes_agg = df_crimes1.groupby(['Offense']).sum().loc[:, ['White', 'Black']]

White

Black

Offense

All Offenses

44594795

22323144

Assault Offenses

12475830

7462272

Drugs Narcotic Offenses

9624596

3453140

Larceny Theft Offenses

9563917

4202235

Murder And Nonnegligent Manslaughter

28913

39617

Sex Offenses

833088

319366

Weapon Law Violation

829485

678861

Или в виде графика:

df_crimes_agg.plot.barh()

Итак, видим, что:

  • В количественном отношении нападения, наркотики, воровство и "все преступления" сильно превалируют над преступлениями, связанными с убийством, оружием и сексом

  • В абсолютных значениях белые совершают больше преступлений, чем черные (ровно в 2 раза для категории "все преступления")

Опять понимаем, что без информации о численности никакие выводы о "криминальности" рас не сделаешь. Соответственно, посмотрим на удельные показатели:

df_crimes_agg1 = df_crimes1.groupby(['Offense']).sum().loc[:, ['White_promln', 'Black_promln']]

White_promln

Black_promln

Offense

All Offenses

194522.307758

574905.952459

Assault Offenses

54513.398833

192454.602875

Drugs Narcotic Offenses

41845.758869

88575.523095

Larceny Theft Offenses

41697.303725

108189.184125

Murder And Nonnegligent Manslaughter

125.943007

1016.403706

Sex Offenses

3633.777035

8225.144985

Weapon Law Violation

3612.671402

17389.163849

И на графике:

df_crimes_agg1.plot.barh()

Здесь уже совсем иная картина. По всем видам преступлений (из анализируемых) черные совершают больше, чем белые. По категории "все преступления" эта разница составляет почти 3 раза.

Давайте теперь оставим только категорию "все преступления" (All Offenses) как наиболее представительную, только удельные показатели по преступлениям (на миллион человек) и сгруппируем данные по годам (так как в исходных данных на каждый год может быть несколько записей - по количеству служб, предоставивших данные).

# оставляем только 'All Offenses' = все преступленияdf_crimes1 = df_crimes1.loc[df_crimes1['Offense'] == 'All Offenses']# чтобы использовать другую выборку, можем, например, оставить нападения и убийства:#df_crimes1 = df_crimes1.loc[df_crimes1['Offense'].str.contains('Assault|Murder')]# убираем абсолютные значения и агрегируем по годамdf_crimes1 = df_crimes1.groupby(level=0).sum().loc[:, ['White_promln', 'Black_promln']]

Полученный датасет:

White_promln

Black_promln

Year

2000

6115.058976

17697.409882

2001

6829.701429

20431.707645

2002

7282.333249

20972.838329

2003

7857.691182

22218.966500

2004

8826.576863

26308.815799

2005

9713.826255

30616.569637

2006

10252.894313

33189.382429

2007

10566.527362

34100.495064

2008

10580.520024

34052.276749

2009

10889.263592

33954.651792

2010

10977.017218

33884.236826

2011

11035.346176

32946.454471

2012

11562.836825

33150.706035

2013

11211.113491

32207.571607

2014

11227.354594

31517.346141

2015

11564.786088

31764.865490

2016

12193.026562

33186.064958

2017

12656.261666

34900.390499

2018

13180.171893

37805.202605

Посмотрим на графике:

plt = df_crimes1.plot(xticks=df_crimes1.index)plt.set_xticklabels(df_fenc_agg.index, rotation='vertical')plt

Промежуточные выводы:

1. Белые совершают в 2 раза больше преступлений, чем черные, в абсолютном выражении, но в 3 раза меньше в относительном выражении (на миллион представителей своей расы).

2. Преступность среди белых относительно монотонно растет на протяжении всего периода (выросла в 2 раза за 18 лет). Преступность среди черных также растет, но скачкообразно: с 2001 по 2006 г. резкий рост, с 2007 по 2016 она даже убывала, с 2017 года опять резкий рост. За весь период преступность среди черных выросла также в 2 раза (аналогично белым).

3. Если не принимать во внимание спад среди черной преступности в 2007-2016 гг., преступность среди черных растет более быстрыми темпами, чем среди белых.

Итак, мы ответили на второй вопрос:

- Представители какой расы статистически чаще совершают преступления?

- Черные статистически совершают преступления в 3 раза чаще белых.

Криминальность и гибель от рук полиции

Теперь мы подошли к самому важному: необходимо ответить на третий поставленный вопрос, а именно "Можно ли сказать, что полиция стреляет насмерть пропорционально количеству совершаемых преступлений?"

То есть надо как-то проследить корреляцию между двумя нашими наборами данных - данных по жертвам полиции и данных по преступлениям.

Начнем с того, что объединим эти два датасета в один:

# объединяем датасетыdf_uof_crimes = df_fenc_agg.join(df_crimes1, lsuffix='_uof', rsuffix='_cr')# удаляем лишние столбцы (абс. показатели по жертвам)df_uof_crimes = df_uof_crimes.loc[:, 'White_pop':'Black_promln_cr']

Что получили?

White_pop

Black_pop

White_promln_uof

Black_promln_uof

White_promln_cr

Black_promln_cr

Year

2000

218756353

35410436

1.330247

4.179559

6115.058976

17697.409882

2001

219843871

35758783

1.605685

4.418495

6829.701429

20431.707645

2002

220931389

36107130

1.643044

4.458953

7282.333249

20972.838329

2003

222018906

36455476

1.747599

4.910099

7857.691182

22218.966500

2004

223106424

36803823

1.949742

4.265861

8826.576863

26308.815799

2005

224193942

37152170

2.016112

4.871855

9713.826255

30616.569637

2006

225281460

37500517

2.041890

5.653255

10252.894313

33189.382429

2007

226368978

37848864

1.983487

5.786171

10566.527362

34100.495064

2008

227456495

38197211

1.943229

5.576323

10580.520024

34052.276749

2009

228544013

38545558

2.091501

6.459888

10889.263592

33954.651792

2010

229397472

38874625

2.205778

5.633495

10977.017218

33884.236826

2011

230838975

39189528

2.499578

7.399936

11035.346176

32946.454471

2012

231992377

39623138

2.724227

7.621809

11562.836825

33150.706035

2013

232969901

39919371

2.974633

7.765653

11211.113491

32207.571607

2014

233963128

40379066

3.009021

6.538041

11227.354594

31517.346141

2015

234940100

40695277

3.102919

6.683822

11564.786088

31764.865490

2016

234644039

40893369

3.081263

6.578084

12193.026562

33186.064958

2017

235507457

41393491

3.154889

6.401973

12656.261666

34900.390499

2018

236173020

41617764

3.281493

6.367473

13180.171893

37805.202605

Давайте вспомним, что хранится в каждом поле:

  1. White_pop - численность белых

  2. Black_pop - численность черных

  3. White promln_uof - количество жертв полиции среди белых (на 1 млн)

  4. Black promln_uof - количество жертв полиции среди черных (на 1 млн)

  5. White promln_cr - количество преступлений, совершенных белыми (на 1 млн)

  6. Black promln_cr - количество преступлений, совершенных черными (на 1 млн)

Наверное, можно было бы не полениться и дать этим столбцам русские названия... Но я надеюсь, читатели меня простят :)

Взглянем, как соотносятся графики преступлений и жертв полиции для каждой расы. Начнем с белых - в шахматном порядке :)

plt = df_uof_crimes['White_promln_cr'].plot(xticks=df_uof_crimes.index, legend=True)df_uof_crimes['White_promln_uof'].plot(xticks=df_uof_crimes.index, legend=True, secondary_y=True, style='g')plt.set_xticklabels(df_uof_crimes.index, rotation='vertical')plt

То же самое на диаграмме рассеяния:

Отметим мимоходом, что определенная корреляция есть. ОК, теперь то же для черных:

plt = df_uof_crimes['Black_promln_cr'].plot(xticks=df_uof_crimes.index, legend=True)df_uof_crimes['Black_promln_uof'].plot(xticks=df_uof_crimes.index, legend=True, secondary_y=True, style='g')plt.set_xticklabels(df_uof_crimes.index, rotation='vertical')plt

И скаттерплот:

Здесь все намного хуже: тренды явно "пляшут", хотя общая тенденция все равно прослеживается: пропорция здесь явно прямая, хотя и нелинейная.

Давайте воспользуемся методами матстатистики для определения величины этих корреляций, построив корреляционную матрицу на основе коэффициента Пирсона:

df_corr = df_uof_crimes.loc[:, ['White_promln_cr', 'White_promln_uof', 'Black_promln_cr', 'Black_promln_uof']].corr(method='pearson')df_corr.style.background_gradient(cmap='PuBu')

Получаем такую картинку:

White_promln_cr

White_promln_uof

Black_promln_cr

Black_promln_uof

White_promln_cr

1.000000

0.885470

0.949909

0.802529

White_promln_uof

0.885470

1.000000

0.710052

0.795486

Black_promln_cr

0.949909

0.710052

1.000000

0.722170

Black_promln_uof

0.802529

0.795486

0.722170

1.000000

Коэффициенты корреляции для обеих рас выделены жирным: для белых = 0.885, для черных = 0.722. Таким образом, положительная корреляция между гибелью от полиции и преступностью прослеживается и для белых, и для черных, но для белых она гораздо выше (статистически значима), в то время как для черных она близка к статистической незначимости. Последний результат, конечно, связан с большей неоднородностью данных как по жертвам полиции, так и по преступлениям среди черных.

Напоследок для этой статьи попробуем выяснить, какова вероятность белых и черных преступников быть застреленным полицией. Прямых способом это выяснить у нас нет (нет данных по тому, кто из погибших от рук полиции был зарегистрирован как преступник, а кто как невинная жертва). Поэтому пойдем простым путем: разделим удельное количество жертв полиции на удельное количество преступлений по каждой расовой группе (и умножим на 100, чтобы выразить в %):

# агрегированные значения (по годам)df_uof_crimes_agg = df_uof_crimes.loc[:, ['White_promln_cr', 'White_promln_uof', 'Black_promln_cr', 'Black_promln_uof']].agg(['mean', 'sum', 'min', 'max'])# "вероятность" преступника быть застреленнымdf_uof_crimes_agg['White_uof_cr'] = df_uof_crimes_agg['White_promln_uof'] * 100. / df_uof_crimes_agg['White_promln_cr']df_uof_crimes_agg['Black_uof_cr'] = df_uof_crimes_agg['Black_promln_uof'] * 100. / df_uof_crimes_agg['Black_promln_cr']

Получаем такие данные:

White_promln_cr

White_promln_uof

Black_promln_cr

Black_promln_uof

White_uof_cr

Black_uof_cr

mean

10238.016198

2.336123

30258.208024

5.872145

0.022818

0.019407

sum

194522.307758

44.386338

574905.952459

111.570747

0.022818

0.019407

min

6115.058976

1.330247

17697.409882

4.179559

0.021754

0.023617

max

13180.171893

3.281493

37805.202605

7.765653

0.024897

0.020541

Отобразим полученные значения в виде столбчатой диаграммы:

plt = df_uof_crimes_agg.loc['mean', ['White_uof_cr', 'Black_uof_cr']].plot.bar()

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

Промежуточные выводы:

1. Гибель от рук полиции связана с криминальностью (количеством совершаемых преступлений). При этом эта корреляция неоднородна по расам: для белых она близка к идеальной, для черных далека от идеальной.

2. При рассмотрении совмещенных диаграмм гибели от полиции и преступности видно, что фатальные встречи с полицией растут "в ответ" на рост преступности, с лагом в несколько лет (особенно видно по данным среди черных). Это согласуется с логическим предположением о том, что власти "отвечают" на преступность (больше преступлений -> больше безнаказанности -> больше стычек с представителями закона -> больше смертельных исходов).

3. Белые преступники немного чаще встречают смерть от рук полиции, чем черные. Однако эта разница почти несущественна.

Итак, ответ на третий вопрос:

- Можно ли сказать, что полиция стреляет насмерть пропорционально количеству совершаемых преступлений?

- Да, такая корреляция наблюдается, хотя она неоднородна по расам: для белых почти идеальная, для черных - почти неидеальная.

В следующей части статьи посмотрим на географическое распределение анализируемых данных по штатам США.

Подробнее..

Из песочницы Вредные советы для идеального REST API

15.11.2020 14:11:45 | Автор: admin

Всем привет!


Почему 'идеального' написано в кавычках?!

То, что написано ниже относится к разряду "так делать не надо", однако, если вы считаете иначе интересно будет услышать ваше мнение на этот счёт )


Наверное, многие из нас делали REST API, либо пользовались чьим-то готовым. Разберём в статье "невероятные" трюки, которые помогут сделать ваше API на голову выше, чем у других.


Белый пояс


Все значения в json строка и не иначе!


Ну что ж, возьмём простейший объект:


{  "stringValue" : "value",  "intValue": 123}

Вот к чему 123 тут задавать числом, зачем подобная путаница? Пусть будет строкой, десериализатор разберётся:


{  "stringValue" : "value",  "intValue": "123"}

Гораздо лучше, не так ли? Хм А что если у нас объект в качестве значения свойства?


{  "stringValue" : "value",  "intValue": "123",  "complexValue": {    "key": "value"  }}

Мда Непорядок! Надо сделать по-нормальному:


{  "stringValue" : "value",  "intValue": "123",  "complexValue": "{    \"key\": \"value\"  }"}

Что? Там ещё одно вложенное свойство? Ну, так в чем проблема? Рецепт есть уже, всё продумано!


{  "stringValue" : "value",  "intValue": "123",  "complexValue": "{    \"key\": \"value\",    \"anotherComplexValue\": {      \"superKey\": \"megaValue\"    }  }"}

Вот, везде строка! Не надо заморачиваться с типами! Попарсил строчки и будь здоров! Что? Библиотека не парсит как надо? Ну, так бери нормальную, которая всё правильно сделает. complexValue как строка воспринимается? Ну, это вообще ерунда, очевидно, что там объект, просто грамотно обёрнутый в строку.


"Key": Value это скучно, да и на сеть нагрузка большая...


Если в объекте 2-3 свойства, зачем городить сложный объект? Есть хорошее решение:


[  25000,   "Петька",   {    "key1": "value1",    "key2": "value2"  }]

Супер! Надо Петькину зарплату вытащить? Так бери первое значение! Как Петьку зовут? Второе. Так, а с третьим то непорядок!


[  25000,   "Петька",   "{    \"key1\": \"value1\",    \"key2\": \"value2\"  }"]

Во! Теперь по уму! А нет, надо же нагрузку снизить на сеть, у нас ведь 5 запросов в секунду:


[  25000,   "Петька",  [    "value1",    "value2"  ] ]

Просто супер! Вот бы весь json такой был, цены бы ему не было!


Желтый пояс


Порядок не важен


Если проблемка с последним примером: все значения строки, помнишь?


[  "Петька",  "[    \"value1\",    \"value2\"  ]",  "25000"]

Что случилось? Зарплата 3-им значением теперь стала? Ну, да, возможно, но так даже лучше. А какая разница? Там ведь значение 25000, понятно же, что число. Сделать числом? Зачем? Все значения только строки, запомни!


Хранимые процедуры. Ммм Лакомство


Давай что-нибудь с этим джейсоном полезное сделаем. Например, универсальный исполнитель запросов сделаем, без него любой бэкенд не бэкенд. Берём объект:


{  "queryType": "select",  "table": "lyudi",  "where": "name = Витька AND zarplata > 15000"}

и в хранимую процедуру его! Она сама разберётся и запрос твой оптимизирует! Где такую взять? А вот сделал добрый человек, пользуйся на здоровье, да добрым словом поминай)
Супер, да?
Не надо спрашивать как это под капотом работает, штука огонь!
Хотя Постой! Можно же лучше сделать:


{  "query": "select * from lyudi where name = Витька AND zarplata > 15000"}

Красота да и только! Что? Обычный запрос проще сделать? А кто права на это даст? Вот есть хранимая процедура для универсального выполнения запросов, права даём только на неё и ей одной родимой и пользуемся. ORM? Это что за база такая? Нет-нет. MSSQL и точка.


Красный пояс


Всё переводим на "хранимки"! Ням-ням


Вот может вопрос возникнуть: при чем тут API и хранимые процедуры? Всё очевидно: избавляемся от накладных расходов! Вызвал хэпэшку и вуаля! Поэтому никакого rest'а не надо. Смысл такой: из хранимой процедуры возвращаем несколько курсоров, по ним проходимся и всё готово! И быстро и красиво!


А где пример сего чуда?!

Простите, люди добрые, под рукой примера не было, столь он "идеален", что с наскоку не воспроизведу, но ежели сильно нужно, то просьбам вниму, добавлю примерчик)


Ну, а ежели партнёры со сторонних компаний захотят api использовать так vpn можно сделать, пускай тоже по уму делают: хранимые процедуры используют.


Коричневый пояс


Не надо насчёт коричнего цвета что-то не то думать, это почти чёрный! Высокий уровень то-есть!


JSON произвольной структуры


Если с тех. заданием есть проблемы и заказчик не знает до конца чего он хочет, есть отличный метод! Для данных, о которых пока мало-что знаем заготовим объект произвольной структуры:


{}

Вот так он выглядит на начальном этапе:


{  "key1": "value1",  "key2": 2}

потом свойства добавятся:


{  "key1": "value1",  "key2": 2,  "key3": {    "123": 456  }}

но ничего страшного, возможно что вообще всё поменяется:


{  "objectAsArray": ["Vasya", 123, 456, "Piter"]}

Вот и объект красивый получился! Конфетка просто!


Хранение произвольных данных


Тут вообще проблем нет! Произвольный JSON пишем в базу: для этого заводим столбец с типом VARCHAR(MAX). Всё гениальное просто!


Оптимизация и ничего кроме!


Вот был раньше формат dbf, отчего про него забыли нынче? Применим!


{  "data": "Vasya     123  456  Piter                  "}

10 символов для имени, 5 для номера квартиры, 5 для номер дома, 20 для города. Универсально, красиво! Да, иногда символы лишние, но зато какая структура красивая!


Чёрный пояс


Быть ближе к железу!


Строка это что? Массив байтов, ну, так и давайте следовать определению!


{  "data": [56, 61, 73, 79, 61, 20, 20, 20, 20, 20, 31, 32, 33, 20, 20, 34, 35, 36, 20, 20, 50, 69, 74, 65, 72, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20]}

Чуть не забыл! Значения должны быть строкой:


{  "data": "[56, 61, 73, 79, 61, 20, 20, 20, 20, 20, 31, 32, 33, 20, 20, 34, 35, 36, 20, 20, 50, 69, 74, 65, 72, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20]"}

И универсально, и красиво!


P.S. Не столь "идеальный" P.S. как всё написанное выше...


Если вам показалось, что автор утрирует и выдумывает, то Автор хотел бы, чтобы так оно и было. Однако, сталкиваться приходилось с каждым из этих случаев. Давайте уважать друг друга и делать добро, а не какую-то ерунду) Спасибо за внимание, надеюсь, местами было не только грустно, но и весело!

Подробнее..
Категории: Sql , Api , Json , Rest , Хранимые процедуры

Как получить OpenIDOAuth2 токен для тестирования front-end rest сервисов?

27.06.2020 20:16:09 | Автор: admin
Сейчас трудно встретить систему в которая бы не была rest и не использовала OAuth. Особенностью архитектуры таких систем является необходимость наличия валидного токена для доступа к требуемому Frontend Business REST API в HTTP заголовке (хэдере) Authorization: Bearer TOKEN.

Поэтому если мы хотим напрямую проводить тесты через фронтальный rest нам нужен этот токен. Вопрос как его взять? И так это сделать красиво и правильно? Вот тут я не знаю.

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

Есть обычная типовая система с веб рест фронтом и типовым Single-Page-Application браузерным клиентом на JS. Аутентификация и авторизация KeyCloak с Authorization Code Grant.
Надо обеспечить регулярное нагрузочное тестирование фронтовых рест сервисов.

Задача достаточная простая, если у нас есть токены которые мы можем просто вставить в заголовок и использовать JMeter для генерации необходимого потока запросов. Вот тут я и споткнулся, веб браузер получает токен просто и естественно (KeyCloak JS), но как его получить без браузера методом последовательных HTTP запросов и без исполнения JS я так и не понял
Токен в системе проверяется непосредственно в рест сервисе, а не на API Gateway. Отключить проверку нельзя ибо нет такой возможностью. Просто тестировать без токена не получится.

Далее мы подумали, а почему бы не использовать имеющиеся у нас функциональные end-to-end Selenium тесты, но быстро отказались от этого так как необходимый ресурс одновременно работающих браузеров оказался достаточно велик. Для минимум 50 потоков нам нужны были 50Гб + 25 ядер. Однако, это дало нам идею что токен можно получить через Селениум, а далее передать его в JMeter для использования.

В результате, по быстрому был сделан MVP по следующей схеме:
  • Повышаем время жизни токенов для тестового окружения
  • Отключаем кэши в системе, чтобы сымитировать уникальность пользователей.
  • С помощью Selenium приложения поводим процедуры логина тестовой группы пользователей и сбрасываем в файл их токены. Для чтения токена используем вызов JS через WebDriver return keykcloak.token;
  • С помощью JMeter проводим нагрузочные тестирования с использование токенов пользователей
  • Отчет JMeter всем нравиться




Мы поставили время жизни токенов в 36 часов. Вторичный код авторизации забираем напрямую из соответствующей базы. Время логина одного пользователя составляет около 10 сек, что дает нам 360 токенов в час. Это позволяет при желании легко подготовить их достаточное количество.

Сейчас, оператор запускает на выделенном ноутбуке селениум для сбора токенов на ночь, и на утро подготовленный файл токенов готов и можно запускать JMeter для нагрузки.
Схема работает, заказчик доволен простотой и дешевизной решения, и предлагает перенести это уже на нормальные рельсы с парой контейнеров и запуском из Jenkins.

А мне вот решение кажется кривым. Ну должен же быть способ обойтись без селениума! Напишите если кто знает. Я не смог ничего нагуглить на тему тестирования OpenID&OAuth2.
Подробнее..

Документирование API в Java приложении с помощью Swagger v3

07.01.2021 18:21:18 | Автор: admin

Веб-приложение часто содержит API для взаимодействия с ним. Документирование API позволит клиентам быстрее понять, как использовать ваши сервисы. Если API закрыт от внешнего мира, то все равно стоит уделить время спецификации это поможет вашим новым коллегам быстрее разобраться с системой.


Создание документации вручную утомительный процесс. Swagger поможет вам упростить эту работу.


Что такое Swagger?


Swagger автоматически генерирует документацию API в виде json. А проект Springdoc создаст удобный UI для визуализации. Вы не только сможете просматривать документацию, но и отправлять запросы, и получать ответы.


Также возможно сгенерировать непосредственно клиента или сервер по спецификации API Swagger, для этого нужен генератор кода Swagger-Codegen.


Swagger использует декларативный подход, все как мы любим. Размечаете аннотациями методы, параметры, DTO.


Вы найдете все примеры представленные тут в моем репозитории.


Создание REST API


Чтобы документировать API, для начала напишем его :smile: Вы можете перейти к следующей главе, чтобы не тратить время.


Добавим примитивные контроллеры и одно DTO. Суть нашей системы программа лояльности пользователей.


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


В качестве DTO у нас будет класс UserDto это пользователь нашей системы. У него пять полей, из которых 3 обязательны: имя, уникальный ключ, пол пользователя, количество баллов, дата регистрации


public class UserDto {    private String key;    private String name;    private Long points = 0L;    private Gender gender;    private LocalDateTime regDate = LocalDateTime.now();    public UserDto() {    }    public UserDto(String key, String name, Gender gender) {        this.key = key;        this.name = name;        this.gender = gender;    }    public static UserDto of(String key, String value, Gender gender) {        return new UserDto(key, value, gender);    }    // getters and setters}

public enum Gender {    MAN, WOMAN}

Для взаимодействия с нашей бизнес-логикой, добавим три контроллера: UserController, PointContoller, SecretContoller.


UserController отвечает за добавление, обновление и получение пользователей.


@RestController@RequestMapping("/api/user")public class UserController {    private final Map<String, UserDto> repository;    public UserController(Map<String, UserDto> repository) {        this.repository = repository;    }    @PutMapping(produces = APPLICATION_JSON_VALUE)    public HttpStatus registerUser(@RequestBody UserDto userDto) {        repository.put(userDto.getKey(), userDto);        return HttpStatus.OK;    }    @PostMapping(produces = APPLICATION_JSON_VALUE)    public HttpStatus updateUser(@RequestBody UserDto userDto) {        if (!repository.containsKey(userDto.getKey())) return HttpStatus.NOT_FOUND;        repository.put(userDto.getKey(), userDto);        return HttpStatus.OK;    }    @GetMapping(value = "{key}", produces = APPLICATION_JSON_VALUE)    public ResponseEntity<UserDto> getSimpleDto(@PathVariable("key") String key) {        return ResponseEntity.ok(repository.get(key));    }}

PointContoller отвечает за взаимодействие с баллами пользователя. Один метод этого контроллера отвечает за добавление и удаление балов пользователям.


@RestController@RequestMapping("api/user/point")public class PointController {    private final Map<String, UserDto> repository;    public PointController(Map<String, UserDto> repository) {        this.repository = repository;    }    @PostMapping("{key}")    public HttpStatus changePoints(            @PathVariable String key,            @RequestPart("point") Long point,            @RequestPart("type") String type    ) {        final UserDto userDto = repository.get(key);        userDto.setPoints(                "plus".equalsIgnoreCase(type)                     ? userDto.getPoints() + point                     : userDto.getPoints() - point        );        return HttpStatus.OK;    }}

Метод destroy в SecretContoller может удалить всех пользователей.


@RestController@RequestMapping("api/secret")public class SecretController {    private final Map<String, UserDto> repository;    public SecretController(Map<String, UserDto> repository) {        this.repository = repository;    }    @GetMapping(value = "destroy")    public HttpStatus destroy() {        repository.clear();        return HttpStatus.OK;    }}

Настраиваем Swagger


Теперь добавим Swagger в наш проект. Для этого добавьте следующие зависимости в проект.


<dependency>    <groupId>io.swagger.core.v3</groupId>    <artifactId>swagger-annotations</artifactId>    <version>2.1.6</version></dependency><dependency>    <groupId>org.springdoc</groupId>    <artifactId>springdoc-openapi-ui</artifactId>    <version>1.5.2</version></dependency>

Swagger автоматически находит список всех контроллеров, определенных в нашем приложении. При нажатии на любой из них будут перечислены допустимые методы HTTP (DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT).


Для каждого метода доступные следующие данные: статус ответа, тип содержимого и список параметров.


Поэтому после добавления зависимостей, у нас уже есть документация. Чтобы убедиться в этом, переходим по адресу: localhost:8080/swagger-ui.html


Swagger запущенный с дефолтными настройками


Также можно вызвать каждый метод с помощью пользовательского интерфейса. Откроем метод добавления пользователей.



Пока у нас не очень информативная документация. Давайте исправим это.


Для начала создадим класс конфигурации сваггера SwaggerConfig имя произвольное.


@Configurationpublic class SwaggerConfig {    @Bean    public OpenAPI customOpenAPI() {        return new OpenAPI()                .info(                        new Info()                                .title("Example Swagger Api")                                .version("1.0.0")                );    }}

  • title это название вашего приложения
  • version версия вашего API

Эти данные больше для визуальной красоты UI документации.


Добавление авторов


Добавьте разработчиков API, чтобы было понятно, кто в ответе за это безобразие


@Beanpublic OpenAPI customOpenAPI() {    return new OpenAPI()            .info(                    new Info()                            .title("Loyalty System Api")                            .version("1.0.0")                            .contact(                                    new Contact()                                            .email("me@upagge.ru")                                            .url("https://uPagge.ru")                                            .name("Struchkov Mark")                            )            );}

Разметка контроллеров


Переопределим описания контроллеров, чтобы сделать документацию понятнее. Для этого пометим контроллеры аннотацией @Tag.


@Tag(name="Название контроллера", description="Описание контролера")public class ControllerName {    // ... ... ... ... ...}

Добавили описание контроллеров в Swagger


Скрыть контроллер


У нас есть контроллер, который мы хотим скрыть SecretController. Аннотация @Hidden поможет нам в этом.


@Hidden@Tag(name = "Секретный контролер", description = "Позволяет удалить всех пользователей")public class SecretController {    // ... ... ... ... ...}

Аннотация скрывает контроллер только из Swagger. Он все также доступен для вызова. Используйте другие методы для защиты вашего API.


Наша документация стала намного понятнее, но давайте добавим описания для каждого метода контроллера.


Разметка методов


Аннотация @Operation описывает возможности методов контроллера. Достаточно определить следующие значения:


  • summary короткое описание.
  • description более полное описание.

@Operation(    summary = "Регистрация пользователя",     description = "Позволяет зарегистрировать пользователя")public HttpStatus registerUser(@RequestBody UserDto userDto) {    // ... ... ... ... ...}

Метод с аннотацией Operation


Разметка переменных метода


При помощи аннотации Parameter также опишем переменные в методе, который отвечает за управление баллами пользователей.


public HttpStatus changePoints(    @PathVariable @Parameter(description = "Идентификатор пользователя") String key,    @RequestPart("point") @Parameter(description = "Количество баллов") Long point,    @RequestPart("type") @Parameter(description = "Тип операции") TypeOperation type) {    // ... ... ... ... ...}

С помощью параметра required можно задать обязательные поля для запроса. По умолчанию все поля необязательные.


Разметка DTO


Разработчики стараются называть переменные в классе понятными именами, но не всегда это помогает. Вы можете дать человеко-понятное описание самой DTO и ее переменным с помощью аннотации @Schema


@Schema(description = "Сущность пользователя")public class UserDto {    @Schema(description = "Идентификатор")    private String key;    // ... ... ... ... ...}

Сваггер заполнит переменные, формат которых он понимает: enum, даты. Но если некоторые поля DTO имеют специфичный формат, то помогите разработчикам добавив пример.


@Schema(description = "Идентификатор", example = "A-124523")

Выглядеть это будет так:


Разметка аннотацией Schema


Разметка аннотацией Schema


Но подождите, зачем мы передаем дату регистрации. Да и уникальный ключ чаще всего будет задаваться сервером. Скроем эти поля из swagger с помощью параметра Schema.AccessMode.READ_ONLY:


public class UserDto {    @Schema(accessMode = Schema.AccessMode.READ_ONLY)    private String key;    ...}

Валидация


Добавим валидацию в метод управления баллами пользователя в PointController. Мы не хотим, чтобы можно было передать отрицательные баллы.


Подробнее о валидации данных в этой статье.


public HttpStatus changePoints(    // ... ... ... ... ...    @RequestPart("point") @Min(0) @Parameter(description = "Количество баллов") Long point,    // ... ... ... ... ...) {    // ... ... ... ... ...}

Давайте посмотрим на изменения спецификации. Для поля point появилось замечание minimum: 0.


Валидация в swagger


И все это нам не стоило ни малейшего дополнительного усилия.


Итог


Этих знаний вам хватит, чтобы сделать хорошее описание API вашего проекта.


Если нужны более тонкие настройки, то вы без труда сможете разобраться открыв документацию к аннотациям сваггера.

Подробнее..

Partial Update library. Частичное обновление сущности в Java Web Services

17.02.2021 12:10:47 | Автор: admin

Введение

В структуре веб-сервисов типичным базовым набором операций над экземплярами сущностей(объектами) является CRUD (Create, Read, Update и Delete). Этим операциям в REST соответствуют HTTP методы POST, GET, PUT и DELETE. Но зачастую у разработчика возникает необходимость частичного изменения объекта, соответствующего HTTP методу PATCH. Смысл его состоит в том, чтобы на стороне сервера изменить только те поля объекта, которые были переданы в запросе. Причины для этого могут быть различные:

  • большое количество полей в сущности;

  • большая вероятность одновременного изменения одного и того же объекта при высокой нагрузке, в результате которого произойдет перезапись не только модифицируемых полей;

  • невозможность или более высокая сложность изменения полей в нескольких или всех объектах в хранилище(bulk update);

Эти и, возможно, другие причины побуждают разработчика реализовать стек операций по частичному изменению объекта.

Рассмотрим наиболее часто применяемые варианты решения задачи частичного обновления.

Использование обычного контроллера и DTO

Один из наиболее часто встречаемых вариантов реализации метода PATCH. В контроллере пришедший объект десериализуется в обычный DTO, и далее по стеку слоев приложения считается, что все поля в DTO со значением null не подлежат обработке.

К плюсам данного метода можно отнести "привычность" реализации.

К минусам относится во-первых потеря валидности значения null для обработки (после десериализации мы не знаем отсутствовало ли это поле в передаваемом объекте или оно пришло нам со значением null).

Вторым минусом является необходимость явной обработки каждого поля при конвертировании DTO в модель и далее по стеку в сущность. Особенно сильно это чувствуется в случае обработки сущностей с большим количеством полей, сложной структурой. Частично вторую проблему возможно решить с использованием ObjectMapper(сериализация/десериализация POJO, аннотированных @JsonInclude(Include.NON_NULL) ) ,а так же библиотекой MapStruct, генерирующей код конвертеров.

Использование Map<String, Object> вместо POJO

Map<String, Object> является универсальной структурой для представления данных и десериализации. Практически любой JSON объект может быть десериализован в эту структуру. Но, как мы можем понять из типов обобщения, мы теряем контроль типов на этапе компиляции (а соответственно и на этапе написания исходного кода в IDE).

К достоинствам этого метода можно отнести универсальность представления данных и возможность корректной обработки значения null.

Недостатки же в данном случае глобальны: потеря контроля типов на этапе написания исходного кода и компиляции, необходимость явного приведения типов, высокая сложность рефакторинга, обнаружение практически всех ошибок, связанных с типами данных и именованием полей только в runtime(тесты или реальный запуск приложения).

Использование JSON Patch и JSON Merge Patch

JSON Patch и JSON Merge Patch являются стандартизованными и наиболее универсальными методами описания частичного изменения объекта. Спецификация Java EE содержит интерфейсы, описывающие работу с обоими этими форматами: JsonPatch и JsonMergePatch. Существуют реализации этих интерфейсов, одной из которых является библиотека json-patch. Оба формата кратко описаны в статье Michael Scharhag REST: Partial updates with PATCH.

Достоинства метода: стандартизация, универсальность, возможность реализовать не только изменение значения в объекте, но и добавление, удаление, перемещение, копирование и проверку значений полей в объекте.

К недостаткам метода можно отнести несколько большую сложность структуры данных, описывающих изменения в объекте и ее визуальное несоответствие типовому DTO объекта, более высокую сложность передачи данных об изменениях между слоями приложения, усложнение процесса валидации, усложненный процесс преобразования структуры описания изменений к запросам множественного обновления объектов в БД, etc.

Partial Update library

Основной целью создания библиотеки стало объединение положительных и исключение отрицательных сторон первых двух методов из описанных: использование классических DTO в фасадах и гибкости структуры Map<String, Object> "под капотом".

Ключевыми элементами библиотеки являются интерфейс ChangeLogger и класс ChangeLoggerProducer.

Класс ChangeLoggerProducer предназначен для создания "оберток" POJO, перехватывающих вызовы сеттеров и реализующих интерфейс ChangeLogger для получения изменений, произведенных вызовами сеттеров в виде структуры Map<String, Object>.

Для дальнейших примеров будут использоваться вот такие POJO:

public class UserModel {private String login;private String firstName;private String lastName;private String birthDate;private String email;private String phoneNumber;}@ChangeLoggerpublic class UserDto extends UserModel {}

Вот пример работы с такой "оберткой":

ChangeLoggerProducer<UserDto> producer = new ChangeLoggerProducer<>(UserDto.class);UserDto user = producer.produceEntity();user.setLogin("userlogin");user.setPhoneNumber("+123(45)678-90-12");Map<String, Object> changeLog = ((ChangeLogger) user).changelog();/*    changeLog in JSON notation will contains:    {        "login": "userlogin",        "phoneNumber": "+123(45)678-90-12"    }*/

Суть "обертки" состоит в следующем: при вызове сеттера его имя добавляется в Set<String>, при дальнейшем вызове метода Map<String, Object> changelog() он вернет ассоциативный список, ключом в котором будет имя поля, а соответствующим ключу значением будет объект, возвращенный соответствующим геттером. В случае, если объект, возвращаемый геттером реализует интерфейс ChangeLogger, то в значение поля пойдет результат вызова метода Map<String, Object> changelog().

Для сериализации/десериализации "оберток" реализован класс ChangeLoggerAnnotationIntrospector. Это класс представляет собой Annotation Introspector для ObjectMapper. Основной задачей класса является создание "обертки" при десериализации класса, аннотированного @ChangeLogger аннотацией библиотеки и сериализацией результата вызова метода Map<String, Object> changelog() вместо обычной сериализации всего объекта. Примеры использования ObjectMapper с ChangeLoggerAnnotationIntrospector приведены ниже.

Сериализация:

ObjectMapper mapper = new ObjectMapper.setAnnotationIntrospector(new ChangeLoggerAnnotationIntrospector());ChangeLoggerProducer<UserDto> producer = new ChangeLoggerProducer<>(UserDto.class);UserDto user = producer.produceEntity();user.setLogin("userlogin");user.setPhoneNumber("+123(45)678-90-12");String result = mapper.writeValueAsString(user);/*    result should be equal    "{\"login\": \"userlogin\",\"phoneNumber\": \"+123(45)678-90-12\"}"*/

Десериализация:

ObjectMapper mapper = new ObjectMapper.setAnnotationIntrospector(new ChangeLoggerAnnotationIntrospector());String source = "{\"login\": \"userlogin\",\"phoneNumber\": \"+123(45)678-90-12\"}";UserDto user = mapper.readValue(source, UserDto.class);Map<String, Object> changeLog = ((ChangeLogger) user).changelog();/*    changeLog in JSON notation will contains:    {        "login": "userlogin",        "phoneNumber": "+123(45)678-90-12"    }*/

Используя ObjectMapper с ChangeLoggerAnnotationIntrospector мы можем десериализовать пришедший нам в контроллер JSON с полями для частичного апдейта и далее передавать эти данные в подлежащие слои сервисов для реализации логики. В библиотеке присутствует инфраструктура для реализации мапперов DTO, Model, Entity с использованием "оберток". Пример полного стека приложения реализован в тестовом проекте Partial Update Example.

Итог

Partial Update library позволяет с рядом ограничений реализовать наиболее типичную задачу частичного изменения объекта, используя максимально близкий к типовому способ организации стека приложения. При этом универсальность ассоциативного списка объединена с сохранением контроля типов данных в процессе написания исходного кода и компиляции, что позволяет избежать большого числа ошибок в runtime.

В настоящее время функционал имеет ряд ограничений:

  • реализован только простейший маппинг "поле в поле", что не позволяет автоматизировать ситуации с разными именами одного и того же поля в DTO, Model, Entity;

  • не реализован модуль интеграции со Spring, в связи с чем для реализации сериализации/десериализации "оберток" DTO необходимо реализовать конфигурацию(как в примере), добавляющую ChangeLoggerAnnotationIntrospector в стандартный ObjectMapper контроллера приложения;

  • не реализованы утилиты формирования SQL/HQL запросов для bulk update операций с БД;

В последующих версиях планируется добавление недостающего функционала.

Формат данной статьи не позволяет более детально рассмотреть инфраструктуру для создания мапперов и показать применение библиотеки в типичном стеке приложения. В дальнейшем я могу более детально разобрать Partial Update Example и уделить больше внимания описанию внутренней реализации библиотеки.

Подробнее..
Категории: Java , Api , Rest , Patch , Controller

Мифология REST

02.06.2021 12:04:43 | Автор: admin

Мифология REST


Матчасть


Мало какая технология в истории IT вызывала столько ожесточённых споров, как REST. Самое удивительное при этом состоит в том, что спорящие стороны, как правило, совершенно не представляют себе предмет спора.


Начнём с самого начала. В 2000 году один из авторов спецификаций HTTP и URI Рой Филдинг защитил докторскую диссертацию на тему Архитектурные стили и дизайн архитектуры сетевого программного обеспечения, пятая глава которой была озаглавлена как Representational State Transfer (REST). Диссертация доступна по ссылке.


Как нетрудно убедиться, прочитав эту главу, она представляет собой довольно абстрактный обзор распределённой сетевой архитектуры, вообще не привязанной ни к HTTP, ни к URL. Более того, она вовсе не посвящена правилам дизайна API; в этой главе Филдинг методично перечисляет ограничения, с которыми приходится сталкиваться разработчику распределённого сетевого программного обеспечения. Вот они:


  • клиент и сервер не знают внутреннего устройства друг друга (клиент-серверная архитектура);
  • сессия хранится на клиенте (stateless-дизайн);
  • данные должны размечаться как кэшируемые или некэшируемые;
  • интерфейсы взаимодействия должны быть стандартизированы;
  • сетевые системы являются многослойными, т.е. сервер может быть только прокси к другим серверам;
  • функциональность клиента может быть расширена через поставку кода с сервера.

Всё, на этом определение REST заканчивается. Дальше Филдинг конкретизирует некоторые аспекты имплементации систем в указанных ограничениях, но все они точно так же являются совершенно абстрактными. Буквально: ключевая информационная абстракция в REST ресурс; любая информация, которой можно дать наименование, может быть ресурсом.


Ключевой вывод, который следует из определения REST по Филдингу, вообще-то, таков: любое сетевое ПО в мире соответствует принципам REST, за очень-очень редкими исключениями.


В самом деле:


  • очень сложно представить себе систему, в которой не было бы хоть какого-нибудь стандартизованного интерфейса взаимодействия, иначе её просто невозможно будет разрабатывать;
  • раз есть интерфейс взаимодействия, значит, под него всегда можно мимикрировать, а значит, требование независимости имплементации клиента и сервера всегда выполнимо;
  • раз можно сделать альтернативную имплементацию сервера значит, можно сделать и многослойную архитектуру, поставив дополнительный прокси между клиентом и сервером;
  • поскольку клиент представляет собой вычислительную машину, он всегда хранит хоть какое-то состояние и кэширует хоть какие-то данные;
  • наконец, code-on-demand вообще лукавое требование, поскольку всегда можно объявить данные, полученные по сети, инструкциями на некотором формальном языке, а код клиента их интерпретатором.

Да, конечно, вышеприведённое рассуждение является софизмом, доведением до абсурда. Самое забавное в этом упражнении состоит в том, что мы можем довести его до абсурда и в другую сторону, объявив ограничения REST неисполнимыми. Например, очевидно, что требование code-on-demand противоречит требованию независимости клиента и сервера клиент должен уметь интерпретировать код с сервера, написанный на вполне конкретном языке. Что касается правила на букву S (stateless), то систем, в которых сервер вообще не хранит никакого контекста клиента в мире вообще практически нет, поскольку ничего полезного для клиента в такой системе сделать нельзя. (Что, кстати, постулируется в соответствующем разделе прямым текстом: коммуникация не может получать никаких преимуществ от того, что на сервере хранится какой-то контекст.)


Наконец, сам Филдинг внёс дополнительную энтропию в вопрос, выпустив в 2008 году разъяснение, что же он имел в виду. В частности, в этой статье утверждается, что:


  • REST API не должно зависеть от протокола;
  • разработка REST API должна фокусироваться на описании медиатипов, представляющих ресурсы; при этом клиент вообще ничего про эти медиатипы знать не должен;
  • в REST API не должно быть фиксированных имён ресурсов и операций над ними, клиент должен извлекать эту информацию из ответов сервера.

Короче говоря, REST по Филдингу подразумевает, что клиент, получив каким-то образом ссылку на точку входа REST API, далее должен быть в состоянии полностью выстроить взаимодействие с API, не обладая вообще никаким априорным знанием о нём, и уж тем более не должен содержать никакого специально написанного кода для работы с этим API.


Оставляя за скобками тот факт, что Филдинг весьма вольно истолковал свою же диссертацию, просто отметим, что ни одна существующая система в мире не удовлетворяет описанию REST по Филдингу-2008.


Здравое зерно REST


Нам неизвестно, почему из всех обзоров абстрактной сетевой архитектуры именно диссертация Филдинга обрела столь широкую популярность; очевидно другое: теория Филдинга, преломившись в умах миллионов программистов (включая самого Филдинга), превратилась в целую инженерную субкультуру. Путём редукции абстракций REST применительно конкретно к протоколу HTTP и стандарту URL родилась химера RESTful API, конкретного смысла которой никто не знает.


Хотим ли мы тем самым сказать, что REST является бессмысленной концепцией? Отнюдь нет. Мы только хотели показать, что она допускает чересчур широкую интерпретацию, в чём одновременно кроется и её сила, и её слабость.


С одной стороны, благодаря многообразию интерпретаций, разработчики API выстроили какое-то размытое, но всё-таки полезное представление о правильной архитектуре API. С другой стороны, если бы Филдинг чётко расписал в 2000 году, что же он конкретно имел в виду, вряд ли бы об этой диссертации знало больше пары десятков человек.


Что же правильного в REST-подходе к дизайну API (таком, как он сформировался в коллективном сознании широких масс программистов)? То, что такой дизайн позволяет добиться более эффективного использования времени времени программистов и компьютеров.


Если коротко обобщить все сломанные в попытках определить REST копья, то мы получим следующий принцип: лучше бы ты разрабатывал распределённую систему так, чтобы промежуточные агенты умели читать метаданные запросов и ответов, проходящих через эту систему (готовая заповедь для RESTful-пастафарианства практически!).


У протокола HTTP есть очень важное достоинство: он предоставляет стороннему наблюдателю довольно подробную информацию о том, что произошло с запросом и ответом, даже если этот наблюдатель ничего не знает о семантике операции:


  • URL идентифицирует некоего конечного получателя запроса, какую-то единицу адресации;
  • по статусу ответа можно понять, выполнена ли операция успешно или произошла ошибка; если имеет место быть ошибка то можно понять, кто виноват (клиент или сервер), и даже в каких-то случаях понять, что же конкретно произошло;
  • по методу запроса можно понять, является ли операция модифицирующей; если операция модифицирующая, то можно выяснить, является ли она идемпотентной;
  • по методу запроса и статусу и заголовкам ответа можно понять, кэшируем ли результат операции, и, если да, то какова политика кэширования.

Немаловажно здесь то, что все эти сведения можно получить, не вычитывая тело ответа целиком достаточно прочитать служебную информацию из заголовков.


Почему это полезно? Потому что современный стек взаимодействия между клиентом и сервером является (как предсказывал Филдинг) многослойным. Разработчик пишет код поверх какого-то фреймворка, который отправляет запросы; фреймворк базируется на API языка программирования, которое, в свою очередь, обращается к API операционной системы. Далее запрос (возможно, через промежуточные HTTP-прокси) доходит до сервера, который, в свою очередь, тоже представляет собой несколько слоёв абстракции в виде фреймворка, языка программирования и ОС; к тому же, перед конечным сервером, как правило, находится веб-сервер, проксирующий запрос, а зачастую и не один. В современных облачных архитектурах HTTP-запрос, прежде чем дойти до конечного обработчика, пройдёт через несколько абстракций в виде прокси и гейтвеев. Если бы все эти агенты трактовали мета-информацию о запросе одинаково, это позволило бы обрабатывать многие ситуации оптимальнее тратить меньше ресурсов и писать меньше кода.


(На самом деле, в отношении многих технических аспектов промежуточные агенты и так позволяют себе разные вольности, не спрашивая разработчиков. Например, свободно менять Accept-Encoding и Content-Length при проксировании запросов.)


Каждый из аспектов, перечисленных Филдингом в REST-принципах, позволяет лучше организовать работу промежуточного ПО. Ключевым здесь является stateless-принцип: промежуточные прокси могут быть уверены, что метаинформация запроса его однозначно описывает.


Приведём простой пример. Пусть в нашей системе есть операции получения профиля пользователя и его удаления. Мы можем организовать их разными способами. Например, вот так:


// Получение профиляGET /meCookie: session_id=<идентификатор сессии>// Удаление профиляGET /delete-meCookie: session_id=<идентификатор сессии>

Почему такая система неудачна с точки зрения промежуточного агента?


  1. Сервер не может кэшировать ответы; все /me для него одинаковые, поскольку он не умеет получать уникальный идентификатор пользователя из куки; в том числе промежуточные прокси не могут и заранее наполнить кэш, так как не знают идентификаторов сессий.
  2. На сервере сложно организовать шардирование, т.е. хранение информации о разных пользователях в разных сегментах сети; для этого опять же потребуется уметь обменивать сессию на идентификатор пользователя.

Первую проблему можно решить, сделав операции более машиночитаемыми, например, перенеся идентификатор сессии в URL:


// Получение профиляGET /me?session_id=<идентификатор сессии>// Удаление профиляGET /delete-me?session_id=<идентификатор сессии>

Шардирование всё ещё нельзя организовать, но теперь сервер может иметь кэш (в нём будут появляться дубликаты для разных сессий одного и того же пользователя, но хотя бы ответить из кэша неправильно будет невозможно), но возникнут другие проблемы:


  1. URL обращения теперь нельзя сохранять в логах, так как он содержит секретную информацию; более того, появится риск утечки данных со страниц пользователей, т.к. одного URL теперь достаточно для получения данных.
  2. Ссылку на удаление пользователя клиент обязан держать в секрете. Если её, например, отправить в мессенджер, то робот-префетчер мессенджера удалит профиль пользователя.

Как же сделать эти операции правильно с точки зрения REST? Вот так:


// Получение профиляGET /user/{user_id}Authorization: Bearer <token>// Удаление профиляDELETE /user/{user_id}Authorization: Bearer <token>

Теперь URL запроса в точности идентифицирует ресурс, к которому обращаются, поэтому можно организовать кэш и даже заранее наполнить его; можно организовать маршрутизацию запроса в зависимости от идентификатора пользователя, т.е. появляется возможность шардирования. Префетчер мессенджера не пройдёт по DELETE-ссылке; а если он это и сделает, то без заголовка Authorization операция выполнена не будет.


Наконец, неочевидная польза такого решения заключается в следующем: промежуточный сервер-гейтвей, обрабатывающий запрос, может проверить заголовок Authorization и переслать запрос далее без него (желательно, конечно, по безопасному соединению или хотя бы подписав запрос). И, в отличие от схемы с идентификатором сессии, мы всё ёщё можем свободно организовывать кэширование данных в любых промежуточных узлах. Более того, агент может легко модифицировать операцию: например, для авторизованных пользователей пересылать запрос дальше как есть, а неавторизованным показывать публичный профиль, пересылая запрос на специальный URL, ну, скажем, GET /user/{user_id}/public-profile для этого достаточно всего лишь дописать /public-profile к URL, не изменяя все остальные части запроса. Для современных микросервисных архитектур возможность корректно и дёшево модифицировать запрос при маршрутизации является самым ценным преимуществом в концепции REST.


Шагнём ещё чуть вперёд. Предположим, что гейтвей спроксировал запрос DELETE /user/{user_id} в нужный микросервис и не дождался ответа. Какие дальше возможны варианты?


Вариант 1. Можно сгенерировать HTML-страницу с ошибкой, вернуть её веб-серверу, чтобы тот вернул её клиенту, чтобы клиент показал её пользователю, и дождаться реакции пользователя. Мы прогнали через систему сколько-то байтов и переложили решение проблемы на конечного потребителя. Попутно заметим, что при этом на уровне логов веб-сервера ошибка неотличима от успеха и там, и там какой-то немашиночитаемый ответ со статусом 200 и, если действительно пропала сетевая связность между гейтвеем и микросервисом, об этом никто не узнает.


Вариант 2. Можно вернуть веб-серверу подходящую HTTP-ошибку, например, 504, чтобы тот вернул её клиенту, чтобы клиент обработал ошибку и, сообразно своей логике, что-то предпринял по этому поводу, например, отправил запрос повторно или показал ошибку пользователю. Мы прокачали чуть меньше байтов, попутно залогировав исключительную ситуацию, но теперь переложили решение на разработчика клиента это ему надлежит не забыть написать код, который умеет работать с ошибкой 504.


Вариант 3. Гейтвей, зная, что метод DELETE идемпотентен, может сам повторить запрос; если исполнить запрос не получилось проследовать по варианту 1 или 2. В этой ситуации мы переложили ответственность за решение на архитектора системы, который должен спроектировать политику перезапросов внутри неё (и гарантировать, что все операции за DELETE действительно идемпотенты), но мы получили важное свойство: система стала самовосстанавливающейся. Теперь она может сама побороть какие-то ситуации, которые раньше вызывали исключения.


Внимательный читатель может заметить, что вариант (3) при этом является наиболее технически сложным из всех, поскольку включает в себя варианты (1) и (2): для правильной работы всей схемы разработчику клиента всё равно нужно написать код работы с ошибкой. Это, однако, не так; есть очень большая разница в написании кода для системы (3) по сравнению с (1) и (2): разработчику клиента не надо знать как устроена политика перезапросов сервера. Он может быть уверен, что сервер уже сам выполнил необходимые действия, и нет никакого смысла немедленно повторять запрос. Все серверные методы с этой точки зрения для клиента начинают выглядеть одинаково, а значит, эту функциональность (ожидание перезапроса, таймауты) можно передать на уровень фреймворка.


Важно, что для разработчика клиента при правильно работающих фреймворках (клиентском и серверном) пропадают ситуации неопределённости: ему не надо предусматривать какую-то логику восстановления, когда запрос вроде бы не прошёл, но его ещё можно попытаться исправить. Клиент-серверное взаимодействие становится бинарным или успех, или ошибка, а все пограничные градации обрабатываются другим кодом.


В итоге, более сложная архитектура оказалась разделена по уровням ответственности, и каждый разработчик занимается своим делом. Разработчик гейтвея гарантирует наиболее оптимальный роутинг внутри дата-центра, разработчик фреймворка предоставляет функциональность по реализации политики таймаутов и перезапросов, а разработчик клиента пишет бизнес-логику обработки ошибок, а не код восстановления из низкоуровневых состояний неопределённости.


Разумеется все подобные оптимизации можно выполнить и без опоры на стандартную номенклатуру методов / статусов / заголовков HTTP, или даже вовсе поверх другого протокола. Достаточно разработать одинаковый формат данных, содержащий нужную мета-информацию, и научить промежуточные агенты и фреймворки его читать. В общем-то, именно это Филдинг и утверждает в своей диссертации. Но, конечно, очень желательно, чтобы этот код уже был кем-то написан за нас.


Заметим, что многочисленные советы как правильно разрабатывать REST API, которые можно найти в интернете, никак не связаны с изложенными выше принципами, а зачастую и противоречат им:


  1. Не используйте в URL глаголы, только существительные этот совет является всего лишь костылём для того, чтобы добиться правильной организации мета-информации об операции. В контексте работы с URL важно добиться двух моментов:


    • чтобы URL был ключом кэширования для кэшируемых операций и ключом идемпотентности для идемпотентных;
    • чтобы при маршрутизации запроса в многослойной системе можно было легко понять, куда будет маршрутизироваться эта операция по её URL как машине, так и человеку.

  2. Используйте HTTP-глаголы для описания действий того, что происходит с ресурсом это правило попросту ставит телегу впереди лошади. Глагол указывает всем промежуточным агентам, является ли операция (не)модифицирующей, (не)кэшируемой, (не)идемпотентной и есть ли у запроса тело; вместо того, чтобы выбирать строго по этим четырём критериям, предлагается воспользоваться какой-то мнемоникой если глагол подходит к смыслу операции, то и ок. Это в некоторых случаях просто опасно: вам может показаться, что DELETE /list?element_index=3 прекрасно описывает ваше намерение удалить третий элемент списка, но т.к. эта операция неидемпотентна, использовать метод DELETE здесь нельзя;


  3. Используйте POST для создания сущностей, GET для доступа к ним, PUT для полной перезаписи, PATCH для частичной и DELETE для удаления вновь мнемоника, позволяющая на пальцах прикинуть, какие побочные эффекты возможны у какого из методов. Если попытаться разобраться в вопросе глубже, то получится, что вообще-то этот совет находится где-то между бесполезен и вреден:


    • использовать метод GET в API имеет смысл тогда и только тогда, когда вы можете указать заголовки кэширования; если выставить Cache-Control в no-cache то получится просто неявный POST; если их не указать совсем, то какой-то промежуточный агент может взять и додумать их за вас;
    • создание сущностей желательно делать идемпотентным, в идеале за PUT (например, через схему с драфтами);
    • частичная перезапись через PATCH опасная и двусмысленная операция, лучше её декомпозировать через более простые PUT;
    • наконец, в современных системах сущности очень редко удаляются скорее архивируются или помечаются скрытыми, так что и здесь PUT /archive?entity_id будет уместнее.

  4. Не используйте вложенные ресурсы это правило просто отражает тот факт, что отношения между сущностями имеют тенденцию меняться, и строгие иерархии перестают быть строгими.


  5. Используйте множественное число для сущностей, приписывайте слэш в конце URL и тому подобные советы по стилистике кода, не имеющие никакого отношения к REST.



Осмелимся в конце этого раздела сформулировать четыре правила, которые действительно позволят вам написать хорошее REST API:


  1. Соблюдайте стандарт HTTP, особенно в части семантики методов, статусов и заголовков.
  2. Используйте URL как ключ кэша и ключ идемпотентности.
  3. Проектируйте архитектуру так, чтобы для организации маршрутизации запросов внутри многослойной системы было достаточно манипулировать частями URL (хост, путь, query-параметры), статусами и заголовками.
  4. Рассматривайте сигнатуры вызовов HTTP-методов вашего API как код, и применяйте к нему те же стилистические правила, что и к коду: сигнатуры должны быть семантичными, консистентными и читабельными.

Преимущества и недостатки REST


Главное преимущество, которое вам предоставляет REST возможность положиться на то, что промежуточные агенты, от клиентских фреймворков до API-гейтвеев, умеют читать метаданные запроса и выполнять какие-то действия с их использованием настраивать политику перезапросов и таймауты, логировать, кэшировать, шардировать, проксировать и так далее без необходимости писать какой-то дополнительный код. Немаловажно уточнить, что, если вы этими преимуществами не пользуетесь, никакой REST вам не нужен.


Главным недостатком REST является то, что промежуточные агенты, от клиентских фреймворков до API-гейтвеев, умеют читать метаданные запроса и выполнять какие-то действия с их использованием настраивать политику перезапросов и таймауты, логировать, кэшировать, шардировать, проксировать и так далее даже если вы их об этом не просили. Более того, так как стандарты HTTP являются сложными, концепция REST непонятной, а разработчики программного обеспечения неидеальными, то промежуточные агенты могут трактовать метаданные запроса неправильно. Особенно это касается каких-то экзотических и сложных в имплементации стандартов.


Разработка распределённых систем в парадигме REST это всегда некоторый торг: какую функциональность вы готовы отдать на откуп чужому коду, а какую нет. Увы, нащупывать баланс приходится методом проб и ошибок.


О метапрограммировании и REST по Филдингу


Отдельно всё-таки выскажемся о трактовке REST по Филдингу-2008, которая, на самом деле, уходит корнями в распространённую концепцию HATEOAS. С одной стороны, она является довольно логичным продолжением принципов, изложенных выше: если машиночитаемыми будут не только метаданные текущей исполняемой операции, но и всех возможных операций над ресурсом, это, конечно, позволит построить гораздо более функциональные сетевые агенты. Вообще сама идея метапрограммирования, когда клиент является настолько сложной вычислительной машиной, что способен расширять сам себя без необходимости привлечь разработчика, который прочитает документацию API и напишет код работы с ним, конечно, выглядит весьма привлекательной для любого технократа.


Недостатком этой идеи является тот факт, что клиент будет расширять сам себя без привлечения разработчика, который прочитает документацию API и напишет код работы с ним. Возможно, в идеальном мире так работает; в реальном нет. Любое большое API неидеально, в нём всегда есть концепции, для понимания которых (пока что) требуется живой человек. А поскольку, повторимся, API работает мультипликатором и ваших возможностей, и ваших ошибок, автоматизированное метапрограммирование поверх API чревато очень-очень дорогими ошибками.


Пока сильный ИИ не разработан, мы всё-таки настаиваем на том, что код работы с API должен писать живой человек, который опирается на подробную документацию, а не догадки о смысле гиперссылок в ответе сервера.


--


Это черновик будущей главы книги о разработке API. Работа ведётся на Github. Англоязычный вариант этой же главы опубликован на medium. Я буду признателен, если вы пошарите его на реддит я сам не могу согласно политике платформы.

Подробнее..
Категории: Api , Rest , Restful api

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru