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

Программирование

Перевод Почему вы можете обойтись без Babel

01.03.2021 20:22:05 | Автор: admin

Для будущих студентов курса "JavaScript Developer. Basic" подготовили перевод материала.

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


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

Ознакомившись с этой статьей, вы поймете:

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

  • как использовать редактор Visual Studio Code, чтобы обойтись без Babel.

  • существует другая альтернатива программного обеспечения, чтобы сделать ту же работу быстрее.

Что такое Babel и какую проблему он решает?

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

По мере развития браузеров добавляются новые функции API и ECMAScript. Различные браузеры развиваются с разной скоростью и расставляют акценты в качестве приоритетных для разных задач. Это ставит нас перед непростым выбором: как мы можем их все поддерживать и при этом использовать современные функции? Некоторые из них будут несовместимы.

Обычное решение заключается в том, чтобы использовать новейшие возможности и трансформировать их в более старый код, который будет понятен браузеру. Транспилирование (Transpiling сочетание двух слов: transforming преобразование и compiling компиляция) описывает специализированный тип компиляции. Она имеет различные значения в разных контекстах. В нашем случае также существуют две отдельные составляющие для переноса (транспилирования).

Разница между транспилированием (transpiling) и полифилингом (polyfilling)

Транспилирование (Transpiling) это процесс преобразования синтаксиса нового языка, который старые браузеры не могут понять, в старый синтаксис, который они распознают.

Приведем пример переноса оператора let:

// the new syntax `let` was added in ECMAScript 2015 aka ES6let x = 11;// `let` transpiles to the old syntax `var` if your transpiler target was ES5var x = 11;

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

Это можно рассматривать как дополнение недостающих элементов. Например, вот полифил (polyfill) для isNaN:

// check if the method `isNaN` exists on the standard built-in `Number` objectif (!Number.isNaN) {  // if not we add our own version of the native method newer browsers provide  Number.isNaN = function isNaN(x) {    return x !== x;  };}

Наилучшим способом для получения полифилов является использование core-js.

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

Альтернатива 1: не поддерживать древние браузеры

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

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

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

Чтобы определить, нужно ли поддерживать определенный браузер, задайте себе следующие вопросы.

1. Какие браузеры в настоящее время используют Ваши клиенты?

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

Если у вас не установлено аналитическое программное обеспечение, вы не будете знать, какие браузеры вам нужно поддерживать. Вы должны будете сделать обоснованное предположение. Если у вас есть корпоративные клиенты, гораздо больше шансов, что вам понадобится поддержка IE11 (Internet Explorer 11), чем если бы вы занимались маркетингом для фанатов web-literate (грамотное программирование) технологий.

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

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

2. Какие современные функции браузера вы хотите использовать?

Использование современных функций языка и API (Application Programming Interfaces) браузера делает написание кода проще, быстрее и интереснее. Это также делает ваш код более удобным в обслуживании.

Если вам нравиться писать ES5 (ECMAScript) и использовать XMLHttpRequest(), тогда определенно не нужен Babel, но может потребоваться какая-нибудь специальная процедура.

3. Какие современные функции браузера поддерживают браузеры ваших клиентов?

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

Альтернатива 2: Используйте eslint-plugin-compat

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

  • исключает любую зависимость от транспилиров (transpilers). Возвращает вам практический контроль над рабочим кодом.

  • если имеется современная функция, без которой вы не можете жить, то ее можно использовать применив полифил (polyfill).

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

Создать тест

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

Ниже приведен современный код, который должна поддерживать наша целевая среда после переноса (transpiled).

После переноса (transportation) для каждой функции есть console.assert (метод записи сообщений на консоли для пользователя), чтобы убедиться, что она работает, как положено. В случае eslint-plugin-compat вместо этого проверим, что несовместимый код помечен в linting (Linting это процесс, выполняемый программой linter, которая анализирует исходный код на определенном языке программирования и отмечает потенциальные проблемы, такие как синтаксические ошибки, отклонения от предписанного стиля кодирования или использование конструкций, о которых известно, что они небезопасны).

test.js

// test nullish coalescing - return right side when left side null or undefinedconst x = null ?? "default string";console.assert(x === "default string");const y = 0 ?? 42;console.assert(y === 0);// test optional chaining - return undefined on non existent property or methodconst adventurer = {  name: "Alice",  cat: {    name: "Dinah",  },};const dogName = adventurer.dog?.name;console.assert(dogName === undefined);console.assert(adventurer.someNonExistentMethod?.() === undefined);// use browser API fetch, to check lintingfetch("https://jsonplaceholder.typicode.com/todos/1")  .then((response) => response.json())  .then((json) => console.log(json));

Использование eslint env свойства с помощью eslint-plugin-compat

Нам нужен обходной путь для объединения функций языка и API браузера.

Вы можете использовать eslint (Eslint это утилита, проверяющая стандарты кодирования на JavaScript) для проверки синтаксиса языка. Для этого измените свойство env наes2020.

Для проверки совместимости API браузера используйте eslint-plugin-compat. Он использует ту же самую конфигурацию Browserlist и остальные инструменты, что и Babel.

Полную инструкцию можно найти в eslint-plugin-compat repo. Мы воспользуемся browserlist defaults как предустановками по умолчанию. Замените их по своему выбору, основанному на аналитике.

Что такое browserlist?

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

Посмотрите список браузеров, поддерживаемых defaults для browserlist.

defaults использует быстрый доступ к таким версиям браузеров:

  • > 0,5 процента (версии браузеров, выбранные по глобальной статистике использования)

  • Последние две версии (каждого "живого (not dead)" браузера)

  • Firefox ESR (Extended Support Release)

  • Живые (not dead) (браузеры без официальной поддержки и обновлений в течение 24 месяцев).

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

Настройка eslint-plugin-compat для Visual Studio Code

Добавьте следующие пакеты в свой проект.

npm install --save-dev eslint eslint-plugin-compat

Добавьте следующее в package.json.

"browserslist": [    "defaults"  ]

Создайте следующий файл .eslintrc.json или добавьте эти настройки к существующим.

{  "extends": ["plugin:compat/recommended"],  "env": {    "browser": true,    "es2020": true  }}

Убедитесь, что у вас установлено расширение VS Code ESLint.

Теперь любой API браузера, несовместимый с конфигурацией browserlistв вашем package.json, отображается как ошибка linting. Вы можете отдельно контролировать, какую версию ECMAScript вы хотите поддержать, используя свойство env в файле .eslintrc.json.

Было бы неплохо, если бы eslint-plugin-compat автоматически добавил и возможности языка, но на данный момент это является нерешённой задачей.

IE 11 с выбранной настройкой

наш API fetch() помечен.

Поменяйте объект env на es6.

Вы сразу же увидите ошибку при попытке использовать nullish coalescing, который был запущен в составе Es2020.

Альтернатива 3: Используйте другое программное обеспечение для замены Babel

Прежде чем рассматривать альтернативы, давайте быстро рассмотрим, как использовать Babel.

Использование Babel для транспилирования (transpile) и полифилинга (polyfill)

Сначала создайте директорию мини-проекта и установите нужные нам взаимосвязи.

mkdir babel-testcd babel-testnpm init -ymkdir src distnpm install --save-dev @babel/core @babel/cli @babel/preset-envnpm install --save @babel/polyfill

Добавьте следующее в свой package.json.

"browserslist": "defaults",

Запишите файл test.js вsrc, а затем выполните следующую команду.

npx babel src --out-dir dist --presets=@babel/env

Наконец, запустите файл, чтобы проверить, что тесты все еще работают.

node dist/test.js

Ошибок ввода не должно быть, но будет сказано, что fetch is not defined, так как в Node.js нет метода fetch().

Вот результирующий транспилированный (transpiled) код. Обратите внимание на весь лишний мусор и хлам.

"use strict";var _ref, _, _adventurer$dog, _adventurer$someNonEx;// test nullish coalescing - return right side when left side null or undefinedvar x = (_ref = null) !== null && _ref !== void 0 ? _ref : "default string";console.assert(x === "default string");var y = (_ = 0) !== null && _ !== void 0 ? _ : 42;console.assert(y === 0); // test optional chaining - return undefined on non existent property or methodvar adventurer = {  name: "Alice",  cat: {    name: "Dinah",  },};var dogName =  (_adventurer$dog = adventurer.dog) === null || _adventurer$dog === void 0    ? void 0    : _adventurer$dog.name;console.assert(dogName === undefined);console.assert(  ((_adventurer$someNonEx = adventurer.someNonExistentMethod) === null ||  _adventurer$someNonEx === void 0    ? void 0    : _adventurer$someNonEx.call(adventurer)) === undefined,); // use browser API fetch, to check lintingfetch("https://jsonplaceholder.typicode.com/todos/1")  .then(function (response) {    return response.json();  })  .then(function (json) {    return console.log(json);  });

Преимущества и недостатки использования Babel

Преимущества:

  • Эта базовая установка была относительно несложной.

  • У Babel есть большое сообщество для поддержки и постоянных обновлений с 36.8k GitHub звездами на момент написания статьи.

Недостатки:

  • Медленное время компиляции

  • Множество зависимостей (dependencies), даже если они являются зависимостями (dev-dependencies). (установлено 269 пакетов)

  • 39М использованного дискового пространства, как сообщает du -sh

  • 5728 установленных файлов, о чем сообщает find . -тип f | wc -l

Использование swc для транспилирования (transpile) и полифилинга (polyfill)


Swc новый конкурент Babel. Он написан на языке программирования Rust и в 20 раз быстрее. Это может быть очень важно, если вы долго ждете, чтобы построить свой проект.

Чтобы все устроить:

mkdir swc-testcd swc-testnpm init -ymkdir src distnpm install --save-dev @swc/cli @swc/core browserslist

Добавьте следующее в свой package.json.

"browserslist": "defaults",

Запишите конфигурационный файл .swcrc в корневую директорию проекта.

{  "env": {    "coreJs": 3  },  "jsc": {    "parser": {      "syntax": "ecmascript"    }  }}

Запишите ваш тестовый файл в src, затем выполните следующую команду для переноса (transpile).

npx swc src -d dist

Запустите полученный файл, чтобы проверить, что тесты все еще работают.

node dist/test.js

В итоге swc-transpiled (транспилированный) файл, выглядит вот так:

var ref, ref1;var ref2;// test nullish coalescing - return right side when left side null or undefinedvar x = (ref2 = null) !== null && ref2 !== void 0 ? ref2 : "default string";console.assert(x === "default string");var ref3;var y = (ref3 = 0) !== null && ref3 !== void 0 ? ref3 : 42;console.assert(y === 0);// test optional chaining - return undefined on non existent property or methodvar adventurer = {  name: "Alice",  cat: {    name: "Dinah",  },};var dogName =  (ref = adventurer.dog) === null || ref === void 0 ? void 0 : ref.name;console.assert(dogName === undefined);console.assert(  ((ref1 = adventurer.someNonExistentMethod) === null || ref1 === void 0    ? void 0    : ref1.call(ref1)) === undefined,);// use browser API fetch, to check lintingfetch("https://jsonplaceholder.typicode.com/todos/1")  .then(function (response) {    return response.json();  })  .then(function (json) {    return console.log(json);  });

Преимущества и недостатки использования swc

Преимущества:

  • Гораздо меньше зависимостей (установлено 43 пакета)

Недостатки:

  • Меньшая пользовательская база и количество постоянных участников

Другие альтернативы: Google Closure Compiler и TypeScript

Я не включил Google Closure Compiler в качестве опции, потому что он, как это ни печально, сложен в использовании. Тем не менее, он может сделать хорошую работу по транспилированию (transpile) и полифилингу (polyfill). Если у вас есть свободное время, я рекомендую вам проверить его особенно если вы цените небольшой размер файла, так как встроенная функция минификации демонстрирует отличные результаты.

Вы также можете использовать TypeScript для переноса (transpile) и core-js для ручной полифил (polyfill) обработки, но это неуклюжее решение, которое может с легкостью создать больше проблем, чем решить их.

Заключение

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

При необходимости Вы можете использовать функцию linting, чтобы убедиться в их совместимости друг с другом. Это избавит Вас от лишних хлопот, связанных с созданием специального сборочного процесса и транспиляции (transpilation).

Если вы выберете автоматический перевод, то SWC будет намного быстрее, чем Babel, и будет содержать гораздо меньше зависимостей. Также есть возможность использовать Google Closure Compiler или TypeScript, но для их настройки потребуется немного больше усилий.

LogRocket: Полная видимость ваших веб-приложений

LogRocket это передовое решение для мониторинга приложений, позволяющее воспроизводить проблемы так, как если бы они возникали в вашем собственном браузере. Вместо того, чтобы гадать, почему происходят ошибки, или спрашивать пользователей на скриншотах и дампах логов, LogRocket позволяет воспроизводить сеанс, чтобы быстро понять, что пошло не так. Он отлично работает с любым приложением, независимо от фреймворка, и имеет плагины для записи дополнительного контекста из Redux, Vuex и @ngrx/store.

В дополнение к регистрации действий и состояния Redux, LogRocket записывает журналы консольных сообщений, ошибки JavaScript, следы стеков, сетевые запросы/ответы в формате заголовок + тело, метаданные браузера и пользовательские журналы. Кроме того при помощи DOM (Document Object Model) позволяет записывать страницы HTML и CSS, воссоздавая превосходные в пиксельном отношении видео даже для самых сложных одностраничных приложений.


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

Смотреть открытый вебинар Код как проект настройка окружения для разработки на JavaScript.

Подробнее..

Перевод Зависимости JavaScript Все, что вы когда-либо хотели знать, но боялись спросить

02.03.2021 16:05:07 | Автор: admin

Ваше подробное руководство по пяти типам зависимости

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

Также приглашаем всех желающих на открытый вебинар по теме
Vue 3 возможности новой версии одного из самых популярных фронтенд фреймворков. На занятии участники вместе с экспертом:
рассмотрят ключевые отличия в синтаксисе vue2 от vue3;
разберут, как работать с vue-router и VueX в новой версии фреймворка;
cоздадут проект на Vue 3 с нуля с помощью Vue-cli.


Независимо от того, являетесь ли Вы back-end разработчиком, работающим с Node.js, или front-end разработчиком, использующим Node.js только в качестве инструмента для пакетирования и комплектации, Вы наверняка наткнулись на систему зависимостей.

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

. . .

Normal (runtime) dependencies (cтандартные (во время выполнения программы) зависимости)

Давайте начнем с простого, хорошо?

Стандартные зависимости это те, которые вы видите в списке "dependencies" в вашем файле package.json. В большинстве случаев они указывают только имя (для своего ключа) и версию (значение), а затем NPM (node package manager или любой другой менеджер пакетов) позаботится об их захвате из глобального регистра (на npmjs.org).

Однако, это еще не все. Вместо точного номера версии вашей зависимости вы можете указать:

  • Примерный вариант. Вы можете работать с обычными числовыми операторами сравнения, указывая версии больше одного определенного числа (т.е. >1.2.0 ), любой версии ниже или равной другому числу (т.е. <=1.2.0 ), а также можете обыгрывать любую из их комбинаций (т.е. >= , > , <) . Вы также можете указать эквивалентную версию с помощью оператора ~ перед номером версии (т.е. "lodash":"~1.2.2, который загрузит что угодно между 1.2.2 и 1.3.0 или, другими словами, только патчи). И мы также можем указать "совместимую" версию с другим номером, который использует semver для понимания совместимости (т.е. "lodash": "^1.2.0", которая не загрузит ничего, из того что включает в себя изменение с нарушением или отсутствием каких-либо функций).

  • URL-АДРЕС. Правильно, вы даже можете обойтись без версии и напрямую ссылаться на определенный URL, по сути загрузив этот модуль откуда-нибудь еще (например, с Github или напрямую скачав tarball-файл).

  • Локальный файл. Да, вы даже можете непосредственно обращаться к одному из ваших локальных файлов. Это очень удобно, если вы разрабатываете модуль и хотите протестировать его на проекте, прежде чем выпускать его на NPM. С помощью опции "Локальный файл" вы можете сделать ссылку на папку вашего локального модуля. Вы можете использовать как полный, так и частичный пути, при условии, что вы используете префикс-функцию с помощью file:// .

Когда ты будешь использовать стандартные зависимости?

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

Это может показаться простым, но если, например, ваш проект на самом деле является модулем, то вам нужно более внимательно разобраться в том, что это значит. Если ваш модуль предназначен для использования с другими проектами, такими как React или Babel, ваши зависимости не должны включать их, так как они, как предполагается, уже присутствуют, тем не менее, они все равно нужны (мы это увидим позже, где они появятся через секунду).

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

. . .

Peer dependencies (Равные зависимости)

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

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

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

Когда ты будешь использовать Peer dependencies?

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

Другими словами:

  • Когда она вам нужна, но нет необходимости употреблять ее сразу и однозначно. Тогда это peer dependency.

  • Когда она вам нужна, но она уже должна быть установлена другим проектом. Тогда это peer dependency.

Примерами того, когда вы хотите использовать peerDependencies, являются:

  • Babel плагины. Ты хочешь декларировать такие же вещи, как и сам Babel, в качестве равной зависимости (peer dependency).

  • Express middleware packages (Экспресс-пакеты для промежуточной обработки): Это всего лишь один пример модуля NPM, который требует использования peer dependencies. Вы хотите определить приложение Express как зависимость, но не жесткую, в противном случае каждое промежуточное ПО (middleware) будет каждый раз инсталлировать всю структуру заново.

  • Если вы собираете Micro Frontend (Микрофронтенд), пытаясь решить, какие зависимости являются внешними (чтобы они не были связаны), а какие нет. Peer dependencies могут быть хорошим решением для этого.

  • Bit components. Если вы создаете и публикуете front-end компоненты, например, когда вы совместно используете React-компоненты на Bit (Github), вам нужно назначить react библиотеку как peer dependency. Это позволит удостовериться, что нужная библиотека доступна в хостинговом проекте без установки ее в качестве прямой зависимости от него.

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

Снимок экрана component, как видно на Bits component hub.

Если вы установите его, вы получите полный код, который содержит файл package.json, в котором перечислены все peer dependencies:

Обратите внимание, что пакет не имеет прямой зависимости, несмотря на то, что для работы ему необходима React-библиотека.

Это также помогает сохранить размер нашего компонента как можно меньше (1KB) ничего лишнего не добавляется.

. . .

Dev Dependencies (Dev зависимости)

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

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

Да, но есть и другие, например, инструменты для подшивки (linting tools), документация и тому подобное.

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

Принцип их работы заключается в том, что зависимости разработки устанавливаются только при запуске npm install или npm link из корневой папки вашего проекта.

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

Когда ты будешь использовать dev dependencies?

Любая зависимость, которая не требуется для производственного процесса, скорее всего, будет считаться dev dependencies (зависимости развития).

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

Все установленные внутри хост-проекта зависимости будут пропускать dev-зависимости модулей во время инсталляции, но всякий раз, когда вы повторно запускаете npm install в хост-проекте, он будет устанавливать заново все пропущенные ранее dev-зависимости модулей.

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

. . .

Связанные зависимости (Bundled Dependencies)

Они предназначены для тех случаев, когда вы упаковываете свой проект в один файл. Это делается с помощью команды npm pack, которая превращает вашу папку в тарбол (tarball-файл).

Теперь, если вы сделаете это и добавите имена некоторых из ваших зависимостей в массив под названием bundledDependencies (забавный факт: если вы используете bundleDependencies, это тоже будет работать), то тарбол также будет содержать эти зависимости внутри этого массива.

{...   "dependencies": {    "lodash": "1.0.2",    "request": "4.0.1"  },  "bundledDependencies": ["lodash"]...}

Посмотрите на эту часть файла package.json, с такой установкой, при запуске команды npm pack вы получите файл tarball, также содержащий пакет lodash внутри.

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

. . .

Дополнительные зависимости (Optional dependencies)

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

Обычно, когда вы запускаете npm install и процесс не может установить обычную зависимость по какой-либо причине (будь то из-за сбоя в сети, не может найти файл, версию или что-нибудь ещё), то он выдаст ошибку и отменит процесс установки.

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

let foo = null;try {  foo = require("foo-dep");} catch (e) {  foo = require("./local-polyfill")}//... use foo from here on out

Когда вы будете использовать дополнительные зависимости (optional dependencies)?

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

Но другой интересный вариант использования, это установка действительно необязательных зависимостей (optional dependencies). Я имею в виду, что иногда у вас могут быть специфические для системы зависимости, для таких вещей, как совместимость с CI(Continuous Integration)-платформой. В таких сценариях, когда используете платформу, вы захотите установить эти зависимости, в другом случае, проигнорируете.

Для таких ситуаций можно использовать обычную npm install, когда вы используете полный набор зависимостей, а затем использовать npm install --no-optional, когда вы хотите избежать их. Таким образом, вы пропустите эти опции и сосредоточитесь только на обязательных зависимостях.

. . .

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

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

Знали ли вы, что у вас так много вариантов? Оставьте комментарий ниже, если вы использовали некоторые из менее распространенных и расскажите нам, как вы их использовали!


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

Смотреть открытый вебинар по теме Vue 3 возможности новой версии одного из самых популярных фронтенд фреймворков.

Подробнее..

Перевод Как Apache Kafka поддерживает 200К партиций в кластере?

02.03.2021 04:06:51 | Автор: admin


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


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


Брокер Kafka по умолчанию выполняет контролируемое отключение, чтобы минимизировать сбои в обслуживании клиентов. Контролируемое отключение проходит следующие этапы. (1) Сигнал SIG_TERM отправляется брокеру для завершения работы. (2) Брокер отправляет запрос контроллеру, уведомляя, что он готов к отключению. (3) Контроллер затем меняет лидеров партиций на этом брокере на других брокеров и сохраняет эту информацию в ZooKeeper. (4) Контроллер отправляет информацию о новых лидерах другим брокерам в кластере. (5) Контроллер отправляет выключающемуся брокеру положительный ответ на запрос, и брокер, наконец, завершает свой процесс. Таким образом это никак не влияет на клиентов, потому что их трафик уже перенаправлен на других брокеров. Этот процесс изображен на Рисунке 1. Заметьте, что шаги (4) и (5) могут происходить параллельно.



Рис. 1. (1) Инициация отключения на брокере 1; (2) брокер 1 отправляет запрос о контролируемом отключении контроллеру на брокере 0; (3) контроллер записывает новых лидеров в ZooKeeper; (4) контроллер отправляет информацию о новых лидерах брокеру 2; (5) контроллер отправляет положительный ответ брокеру 1.


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


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


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


Мы провели тесты для оценки улучшений по времени контролируемого отключения и перезагрузки контроллера. Для обоих тестов мы загрузили набор из пяти узлов ZooKeeper на выделенных серверах.


Для первого теста мы подготовили кластер Kafka с пятью брокерами на отдельных серверах. В этом кластере мы создали 25 000 топиков, в каждом топике по одной партиции и две реплики, в общем получив 50 000 партиций. Таким образом на каждом брокере было 10 000 партиций. Затем мы замерили время, которое понадобилось для контролируемого отключения. Результаты представлены в таблице ниже.


Версия Kafka 1.0.0 Kafka 1.1.0
Время контролируемого отключения 6,5 минут 3 секунды

Большую часть улучшений дает исправление затрат на журналирование, при котором проводятся ненужные записи для каждой партиции в кластере при смене лидера одной партиции. Просто исправив затраты на журналирование, мы снизили время контролируемого отключения с 6,5 минут до 30 секунд. Переход на асинхронный API ZooKeeper снизил это время до 3 секунд. Эти улучшения существенно снизили время, необходимое для перезагрузки кластера Kafka.


Для второго теста мы подготовили другой кластер Kafka, состоящий из пяти брокеров, создали 2 000 топиков, в каждом по 50 партиций и одной реплике. В сумме во всем кластере получилось 100 000 партиций. Затем мы замерили время перезагрузки состояния контроллера и увидели 100% улучшение (время перезагрузки снизилось с 28 секунд в Kafka 1.0.0 до 14 секунда в Kafka 1.1.0).


Учитывая эти изменения, на поддержку какого количества партиций вы можете рассчитывать в Kafka? Точное число зависит от таких факторов как допустимое окно недоступности, время задержки ZooKeeper, тип хранения на брокере и т.д. В качестве общего правила мы рекомендуем иметь на каждом брокере до 4 000 партиций и до 200 000 партиций в кластере. Основная причина для лимита на кластере заключается в том, что нужно подстраховаться на тот редкий случай серьезного сбоя контроллера, о котором мы писали выше. Обратите внимание, что другие соображения, связанные с партициями, также применимы, и вам может потребоваться дополнительная настройка конфигурации с большим количеством партиций.


Более подробную информацию вы найдете в KAFKA-5642 и KAFKA-5027.


От редакции: Команда Слёрма готовит подробный видеокурс по Apache Kafka. Спикеры Анатолий Солдатов из Авито и Александр Миронов из Stripe. Вводные уроки уже доступны в личном кабинете. Релиз полной версии курса апрель 2021. Подробности.

Подробнее..

Функциональное программирование на TypeScript Option и Either

28.02.2021 16:06:27 | Автор: admin

Предыдущие статьи цикла:


  1. Полиморфизм родов высших порядков
  2. Паттерн класс типов



В предыдущей статье мы рассмотрели понятие класса типов (type class) и бегло познакомились с классами типов функтор, монада, моноид. В этой статье я обещал подойти к идее алгебраических эффектов, но решил всё-таки написать про работу с nullable-типами и исключительными ситуациями, чтобы дальнейшее изложение было понятнее, когда мы перейдем к работе с задачами (tasks) и эффектами. Поэтому в этой статье, всё еще рассчитанной на начинающих ФП-разработчиков, я хочу поговорить о функциональном подходе к решению некоторых прикладных проблем, с которыми приходится иметь дело каждый день.


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


Стало уже некоторым моветоном цитировать Тони Хоара с его ошибкой на миллиард введению в язык ALGOL W понятия нулевого указателя. Эта ошибка, как опухоль, расползлась по другим языкам C, C++, Java, и, наконец, JS. Возможность присвоения переменной любого типа значения null приводит к нежелательным побочным эффектам при попытке доступа по этому указателю среда исполнения выбрасывает исключение, поэтому код приходится обмазывать логикой обработки таких ситуаций. Думаю, вы все встречали (а то и писали) лапшеобразный код вида:


function foo(arg1, arg2, arg3) {  if (!arg1) {    return null;  }  if (!arg2) {    throw new Error("arg2 is required")  }  if (arg3 && arg3.length === 0) {    return null;  }  // наконец-то начинается бизнес-логика, использующая arg1, arg2, arg3}

TypeScript позволяет снять небольшую часть этой проблемы с флагом strictNullChecks компилятор не позволяет присвоить не-nullable переменной значение null, выбрасывая ошибку TS2322. Но при этом из-за того, что тип never является подтипом всех других типов, компилятор никак не ограничивает программиста от выбрасывания исключения в произвольном участке кода. Получается до смешного нелепая ситуация, когда вы видите в публичном API библиотеки функцию add :: (x: number, y: number) => number, но не можете использовать её с уверенностью из-за того, что её реализация может включать выбрасывание исключения в самом неожиданном месте. Более того, если в той же Java метод класса можно пометить ключевым словом throws, что обяжет вызывающую сторону поместить вызов в try-catch или пометить свой метод аналогичной сигнатурой цепочки исключений, то в TypeScript что-то, кроме (полу)бесполезных JSDoc-аннотаций, придумать для типизации выбрасываемых исключений сложно.


Также стоит отметить, что зачастую путают понятия ошибки и исключительной ситуации. Мне импонирует разделение, принятое в JVM-мире: Error (ошибка) это проблема, от которой нет возможности восстановиться (скажем, закончилась память); exception (исключение) это особый случай поток исполнения программы, который необходимо обработать (скажем, произошло переполнение или выход за границы массива). В JS/TS-мире мы выбрасываем не исключения, а ошибки (throw new Error()), что немного запутывает. В последующем изложении я буду говорить именно об исключениях как о сущностях, генерируемых пользовательским кодом и несущими вполне конкретную семантику исключительная ситуация, которую было бы неплохо обработать.

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


Option<A> замена nullable-типам


В современном JS и TS для безопасной работы с nullable-типам есть возможность использовать optional chaining и nullish coalescing. Тем не менее, эти синтаксические возможности не покрывают всех потребностей, с которыми приходится сталкиваться программисту. Вот пример кода, который нельзя переписать с помощью optional chaining только путём монотонной работы с if (a != null) {}, как в Go:


const getNumber = (): number | null => Math.random() > 0.5 ? 42 : null;const add5 = (n: number): number => n + 5;const format = (n: number): string => n.toFixed(2);const app = (): string | null => {  const n = getNumber();  const nPlus5 = n != null ? add5(n) : null;  const formatted = nPlus5 != null ? format(nPlus5) : null;  return formatted;};

Тип Option<A> можно рассматривать как контейнер, который может находиться в одном из двух возможных состояний: None в случае отсутствия значения, и Some в случае наличия значения типа A:


type Option<A> = None | Some<A>;interface None {  readonly _tag: 'None';}interface Some<A> {  readonly _tag: 'Some';  readonly value: A;}

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


import { Monad1 } from 'fp-ts/Monad';const URI = 'Option';type URI = typeof URI;declare module 'fp-ts/HKT' {  interface URItoKind<A> {    readonly [URI]: Option<A>;  }}const none: None = { _tag: 'None' };const some = <A>(value: A) => ({ _tag: 'Some', value });const Monad: Monad1<URI> = {  URI,  // Функтор:  map: <A, B>(optA: Option<A>, f: (a: A) => B): Option<B> => {    switch (optA._tag) {      case 'None': return none;      case 'Some': return some(f(optA.value));    }  },  // Аппликативный функтор:  of: some,  ap: <A, B>(optAB: Option<(a: A) => B>, optA: Option<A>): Option<B> => {    switch (optAB._tag) {      case 'None': return none;      case 'Some': {        switch (optA._tag) {          case 'None': return none;          case 'Some': return some(optAB.value(optA.value));        }      }    }  },  // Монада:  chain: <A, B>(optA: Option<A>, f: (a: A) => Option<B>): Option<B> => {    switch (optA._tag) {      case 'None': return none;      case 'Some': return f(optA.value);    }  }};

Как я писал в предыдущей статье, монада позволяет организовывать последовательные вычисления. Интерфейс монады один и тот же для разных типов высшего порядка это наличие функций chain (она же bind или flatMap в других языках) и of (pure или return).


Если бы в JS/TS был синтаксический сахар для более простой работы с интерфейсом монады, как в Haskell или Scala, то мы единообразно работали бы с nullable-типам, промисами, кодом с исключениями, массивами и много чем еще вместо того, чтобы раздувать язык большим количеством точечных (и, зачастую, частичных) решений частных случаев (Promise/A+, потом async/await, потом optional chaining). К сожалению, подведение под основу языка какой-либо математической базы не является приоритетным направлением работы комитета TC39, поэтому мы работаем с тем, что есть.

Контейнер Option доступен в модуле fp-ts/Option, поэтому я просто импортирую его оттуда, и перепишу императивный пример выше в функциональном стиле:


import { pipe, flow } from 'fp-ts/function';import * as O from 'fp-ts/Option';import Option = O.Option;const getNumber = (): Option<number> => Math.random() > 0.5 ? O.some(42) : O.none;// эти функции модифицировать не нужно!const add5 = (n: number): number => n + 5;const format = (n: number): string => n.toFixed(2);const app = (): Option<string> => pipe(  getNumber(),  O.map(n => add5(n)), // или просто O.map(add5)  O.map(format));

Благодаря тому, что один из законов для функтора подразумевает сохранение композиции функций, мы можем переписать app еще короче:


const app = (): Option<string> => pipe(  getNumber(),  O.map(flow(add5, format)),);

N.B. В этом крохотном примере не нужно смотреть на конкретную бизнес-логику (она умышленно сделана примитивной), а важно подметить одну вещу касательно функциональной парадигмы в целом: мы не просто использовали функцию по-другому, мы абстрагировали общее поведение для вычислительного контекста контейнера Option (изменение значения в случае его наличия) от бизнес-логики (работа с числами). При этом само вынесенное в функтор/монаду/аппликатив/etc поведение можно переиспользовать в других местах приложения, получив один и тот же предсказуемый порядок вычислений в контексте разной бизнес-логики. Как это сделать мы рассмотрим в последующих статьях, когда будем говорить про Free-монады и паттерн Tagless Final. С моей точки зрения, это одна из сильнейших сторон функциональной парадигмы отделение общих абстрактных вещей с последующим переиспользованием их для композиции в более сложные структуры.

Either<E, A> вычисления, которые могут идти двумя путями


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


type Either<E, A> = Left<E> | Right<A>;interface Left<E> {  readonly _tag: 'Left';  readonly left: E;}interface Right<A> {  readonly _tag: 'Right';  readonly right: A;}

Тип Either<E, A> выражает идею вычислений, которые могут пойти по двум путям: левому, завершающемуся значением типа E, или правому, завершающемуся значением типа A. Исторически сложилось соглашение, в котором левый путь считается носителем данных об ошибке, а правый об успешном результате. Для Either точно так же можно реализовать множество классов типов функтор/монаду/альтернативу/бифунктор/etc, и всё это уже есть реализовано в fp-ts/Either. Я же приведу реализацию интерфейса монады для общей справки:


import { Monad2 } from 'fp-ts/Monad';const URI = 'Either';type URI = typeof URI;declare module 'fp-ts/HKT' {  interface URItoKind2<E, A> {    readonly [URI]: Either<E, A>;  }}const left = <E, A>(e: E) => ({ _tag: 'Left', left: e });const right = <E, A>(a: A) => ({ _tag: 'Right', right: a });const Monad: Monad2<URI> = {  URI,  // Функтор:  map: <E, A, B>(eitherEA: Either<E, A>, f: (a: A) => B): Either<E, B> => {    switch (eitherEA._tag) {      case 'Left':  return eitherEA;      case 'Right': return right(f(eitherEA.right));    }  },  // Аппликативный функтор:  of: right,  ap: <E, A, B>(eitherEAB: Either<(a: A) => B>, eitherEA: Either<A>): Either<B> => {    switch (eitherEAB._tag) {      case 'Left': return eitherEAB;      case 'Right': {        switch (eitherEA._tag) {          case 'Left':  return eitherEA;          case 'Right': return right(eitherEAB.right(eitherEA.right));        }      }    }  },  // Монада:  chain: <E, A, B>(eitherEA: Either<E, A>, f: (a: A) => Either<E, B>): Either<E, B> => {    switch (eitherEA._tag) {      case 'Left':  return eitherEA;      case 'Right': return f(eitherEA.right);    }  }};

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


  1. Email содержит знак @;
  2. Email хотя бы символ до знака @;
  3. Email содержит домен после знака @, состоящий из не менее 1 символа до точки, самой точки и не менее 2 символов после точки;
  4. Пароль имеет длину не менее 1 символа.

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


interface Account {  readonly email: string;  readonly password: string;}class AtSignMissingError extends Error { }class LocalPartMissingError extends Error { }class ImproperDomainError extends Error { }class EmptyPasswordError extends Error { }type AppError =  | AtSignMissingError  | LocalPartMissingError  | ImproperDomainError  | EmptyPasswordError;

Императивную реализацию можно представить как-нибудь так:


const validateAtSign = (email: string): string => {  if (!email.includes('@')) {    throw new AtSignMissingError('Email must contain "@" sign');  }  return email;};const validateAddress = (email: string): string => {  if (email.split('@')[0]?.length === 0) {    throw new LocalPartMissingError('Email local-part must be present');  }  return email;};const validateDomain = (email: string): string => {  if (!/\w+\.\w{2,}/ui.test(email.split('@')[1])) {    throw new ImproperDomainError('Email domain must be in form "example.tld"');  }  return email;};const validatePassword = (pwd: string): string => {  if (pwd.length === 0) {    throw new EmptyPasswordError('Password must not be empty');  }  return pwd;};const handler = (email: string, pwd: string): Account => {  const validatedEmail = validateDomain(validateAddress(validateAtSign(email)));  const validatedPwd = validatePassword(pwd);  return {    email: validatedEmail,    password: validatedPwd,  };};

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


import * as E from 'fp-ts/Either';import { pipe } from 'fp-ts/function';import * as A from 'fp-ts/NonEmptyArray';import Either = E.Either;

Переписать императивный код, выбрасывающий исключения, на код с Either'ами достаточно просто в месте, где был оператор throw, пишется возврат левого (Left) значения:


// Было:const validateAtSign = (email: string): string => {  if (!email.includes('@')) {    throw new AtSignMissingError('Email must contain "@" sign');  }  return email;};// Стало:const validateAtSign = (email: string): Either<AtSignMissingError, string> => {  if (!email.includes('@')) {    return E.left(new AtSignMissingError('Email must contain "@" sign'));  }  return E.right(email);};// После упрощения через тернарный оператор и инверсии условия:const validateAtSign = (email: string): Either<AtSignMissingError, string> =>  email.includes('@') ?    E.right(email) :    E.left(new AtSignMissingError('Email must contain "@" sign'));

Аналогичным образом переписываются другие функции:


const validateAddress = (email: string): Either<LocalPartMissingError, string> =>  email.split('@')[0]?.length > 0 ?    E.right(email) :    E.left(new LocalPartMissingError('Email local-part must be present'));const validateDomain = (email: string): Either<ImproperDomainError, string> =>  /\w+\.\w{2,}/ui.test(email.split('@')[1]) ?    E.right(email) :    E.left(new ImproperDomainError('Email domain must be in form "example.tld"'));const validatePassword = (pwd: string): Either<EmptyPasswordError, string> =>  pwd.length > 0 ?     E.right(pwd) :     E.left(new EmptyPasswordError('Password must not be empty'));

Остается теперь собрать всё воедино в функции handler. Для этого я воспользуюсь функцией chainW это функция chain из интерфейса монады, которая умеет делать расширение типов (type widening). Вообще, есть смысл рассказать немного о конвенции именования функций, принятой в fp-ts:


  • Суффикс W означает type Widening расширение типов. Благодаря этому можно в одну цепочку поместить функции, возвращающие разные типы в левых частях Either/TaskEither/ReaderTaskEither и прочих структурах, основанных на типах-суммах:


    // Предположим, есть некие типы A, B, C, D, типы ошибок E1, E2, E3, // и функции foo, bar, baz, работающие с ними:declare const foo: (a: A) => Either<E1, B>declare const bar: (b: B) => Either<E2, C>declare const baz: (c: C) => Either<E3, D>declare const a: A;// Не скомпилируется, потому что chain ожидает мономорфный по типу левой части Either:const willFail = pipe(  foo(a),  E.chain(bar),  E.chain(baz));// Скомпилируется корректно:const willSucceed = pipe(  foo(a),  E.chainW(bar),  E.chainW(baz));
    

  • Суффикс T может означать две вещи либо Tuple (например, как в функции sequenceT), либо монадные трансформеры (как в модулях EitherT, OptionT и тому подобное).
  • Суффикс S означает structure например, как в функциях traverseS и sequenceS, которые принимают на вход объект вида ключ функция преобразования.
  • Суффикс L раньше означал lazy, но в последних релизах от него отказались в пользу ленивости по умолчанию.

Эти суффиксы могут объединяться например, как в функции apSW: это функция ap из класса типов Apply, которая умеет делать type widening и принимает на вход структуру, по ключам которой итерирует.


Возвращаемся к написанию handler. Я использую chainW, чтобы собрать тип возможных ошибок как тип-сумму AppError:


const handler = (email: string, pwd: string): Either<AppError, Account> => pipe(  validateAtSign(email),  E.chainW(validateAddress),  E.chainW(validateDomain),  E.chainW(validEmail => pipe(    validatePassword(pwd),    E.map(validPwd => ({ email: validEmail, password: validPwd })),  )),);

Что же мы получили в результате такого переписывания? Во-первых, функция handler явно сообщает о своих побочных эффектах она может не только вернуть объект типа Account, но и вернуть ошибки типов AtSignMissingError, LocalPartMissingError, ImproperDomainError, EmptyPasswordError. Во-вторых, функция handler стала чистой контейнер Either это просто значение, не содержащее дополнительной логики, поэтому с ним можно работать без боязни, что произойдет что-то нехорошее в месте вызова.


NB: Разумеется, эта оговорка просто соглашение. TypeScript как язык и JavaScript как рантайм никак нас не ограничивают от того, чтобы написать код в духе:
const bad = (cond: boolean): Either<never, string> => {  if (!cond) {    throw new Error('COND MUST BE TRUE!!!');  }  return E.right('Yay, it is true!');};


Понятное дело, что в приличном обществе за такой код бьют канделябром по лицу на код ревью, а после просят переписать с использованием безопасных методов и комбинаторов. Скажем, если вы работаете со сторонними синхронными функциями, их стоит оборачивать в Either/IOEither с помощью комбинатора tryCatch, если с промисами через TaskEither.tryCatch и так далее.

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


Есть у Either брат-близнец тип Validation. Это точно такой же тип-сумма, у которого правая часть означает успех, а левая ошибку валидации. Нюанс заключается в том, что Validation требует, чтобы для левой части типа E была определена операция contact :: (a: E, b: E) => E из класса типов Semigroup. Это позволяет использовать Validation вместо Either в задачах, где необходимо собирать все возможные ошибки. Например, мы можем переписать предыдущий пример (функцию handler) так, чтобы собрать все возможные ошибки валидации входных данных, не переписывая при этом остальные функции валидации (validateAtSign, validateAddress, validateDomain, validatePassword).


Расскажу пару слов об алгебраических структурах, умеющих объединять два элемента

Они выстраиваюся в следующую иерархию:


  • Magma (Магма), или группоид базовый класс типов, определяющий операцию contact :: (a: A, b: A) => A. На эту операцию не налагается никаких других ограничений.
  • Если к магме добавить ограничение ассоциативности для операции concat, получим полугруппу (Semigroup). На практике оказывается, что полугруппы более полезны, так как чаще всего работа ведется со структурами, в которых порядок элементов имеет значимость вроде массивов или деревьев.
  • Если к полугруппе добавить единицу (unit) значение, которое можно сконструировать в любой момент просто так, получим моноид (Monoid).
  • Наконец, если к моноиду добавим операцию inverse :: (a: A) => A, которая позволяет получить для произвольного значения его инверсию, получим группу (Group).

Groupoid hierarchy
Детальнее об иерархии алгебраических структур можно почитать в вики.


Иерархию классов типов, соответствующих таким алгебраическим структурам, можно продолжать и дальше: в библиотеке fp-ts определены классы типов Semiring, Ring, HeytingAlgebra, BooleanAlgebra, разного рода решётки (lattices) и т.п.


Нам для решения задачи получения списка всех ошибок валидации понадобится две вещи: тип NonEmptyArray (непустой массив) и полугруппа, которую можно определить для этого типа. Вначале напишем вспомогательную функцию lift, которая будет переводить функцию вида A => Either<E, B> в функцию A => Either<NonEmptyArray<E>, B>:


const lift = <Err, Res>(check: (a: Res) => Either<Err, Res>) => (a: Res): Either<NonEmptyArray<Err>, Res> => pipe(  check(a),  E.mapLeft(e => [e]),);

Для того, чтобы собрать все ошибки в большой кортеж, я возпользуюсь функцией sequenceT из модуля fp-ts/Apply:


import { sequenceT } from 'fp-ts/Apply';import NonEmptyArray = A.NonEmptyArray;const NonEmptyArraySemigroup = A.getSemigroup<AppError>();const ValidationApplicative = E.getApplicativeValidation(NonEmptyArraySemigroup);const collectAllErrors = sequenceT(ValidationApplicative);const handlerAllErrors = (email: string, password: string): Either<NonEmptyArray<AppError>, Account> => pipe(  collectAllErrors(    lift(validateAtSign)(email),    lift(validateAddress)(email),    lift(validateDomain)(email),    lift(validatePassword)(password),  ),  E.map(() => ({ email, password })),);

Если запустим эти функции с одним и тем же некорректным примером, содержащим более одной ошибки, то получим разное поведение:


> handler('user@host.tld', '123'){ _tag: 'Right', right: { email: 'user@host.tld', password: '123' } }> handler('user_host', ''){ _tag: 'Left', left: AtSignMissingError: Email must contain "@" sign }> handlerAllErrors('user_host', ''){  _tag: 'Left',  left: [    AtSignMissingError: Email must contain "@" sign,    ImproperDomainError: Email domain must be in form "example.tld",    EmptyPasswordError: Password must not be empty  ]}

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



На этом текущую статью я заканчиваю, а в следующей будем говорить уже про Task, TaskEither и ReaderTaskEither. Они позволят нам подойти к идее алгебраических эффектов и понять, что это даёт в плане удобства разработки.

Подробнее..

Запускаем скрипты Ruby из Go Lang

27.02.2021 20:23:19 | Автор: admin

Для использования Ruby как скриптового языка, то есть как языка для встраивания, вроде lua, существует легковесная реализация Ruby под названием MRuby.

Для go удалось найти только одну стабильную библиотеку с биндингом к mruby (https://github.com/mitchellh/go-mruby). По умолчанию, она может собрать mruby версии 1.2.0 (2015 год выпуска), и можно попробовать сделать сборку вплоть до версии 1.4.1 (2018 год выпуска). Но актуальная версия mruby сейчас имеет версию 2.1.2 (2020 год). Есть форк с поддержкой версии mruby 2.1.0 (https://github.com/mrbgems/go-mruby). Этот форк и будем использовать, что бы после небольших изменений получить в том числе версию 2.1.2.

В версиях старше 2.1.0 внесли как минимум следующие несовместимости на которые следует обратить внимание.

Версия 2.1.1:

  • Remove MRB_INT16 configuration option.

Версия 2.1.2

  • IO#readchar returns a UTF-8 character fragment instead of EOFError if EOF is reached in the middle of UTF-8 characters. (86271572) This behavior is different from CRuby, but it is a mruby specification that supports either ASCII or UTF-8 exclusively.

  • Remove mrb_run() from C APIs.

Для успешной сборки go-mruby критично удаление метода mrb_run. Как могут проявиться другие "Breaking Changes" пока не особо ясно. Теперь пошагово установка go-mruby:

  • Клонируем https://github.com/mrbgems/go-mruby/tree/mruby-2 в корень проекта и переключаемся на ветку mruby-2.

  • Удаляем go.mod и go.sum из директории go-mruby. Если их оставить, то go может не дать корректно импортировать модуль внутри программы. Возможно, зависит от версии go и надо покрутить настройки вендоринга, так как на протяжении нескольких версий в go поведение по умолчанию менялось. Ещё можно положить go-mruby в директорию vendor и исправить пути импортов.

  • Из файла mruby.go необходимо удалить метод Run(), а в тестах заменить все вызовы этого метода на RunWithContext().

  • В Makefile исправляем MRUBY_COMMIT на 2.1.2 - так мы получим актуальную версию mruby.

  • Запускаем make. Он сделает клон репозитория указанной версии mruby во вложенную директорию vendor и скомпилирует Си библиотеку libmruby.a.

На что ещё стоит обратить внимание. В форке в обязательном порядке занесён гем mruby-error как раз из за которого форк не был принят (https://github.com/mitchellh/go-mruby/pull/75). В mruby нет поддержки require и нельзя подключить модули в рантайме. Все необходимые модули необходимо подключить на этапе компиляции. Список доступных библиотек есть на странице http://mruby.org/libraries/, а в оригинальном файле mruby/build_config.rb есть пример подключения подключения стандартных и пользовательских библиотек. В mruby/examples/mrbgems можно подсмотреть примеры реализации собственных расширений, а в mruby/mrbgems стандартные библиотеки. Например, возможности метапрограммирования вынесены в отдельный гем mruby-metaprog.

Попробуем подключить поддержку json. Для этого необходимо в go-mruby/build_config.rb прописать библиотеку:

gem :github => 'iij/mruby-iijson'

Пример использования JSON.parse, при этом, как видим, опция symbolize_names этой библиотекой похоже не поддерживается.

func main() {mrb := mruby.NewMrb()defer mrb.Close()class := mrb.DefineClass("Example", nil)class.DefineClassMethod("json_value", func(m *mruby.Mrb, self *mruby.MrbValue) (mruby.Value, mruby.Value) {return mruby.String(`{"int":1, "array":["s1", "s2", {"nil": null}]}`), nil}, mruby.ArgsReq(1))result, err := mrb.LoadString(`JSON.parse(Example.json_value, {"symbolize_names" => true})`)if err != nil {panic(err.Error())}// Result: {"int"=>1, "array"=>["s1", "s2", {"nil"=>nil}]}fmt.Printf("Result: %s\n", result.String())}
Подробнее..

Перевод Дружим WSL и VSCode через Tailscale и упрощаем работу в сети

27.02.2021 20:23:19 | Автор: admin

От переводчика:

Когда-нибудь я подключу к одной сети VPN свою нынешнюю машину в Беларуси и машину в России. Пробовал на зуб ZeroTier, чтобы соединить их вообще, но сервис мне не зашёл, тем более, тогда речь не шла о том, чтобы легко подружить подсистему Linux внутри Windows с любой другой машиной извне. Здесь речь именно об этом. Поэтому, думаю, этот перевод окажется полезным не только мне.


Tailscale это сеть VPN, которая не нуждается в конфигурировании. Она работает поверх других сетей, выравнивает сети и позволяет пользователям и сервисам упростить коммуникацию и сделать её безопаснее. Я подробно писал о том, как подключиться к WSL2 внутри Windows 10 по SSH, с другого компьютера. В инструкции по ссылке не только множество шагов, но и несколько способов подключения.

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

Не было бы проще, если бы все были в одной сети и одной подсети?

Поясню. WSL первой версии делит сетевой стек с Windows 10, поэтому машина WSL и Windows рассматривается как одна и та же. Выполняемый на порте 5000 сервис, сервис Windows или работающее в Linux под WSL1 приложение выполняется на одной и той же машине. Однако в WSL2 Linux находится за хостом Windows.

Хотя WSL2 упрощает доступ к http://localhost:5000 через прозрачную переадресацию портов, ваш Linux на WSL на самом деле не является одноранговым узлом в той же сети, что и другие ваши устройства.

Инструмент вроде Tailscale решает проблему, то есть как будто выравнивает сеть. Надо сказать, что из-за некоторых особенностей WSL2 при работе с ней можно допустить несколько ошибок. Я расскажу, что делал, чтобы всё заработало у меня.

Tailscale на WSL2

Установите WSL2 следуйте этой инструкции. Установите дистрибутив Linux. Я работал с Ubuntu 20.04. Погрузитесь в процесс, создайте пользователя и т.д. Установите Windows Terminal работа с командной строкой станет приятнее. Установите Tailscale я руководствовался инструкцией для Ubuntu 20.04.

Настройка WSL2

Сейчас нельзя запустить Tailscale на WSL2 через IPv6, поэтому я отключил протокол:

sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1

Здесь мы запускаем демон. На WSL2 пока нет systemd, но если ваша версия сборки Windows 10 выше 21286, можно выполнять команды при запуске WSL. Лично я прописал всё в bash.

sudo tailscaled

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

tailscale up --authkey=tskey-9e85d94f237c54253cf0

Мне нравится держать Tailscale открытым в другой вкладке терминала или в другой панели вкладок, так, чтобы смотреть логи. Это интересно и информативно!

В панели администрирования машин Tailscale вы увидите все машины в сети, как показано ниже. Обратите внимание: scottha-proto в списке это Windows, а scottha-proto-1 это Linux. Первая машина это мой хост, вторая экземпляр Linux WSL2, они теперь в одной сети!

У меня получилось пригласить пользователя вне своей сети при помощи новой функции совместного использования узлов. Мой друг Гленн не работает в моей организации, но так же, как и я, пользуется OneDrive или DropBox, чтобы создать ссылку или по созданной ссылке получить доступ к одной сущности системы, а не ко всей системе. Я могу сделать то же самое в смысле Tailscale:

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

Создаём сервис и привязываем его к сети Tailscale

Я установил .NET 5 в Ubuntu на WSL2, создал папку и запустил команду dotnet new web, чтобы сгенерировать микросервисный Hello World. Когда я запускаю сервис .NET, Node, или какой-то ещё, важно, чтобы он прослушивался в сети Tailscale. Linux в WSL2 подключена к нескольким сетям.

По умолчанию мои системы разработчика прослушиваются только локальным хостом; прослушивать сервисы во всех сетях (включая Tailscale) средствами .NET можно по-разному, я запустил прослушивание так:

dotnet run --urls http://*:5100;https://*:5101

Итак, я подключился к IP-адресу в Tailscale, который связан с моим экземпляром WSL2, и постучался к моему сервису внутри Linux:

Что теперь можно сделать? Мы с Гленном находимся в сети Tailscale, кроме того, вся сеть единообразная, а значит, Гленн может легко достучаться до моего сервиса! На скрине ниже я нахожусь в Teams: мой рабочий показывается внизу, а рабочий стол Гленна наверху.

Добавляем VSCode и расширение SSH для удалённой разработки

Окей, у нас есть безопасная, единообразная сеть и полная свобода! Могу ли я превратить мой экземпляр WSL2 в систему удалённой разработки для Гленна? Конечно, почему бы и нет?

Для ясности: это просто разговоры, эксперимент, но в нём что-то есть. Например, кроссплатформенность: можно работать с Mac, Windows, WSL2 и так далее. Этим разделом можно руководствоваться, чтобы поднять виртуальную машину на любом облачном или обычном хостере, установить Tailscale и больше не думать о перенаправлении портов, пользуясь поднятой машиной как песочницей. Да, можно работать с WSL локально, но установка на хосте это весело, так появляется много классных вариантов.

Начнём. В WSL2 я запускаю сервис ssh. Можно было поделиться открытыми ключами и войти в систему с их помощью, но здесь залогинюсь по имени пользователя и отредактирую /etc/ssh/sshd_config, установлю порт, ListenAddressи PasswordAuthenticationв Yes:

Port 22#AddressFamily anyListenAddress 0.0.0.0ListenAddress ::PasswordAuthentication yes

glenn локальный суперпользователь только в моём инстансе WSL2:

sudo adduser glennusermoid -aG sudo glenn

Теперь Гленн устанавливает пакет VS Code Remote Development и при помощи Remote via SSH подключается к моему IP в сети Tailscale. Ниже вы видите VS Code на машине Гленна, где ставится VS Code Server [не перепутайте с code-server для развёртывания VSC в вебе] и удалённую разработку вообще.

С точки зрения архитектуры Гленн и код в VS Code поделены пополам между клиентом на его машине Windows и сервером на моём экземпляре WSL2. Обратите внимание: в левом нижнем углу видно, что его VSCode подключён к IP моей WSL2 в сети Tailscale!

Что вы думаете об этом? Можно сравнить Tailscale с инструментами типа ориентированного на разработчиков типа NGrok, который прокладывает туннели к localhost, но есть кое-какие существенные отличия. Проведите расследование! Я никак не отношусь к компании NGrok, разве что я её фанат.

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

Подробнее..

Перевод Эффективная конструкция агрегатов. Понимание через исследование

28.02.2021 10:04:21 | Автор: admin

Эта статья является конспектом материала Effective Aggregate DesignPart III: Gaining Insight Through Discovery.

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

Переосмысление конструкции модели

После итерации рефакторинга, благодаря которой избавились от большого агрегата Product, BacklogItem стал отдельным агрегатом. Новую версию модели можно увидеть на рисунке 1. Агрегат BacklogItem содержит коллекцию экземпляров Task. Каждый BacklogItem имеет глобальный уникальный идентификатор BacklogItemId. Ассоциация с другими агрегатами происходит через идентификаторы. Агрегат BacklogItem кажется довольно небольшим.

Рис.1. Схема модели агрегата BacklogItemРис.1. Схема модели агрегата BacklogItem

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

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

Ответ лежит в едином языке. Имеются следующие инварианты:

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

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

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

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

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

Оценка стоимости агрегата

Как показано на рисунке 1, каждый Task содержит коллекцию экземпляров EstimationLogEntry. Этот журнал фиксирует конкретные случаи, когда член команды выполняет новую оценку оставшихся часов. На практике, сколько элементов Task может содержать BacklogItem, и сколько элементов EstimationLogEntry будет содержать Task? Точно сказать сложно. Во многом это показатель того, насколько сложна задача и сколько будет длиться спринт. Но некоторые расчеты все же могут помочь.

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

Теперь рассмотрим количество часов, выделенных на каждую задачу. Обычно используют количество часов от 4 до 16. Часто, если задача превышает 12 часов, то эксперты Scrum предлагают разбить ее на более мелкие. В качестве теста предположим, что задачи оцениваются в 12 часов (1 час на каждый день спринта). Итак, получается 12 пересчетов для каждой задачи, предполагая, что каждая задача начинается с 12 часов, выделенные на нее.

Остается вопрос: сколько задач потребуется всего для одного элемента бэклога? Пусть будет, например, тоже 12 (я не стал расписывать, как автор пришел к такому числу; можно самому глянуть в оригинале). В итоге получается 12 задач, каждая из которых содержит 12 оценок в журнале, или 144 (12*12) на элемент бэклога. Хотя это может быть больше чем обычно, но это дает нам конкретную оценку для анализа.

Есть еще одно, что следует учесть. Если следовать рекомендациям экспертов Scrum по определению более мелких задач, это бы несколько изменило ситуацию. Удвоение числа задач (24) и уменьшение вдвое числа записей журнала (6) все равно дают 144. Однако это приведет к загрузке большего количества задач (24 вместо 12) во время запроса на оценку часов, потребляя при этом больше памяти. Но для начала давайте использовать 12 задач по 12 часов каждая.

Общие сценарии использования

Теперь важно рассмотреть общие сценарии использования. Как часто в одном пользовательском запросе нужно будет загружать в память все 144 объекта одновременно? Произойдет ли вообще это когда-то? Если нет, то каково максимальное количество объектов? Кроме того, будет ли многопользовательские запросы, которые могут привести к транзакционной конкуренции? Давайте посмотрим.

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

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

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

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

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

Будут ли ежедневные оценки приводить к проблемам? В первый день спринта обычно нет журналов оценки по заданной задаче элемента бэклога. В конце первого дня каждый член команды, работающий над задачей, сокращает расчетное количество часов на один. Это добавляет новую запись в журнал оценки к каждой задаче, но статус элемента бэклога не изменяется. При этом только один член команды корректирует часы определенной задачи. Только на 12-й день происходит изменение статуса. После того, как будет добавлена последняя 144 запись в журнал для 12 задаче, происходит автоматический переход статуса в done.

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

Потребление памяти

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

Что насчет общего количества задач и оценок в памяти во время каждого повторного оценивания? При использовании ленивой загрузки для задач и журналов оценки у нас будет до 12 + 12 объектов в памяти во время одного запроса, поскольку все 12 задач будут загружены во время обращения к этой коллекции. Чтобы добавить последнюю запись в журнал оценки к одной из задач, нужно загрузить коллекцию записей журнала и это дает еще до 12 объектов. В конечном итоге агрегат содержит один элемент бэклога, до 12 задач и до 12 записей в журнале, что в сумме дает максимум 25 объектов. Это не очень много. Другой факт заключается в том, что максимальное количество объектов не достигается до последнего дня спринта. В течение большей части спринта агрегат будет еще меньше.

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

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

Альтернативная конструкция

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

Рис. 2. BacklogItem и Task как отдельные агрегатыРис. 2. BacklogItem и Task как отдельные агрегаты

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

Реализация конечной согласованности

Когда Task выполняет команду estimateHoursRemaining(), она публикует соответствующие доменное событие для достижения конечной согласованности. Событие имеет следующие свойства:

public class TaskHoursRemainingEstimated implements DomainEvent {     private Date occurredOn;    private TenantId tenantId;    private BacklogItemId backlogItemId;     private TaskId taskId;    private int hoursRemaining;    ...}

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

  • Использует BacklogItemRepository для получения BacklogItem по идентификатору.

  • Использует TaskRepository для получения всех экземпляров Task, связанных с конкретным BacklogItem

  • Выполняет BacklogItem команду estimateTaskHoursRemaining(), передавав ей значение hoursRemaining и определенный экземпляр Task. BacklogItem может менять свой статус в зависимости от параметров.

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

public class TaskRepositoryImpl implements TaskRepository {    ...    public int totalBacklogItemTaskHoursRemaining(       TenantId aTenantId,        BacklogItemId aBacklogItemId) {            Query query = session.createQuery(            "select sum(task.hoursRemaining) from Task task "            + "where task.tenantId = ? and "            + "task.backlogItemId = ?");            ...    }}

Конечная согласованность немного усложняет пользовательский интерфейс. Если согласование состояния не произойдет в течение нескольких сотен миллисекунд, как пользовательский интерфейс отобразит новое состояние? Необходимо ли размещать бизнес-логику в представлении для определения текущего статуса? Это был бы антипаттерн умный UI. В противном случае представление отобразит устаревший статус. Это легко может быть воспринято как ошибка.

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

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

Время принимать решение

Исходя из всего этого анализа, возможно будет лучше отказаться от разделения Task и BacklogItem. Сейчас оно не стоит дополнительных усилий, риска нарушения истинного инварианта или возможности столкнутся с устаревшим статусом в представлении. Текущий агрегат довольно мал. Даже если в худшем случае будет загружено 50 объектов, а не 25, это все равно кластер небольшого размера.

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

Вывод

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

  • Моделируйте истинные инварианты в границах согласованности.

  • Проектируйте небольшие агрегаты.

  • Ссылайтесь на другие агрегаты по идентификатору.

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

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

Ссылки на все части

Подробнее..

Json api сервис на aiohttp middleware и валидация

28.02.2021 18:05:07 | Автор: admin

В этой статье я опишу один из подходов для создания json api сервиса с валидацией данных.


Сервис будет реализован на aiohttp. Это современный, постоянно развивающийся фреймворк на языке python, использующий asyncio.


Об аннотациях:


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


Используемые библиотеки:


  • aiohttp фреймворк для создания web-приложений
  • pydantic классы, которые позволяют декларативно описывать данные и валидировать их
  • valdec декоратор для валидации аргументов и возвращаемых значений у функций

Оглавление:



1. Файлы и папки приложения


- sources - Папка с кодом приложения    - data_classes - Папка с модулями классов данных        - base.py - базовый класс данных        - person.py - классы данных о персоне        - wraps.py - классы данных оболочек для запросов/ответов    - handlers - Папка с модулями обработчиков запросов        - kwargs.py - обработчики для примера работы с `KwargsHandler.middleware`        - simple.py - обработчики для примера работы с `SimpleHandler.middleware`        - wraps.py - обработчики для примера работы с `WrapsKwargsHandler.middleware`    - middlewares - Папка с модулями для middlewares        - exceptions.py - классы исключений        - kwargs_handler.py - класс `KwargsHandler`        - simple_handler.py - класс `SimpleHandler`        - utils.py - вспомогательные классы и функции для middlewares        - wraps_handler.py - класс `WrapsKwargsHandler`    - requirements.txt - зависимости приложения    - run_kwargs.py - запуск с `KwargsHandler.middleware`    - run_simple.py - запуск c `SimpleHandler.middleware`    - run_wraps.py - запуск c `WrapsKwargsHandler.middleware`    - settings.py - константы с настройками приложения- Dockerfile - докерфайл для сборки образа

Код доступен на гитхаб: https://github.com/EvgeniyBurdin/api_service


2. json middlewares


middleware в aiohttp.web.Application() является оболочкой для обработчиков запросов.


Если в приложении используется middleware, то поступивший запрос сначала попадает в неё, и только потом передается в обработчик. Обработчик формирует и отдает ответ. Этот ответ снова сначала попадает в middleware и уже она отдает его наружу.


Если в приложении используются нескольно middleware, то каждая из них добавляет новый уровень вложенности.


Между middleware и обработчиком не обязательно должны передаваться "запрос" и "ответ" в виде web.Request и web.Response. Допускается передавать любые данные.


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


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


2.1. Простая middleware для json сервиса


Обычно, объявление обработчика запроса в приложении aiohttp.web.Application() выглядит, примерно, так:


from aiohttp import webasync def some_handler(request: web.Request) -> web.Response:    data = await request.json()    ...    text = json.dumps(some_data)    ...    return web.Response(text=text, ...)

Для доступа к данным обработчику необходимо "вытащить" из web.Request объект, который был передал в json. Обработать его, сформировать объект с данными для ответа. Закодировать ответ в строку json и отдать "наружу" web.Response (можно отдать и сразу web.json_response()).


2.1.1. Объявление обработчика


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


from aiohttp import webasync def some_handler(request: web.Request, data: Any) -> Any:    ...    return some_data

Каждый из обработчиков имеет два позиционных аргумента. В первый будет передан оригинальный экземпляр web.Request (на всякий случай), во второй уже готовый объект python, с полученными данными.


В примере, второй аргумент имеет такое объявление: data: Any. Имя у него может быть любым (как и у первого аргумента), а вот в аннотации лучше сразу указать тип объекта, который "ждет" обработчик. Это пожелание справедливо и для возврата.


То есть, в реальном коде, объявление обработчика может быть таким:


from aiohttp import webfrom typing import Union, Listasync def some_handler(    request: web.Request, data: Union[str, List[str]]) -> List[int]:    ...    return some_data

2.1.2. Класс SimpleHandler для middleware


Класс SimpleHandler реализует метод для самой middleware и методы, которые впоследствии помогут изменять/дополнять логику работы middleware (ссылка на код класса).


Остановлюсь подробнее только на некоторых.


2.1.2.1. Метод middleware

    @web.middleware    async def middleware(self, request: web.Request, handler: Callable):        """ middleware для json-сервиса.        """        if not self.is_json_service_handler(request, handler):            return await handler(request)        try:            request_body = await self.get_request_body(request, handler)        except Exception as error:            response_body = self.get_error_body(request, error)            status = 400        else:            # Запуск обработчика            response_body, status = await self.get_response_body_and_status(                request, handler, request_body            )        finally:            # Самостоятельно делаем дамп объекта python (который находится в            # response_body) в строку json.            text, status = await self.get_response_text_and_status(                request, response_body, status            )        return web.Response(            text=text, status=status, content_type="application/json",        )

Именно этот метод надо будет добавить в список middlewares в процессе создания приложения.


Например, так:


    ...    app = web.Application()    service_handler = SimpleHandler()    app.middlewares.append(service_handler.middleware)    ...

2.1.2.2. Метод для получения данных ответа с ошибкой

Так как у нас json сервис, то, желательно, чтобы ошибки во входящих данных (с кодом 400), и внутренние ошибки сервиса (с кодом 500), отдавались в формате json.


Для этого создан метод формирования "тела" для ответа с ошибкой:


    def get_error_body(self, request: web.Request, error: Exception) -> dict:        """ Отдает словарь с телом ответа с ошибкой.        """        return {"error_type": str(type(error)), "error_message": str(error)}

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


2.1.2.3. Метод запуска обработчика

В текущем классе он очень простой:


    async def run_handler(        self, request: web.Request, handler: Callable, request_body: Any    ) -> Any:        """ Запускает реальный обработчик, и возвращает результат его работы.        """        return await handler(request, request_body)

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


2.1.3. Примеры


Имеется такой обработчик:


async def some_handler(request: web.Request, data: dict) -> dict:    return data

Будем посылать запросы на url этого обработчика.


текст примеров...
2.1.3.1. Ответ с кодом 200

Запрос POST на /some_handler:


{    "name": "test",    "age": 25}

ожидаемо вернет ответ с кодом 200:


{    "name": "test",    "age": 25}

2.1.3.2. Ответ с кодом 400

Сделаем ошибку в теле запроса.


Запрос POST на /some_handler:


{    "name": "test", 111111111111    "age": 25}

Теперь ответ сервиса выглядит так:


{    "error_type": "<class 'json.decoder.JSONDecodeError'>",    "error_message": "Expecting property name enclosed in double quotes: line 2 column 21 (char 22)"}

2.1.3.3. Ответ с кодом 500

Добавим в код обработчика исключение (эмулируем ошибку сервиса).


async def handler500(request: web.Request, data: dict) -> dict:    raise Exception("Пример ошибки 500")    return data

Запрос POST на /handler500:


{    "name": "test",    "age": 25}

в ответ получит такое:


{    "error_type": "<class 'Exception'>",    "error_message": "Пример ошибки 500"}

2.2. middleware для "kwargs-обработчиков"


middleware из предыдущего раздела уже можно успешно использовать.


Но проблема дублирования кода в обработчиках не решена до конца.


Рассмотрим такой пример:


async def some_handler(request: web.Request, data: dict) -> dict:    storage = request.app["storage"]    logger = request.app["logger"]    user_id = request.match_info["user_id"]    # и т.д. и т.п...    return data

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


2.2.1. Объявление обработчика


Хотелось бы, чтобы обработчики объявлялись, например, так:


async def some_handler_1(data: dict) -> int:    # ...    return some_dataasync def some_handler_2(storage: StorageClass, data: List[int]) -> dict:    # ...    return some_dataasync def some_handler_3(    data: Union[dict, List[str]], logger: LoggerClass, request: web.Request) -> str:    # ...    return some_data

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


2.2.2. Вспомогательный класс ArgumentsManager


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


За регистрацию, хранение и "выдачу" таких сущностей отвечает класс ArgumentsManager. Он объявлен в модуле middlewares/utils.py (ссылка на код класса).


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


Звучит немного запутано, но на самом деле всё просто:


@dataclassclass RawDataForArgument:    request: web.Request    request_body: Any    arg_name: Optional[str] = Noneclass ArgumentsManager:    """ Менеджер для аргументов обработчика.        Связывает имя аргумента с действием, которое надо совершить для        получения значения аргумента.    """    def __init__(self) -> None:        self.getters: Dict[str, Callable] = {}    # Тело json запроса ------------------------------------------------------    def reg_request_body(self, arg_name) -> None:        """ Регистрация имени аргумента для тела запроса.        """        self.getters[arg_name] = self.get_request_body    def get_request_body(self, raw_data: RawDataForArgument):        return raw_data.request_body    # Ключи в request --------------------------------------------------------    def reg_request_key(self, arg_name) -> None:        """ Регистрация имени аргумента который хранится в request.        """        self.getters[arg_name] = self.get_request_key    def get_request_key(self, raw_data: RawDataForArgument):        return raw_data.request[raw_data.arg_name]    # Ключи в request.app ----------------------------------------------------    def reg_app_key(self, arg_name) -> None:        """ Регистрация имени аргумента который хранится в app.        """        self.getters[arg_name] = self.get_app_key    def get_app_key(self, raw_data: RawDataForArgument):        return raw_data.request.app[raw_data.arg_name]    # Параметры запроса ------------------------------------------------------    def reg_match_info_key(self, arg_name) -> None:        """ Регистрация имени аргумента который приходит в параметрах запроса.        """        self.getters[arg_name] = self.get_match_info_key    def get_match_info_key(self, raw_data: RawDataForArgument):        return raw_data.request.match_info[raw_data.arg_name]    # Можно добавить и другие регистраторы...

Регистрация имен аргументов выполняется при создании экземпляра web.Application():


# ...app = web.Application()arguments_manager = ArgumentsManager()# Регистрация имени аргумента обработчика, в который будут передаваться# данные полученные из json-тела запросаarguments_manager.reg_request_body("data")# Регистрация имени аргумента обработчика, в который будет передаваться# одноименный параметр запроса из словаря request.match_infoarguments_manager.reg_match_info_key("info_id")# В приложении будем использовать хранилище# (класс хранилища "взят с потолка" и здесь просто для примера)app["storage"] = SomeStorageClass(login="user", password="123")# Регистрация имени аргумента обработчика, в который будет передаваться# экземпляр хранилищаarguments_manager.reg_app_key("storage")# ...

Теперь экземпляр ArgumentsManager хранит информацию о возможных аргументах обработчиков. Он передается при создании экземпляра класса для middleware:


...service_handler = KwargsHandler(arguments_manager=arguments_manager)app.middlewares.append(service_handler.middleware)...

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


2.2.3. Класс KwargsHandler для middleware


Класс KwargsHandler является наследником SimpleHandler и расширяет его возможности тем, что позволяет создавать обработчики согласно требованию п.2.2.1.


В этом классе переопределяется один метод run_handler, и добавляется еще два make_handler_kwargs и build_error_message_for_invalid_handler_argument (ссылка на код класса).


2.2.3.1. Метод запуска обработчика

Переопределяется метод родительского класса:


    async def run_handler(        self, request: web.Request, handler: Callable, request_body: Any    ) -> Any:        """ Запускает реальный обработчик, и возвращает результат его работы.            (Этот метод надо переопределять, если необходима дополнительная            обработка запроса/ответа/исключений)        """        kwargs = self.make_handler_kwargs(request, handler, request_body)        return await handler(**kwargs)

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


2.2.3.2. Метод формирования словаря с именами аргументов и их значениями

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


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


Но у этого требования есть одно исключение. А именно, аргумент с экземпляром web.Request может иметь в сигнатуре обработчика любое имя, но он обязательно должен иметь аннотацию типом web.Request (например, r: web.Request или req: web.Request или request: web.Request). То есть, экземпляр web.Request "зарегистрирован" по умолчанию, и может быть использован в любом обработчике.


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


Метод build_error_message_for_invalid_handler_argument просто формирует строку с сообщением об ошибке. Он создан для возможности изменить сообщение на свой вкус.


2.2.4. Примеры


Сигнатуры методов такие:


async def create(    data: Union[dict, List[dict]], storage: dict,) -> Union[dict, List[dict]]:    # ...async def read(storage: dict, data: str) -> dict:    # ...async def info(info_id: int, request: web.Request) -> str:    # ...

Первые два обслуживают POST запросы, последний GET (просто, для примера)


текст примеров...
2.2.4.1. Метод /create

Запрос:


[    {        "name": "Ivan"    },    {        "name": "Oleg"    }]

Ответ:


[    {        "id": "5730bab1-9c1b-4b01-9979-9ad640ea5fc1",        "name": "Ivan"    },    {        "id": "976d821a-e871-41b4-b5a2-2875795d6166",        "name": "Oleg"    }]

2.2.4.2. Метод /read

Запрос:


"5730bab1-9c1b-4b01-9979-9ad640ea5fc1"

Ответ:


{    "id": "5730bab1-9c1b-4b01-9979-9ad640ea5fc1",    "name": "Ivan"}

Примечание: читайте данные с одним из UUID которые получили в предыдущем примере, иначе будет ответ с ошибкой 500 PersonNotFound.


2.2.4.3. Метод /info/{info_id}

Запрос GET на /info/123:


"any json"

Ответ:


"info_id=123 and request=<Request GET /info/123 >"

2.3. middleware c оболочками запроса/ответа и валидацией


Иногда, требования для api-сервиса включают в себя стандартизированные оболочки для запросов и ответов.


Например, тело запроса к методу create может быть таким:


{    "data": [        {            "name": "Ivan"        },        {            "name": "Oleg"        }    ],    "id": 11}

а ответ таким:


{    "success": true,    "result": [        {            "id": "9738d8b8-69da-40b2-8811-b33652f92f1d",            "name": "Ivan"        },        {            "id": "df0fdd43-4adc-43cd-ac17-66534529d440",            "name": "Oleg"        }    ],    "id": 11}

То есть, данные для запроса в ключе data а от ответа в result.


Имеется ключ id, который в ответе должен иметь такое же значение как и в запросе.


Ключ ответа success является признаком успешности запроса.


А если запрос закончился неудачно, то ответ может быть таким:


Запрос к методу read:


{    "data":  "ddb0f2b1-0179-44b7-b94d-eb2f3b69292d",    "id": 3}

Ответ:


{    "success": false,    "result": {        "error_type": "<class 'handlers.PersonNotFound'>",        "error_message": "Person whith id=ddb0f2b1-0179-44b7-b94d-eb2f3b69292d not found!"    },    "id": 3}

Уже представленные классы для json middleware позволяют добавить логику работы с оболочками в новый класс для middleware. Надо будет дополнить метод run_handler, и заменить (или дополнить) метод get_error_body.


Таким образом, в обработчики будут "прилетать" только данные, необходимые для их работы (в примере это значение ключа data). Из обработчиков будет возвращаться только положительный результат (значение ключа result). А исключения будет обрабатывать middleware.


Так же, если это необходимо, можно добавить и валидацию данных.


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


2.3.1. Класс данных pydantic.BaseModel


pydantic.BaseModel позволяет декларативно объявлять данные.


При создании экземпляра происходит валидация данных по их аннотациям (и не только). Если валидация провалилась поднимается исключение.


Небольшой пример:


from pydantic import BaseModelfrom typing import Union, Listclass Info(BaseModel):    foo: intclass Person(BaseModel):    name: str    info: Union[Info, List[Info]]kwargs = {"name": "Ivan", "info": {"foo": 0}}person = Person(**kwargs)assert person.info.foo == 0kwargs = {"name": "Ivan", "info": [{"foo": 0}, {"foo": 1}]}person = Person(**kwargs)assert person.info[1].foo == 1kwargs = {"name": "Ivan", "info": {"foo": "bar"}}  # <- Ошибка, str не intperson = Person(**kwargs)# Возникло исключение:# ...# pydantic.error_wrappers.ValidationError: 2 validation errors for Person# info -> foo#  value is not a valid integer (type=type_error.integer)# info#  value is not a valid list (type=type_error.list)

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


В аннотациях к полям мы можем использовать алиасы из typing.


Если в аннотации к полю присутствует класс-потомок pydantic.BaseModel, то данные "маппятся" и в него (и так с любой вложенностью хотя, на счет "любой" не проверял).


Провал валидации сопровождается довольно информативным сообщением об ошибке. В примере мы видим, что на самом деле было две ошибки: info.foo не int, и info не list, что соответствует аннотации и сопоставленному с ней значению.


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


2.3.1.1. Строгие типы

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


kwargs = {"name": "Ivan", "info": {"foo": "0"}}person = Person(**kwargs)assert person.info.foo == 0

То есть, имеем неявное приведение типов. И такое встречается не только с str->int (более подробно про типы pydantic см. в документации).


Приведение типов, в определенных ситуациях, может оказаться полезным, например строка с UUID -> UUID. Но, если приведение некоторых типов недопустимо, то в аннотациях надо использовать типы, наименование у которых начинается со Strict.... Например, pydantic.StrictInt, pydantic.StrictStr, и т.п...


2.3.1.2. Строгая сигнатура при создании экземпляра

Если, для определенных выше классов, попробовать выполнить такой пример:


kwargs = {"name": "Ivan", "info": {"foo": 0}, "bar": "BAR"}person = Person(**kwargs)

То создание экземпляра пройдет без ошибок.


Это тоже может оказаться не тем, что ожидаешь по умолчанию.


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


from pydantic import BaseModel, Extra, StrictInt, StrictStrfrom typing import Union, Listclass BaseApi(BaseModel):    class Config:        # Следует ли игнорировать (ignore), разрешать (allow) или        # запрещать (forbid) дополнительные атрибуты во время инициализации        # модели, подробнее:        # https://pydantic-docs.helpmanual.io/usage/model_config/        extra = Extra.forbidclass Info(BaseApi):    foo: StrictIntclass Person(BaseApi):    name: StrictStr    info: Union[Info, List[Info]]kwargs = {"name": "Ivan", "info": {"foo": 0}, "bar": "BAR"}person = Person(**kwargs)# ...# pydantic.error_wrappers.ValidationError: 1 validation error for Person# bar#   extra fields not permitted (type=value_error.extra)

Теперь все нормально, валидация провалилась.


2.3.2. Декоратор valdec.validate


Декоратор valdec.validate позволяет валидировать аргументы и/или возвращаемое значение функции или метода.


Можно валидировать только те аргументы, для которых указана аннотация.


Если у возврата нет аннотации, то считается что функция должна вернуть None (имеет аннотацию -> None:).


Определен декоратор как для обычных функций/методов:


from valdec.decorators import validate@validate  # Валидируем все аргументы с аннотациями, и возвратdef foo(i: int, s: str) -> int:    return i@validate("i", "s")  # Валидируем только "i" и "s"def bar(i: int, s: str) -> int:    return i

так и для асинхронных.


# Импортируем асинхронный вариантfrom valdec.decorators import async_validate as validate@validate("s", "return", exclude=True)  # Валидируем только "i"async def foo(i: int, s: str) -> int:    return int(i)@validate("return")  # Валидируем только возвратasync def bar(i: int, s: str) -> int:    return int(i)

2.3.2.1. Функции-валидаторы

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


Сигнатура функции-валидатора:


def validator(    annotations: Dict[str, Any],    values: Dict[str, Any],    is_replace: bool,    extra: dict) -> Optional[Dict[str, Any]]:

Аргументы:


  • annotations Словарь, который содержит имена аргументов и их аннотации.
  • values Словарь, который содержит имена аргументов и их значения.
  • is_replace управляет тем, что возвращает функция-валидатор, а именно возвращать отвалидированные значения или нет.
    • Если True, то функция должна вернуть словарь с именами отвалидированных аргументов и их значениями после валидации. Таким образом, например, если у аргумента была аннотация с наследником BaseModel и данные для него поступили в виде словаря, то они будут заменены на экземпляр BaseModel, и в декорируемой функции к ним можно будет обращаться "через точку".
    • Если параметр равен False, то функция вернет None, а декорируемая функция получит оригинальные данные (то есть, например, словарь так и останется словарем, а не станет экземпляром BaseModel).
  • extra Словарь с дополнительными параметрами.

По умолчанию, в декораторе validate используется функция-валидатор на основе pydantic.BaseModel.


В ней происходит следующее:


  • На основании словаря с именами аргументов и их аннотаций создается класс данных (потомок pydantic.BaseModel)
  • Создается экземпляр этого класса в который передается словарь с именами и значениями. В этот момент и происходит валидация.
  • Возвращает функция аргументы после валидации (которые уже буду содержать значения из созданного экземпляра), или ничего не возвращает, зависит от аргумента is_replace.

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


Функция-валидатор может быть реализована на основе любого валидирующего класса (в репозитарии valdec есть пример реализации на ValidatedDC). Но необходимо учесть следующее: далее в статье, я буду использовать потомков pydantic.BaseModel в аннотациях аргументов у обработчиков. Соответственно, при другом валидирующем классе, в аннотациях необходимо будет указывать потомков этого "другого" класса.


2.3.2.2. Настройка декоратора

По умолчанию, декоратор "подменяет" исходные данные на данные экземпляра валидирующего класса:


from typing import List, Optionalfrom pydantic import BaseModel, StrictInt, StrictStrfrom valdec.decorators import validateclass Profile(BaseModel):    age: StrictInt    city: StrictStrclass Student(BaseModel):    name: StrictStr    profile: Profile@validate("group")def func(group: Optional[List[Student]] = None):    for student in group:        assert isinstance(student, Student)        assert isinstance(student.name, str)        assert isinstance(student.profile.age, int)data = [    {"name": "Peter", "profile": {"age": 22, "city": "Samara"}},    {"name": "Elena", "profile": {"age": 20, "city": "Kazan"}},]func(data)

Обратите внимание на assert'ы.


Это работает и для возврата:


@validate  # Валидируем всёdef func(group: Optional[List[Student]] = None, i: int) -> List[Student]:    #...    return [        {"name": "Peter", "profile": {"age": 22, "city": "Samara"}},        {"name": "Elena", "profile": {"age": 20, "city": "Kazan"}},    ]

Здесь, несмотря на то, что в return явно указан список словарей, функция вернет список экземпляров Student (подмену выполнит декоратор).


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


from valdec.data_classes import Settingsfrom valdec.decorators import validate as _validatefrom valdec.validator_pydantic import validatorcustom_settings = Settings(    validator=validator,     # Функция-валидатор.    is_replace_args=False,   # Делать ли подмену в аргументах    is_replace_result=False, # Делать ли подмену в результате    extra={}                 # Дополнительные параметры, которые будут                             # передаваться в функцию-валидатор)# Определяем новый декораторdef validate_without_replacement(*args, **kwargs):    kwargs["settings"] = custom_settings    return _validate(*args, **kwargs)# Используем@validate_without_replacementdef func(group: Optional[List[Student]] = None, i: int) -> List[Student]:    #...    return [        {"name": "Peter", "profile": {"age": 22, "city": "Samara"}},        {"name": "Elena", "profile": {"age": 20, "city": "Kazan"}},    ]

И теперь func вернет список словарей, так как is_replace_result=False. И получит тоже список словарей, так как is_replace_args=False.


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


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


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


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


2.3.2.3. Еще раз про приведение типов

Рассмотрим такой пример применения декоратора:


from valdec.decorators import validate@validatedef foo(i: int):    assert isinstance(i, int)foo("1")

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


Как я уже говорил, по умолчанию, в декораторе validate, используется функция-валидатор на основе pydantic.BaseModel. В п.2.3.1.1. можно еще раз почитать про неявное приведение типов в этом классе.


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


from valdec.decorators import validatefrom pydantic import StrictInt@validatedef foo(i: StrictInt):    passfoo("1")# ...# valdec.errors.ValidationArgumentsError: Validation error# <class 'valdec.errors.ValidationError'>: 1 validation error for# argument with the name of:# i#  value is not a valid integer (type=type_error.integer).

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


Не забывайте про это.


2.3.2.4. Исключения

  • valdec.errors.ValidationArgumentsError "поднимается" если валидация аргументов функции потерпела неудачу
  • valdec.errors.ValidationReturnError если не прошел валидацию возврат

Само сообщение с описанием ошибки берется из валидирующего класса. В нашем примере это сообщение об ошибке от pydantic.BaseModel.


2.3.3. Базовый класс данных


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


Cначала обязательно определим базовый класс данных:


data_classes/base.py


from pydantic import BaseModel, Extraclass BaseApi(BaseModel):    """ Базовый класс данных для api.    """    class Config:        extra = Extra.forbid

2.3.4. Объявление обработчика


Класс для middleware, над созданием которого мы сейчас работаем, позволит объявлять обработчики, например, так:


from typing import List, Unionfrom valdec.decorators import async_validate as validatefrom data_classes.person import PersonCreate, PersonInfo@validate("data", "return")async def create(    data: Union[PersonCreate, List[PersonCreate]], storage: dict,) -> Union[PersonInfo, List[PersonInfo]]:    # ...    return result

Что здесь добавилось (по сравнению с обработчиками из прошлых глав):


  • декоратор validate валидирует поступившие данные и ответ, и "подменяет" их на экземпляры валидирующих классов
  • в аннотациях у данных указаны уже конкретные классы.

Про оболочки запросов/ответов обработчик ничего не знает, ему это и не надо.


Позволю себе небольшую ремарку: если в аргументах у обработчика нет экземпляра web.Request(), то он является обычной функцией, которую можно использовать не только в wep.Aplication(). На самом деле, даже если он там есть, эту функцию по-прежнему можно будет использовать в приложениях другого типа, если обеспечить совместимый с web.Request() экземпляр данных.


Соответственно, классы данных для этого обработчика могут быть такими:


data_classes/person.py


from uuid import UUIDfrom pydantic import Field, StrictStrfrom data_classes.base import BaseApiclass PersonCreate(BaseApi):    """ Данные для создания персоны.    """    name: StrictStr = Field(description="Имя.", example="Oleg")class PersonInfo(BaseApi):    """ Информация о персоне.    """    id: UUID = Field(description="Идентификатор.")    name: StrictStr = Field(description="Имя.")

2.3.5. Классы данных для оболочек


В самом начале п.2.3. были обозначены тебования к оболочкам запроса и ответа.


Для их выполнения создадим классы данных.


data_classes/wraps.py


from typing import Any, Optionalfrom pydantic import Field, StrictIntfrom data_classes.base import BaseApi_ID_DESCRIPTION = "Идентификатор запроса к сервису."class WrapRequest(BaseApi):    """ Запрос.    """    data: Any = Field(description="Параметры запроса.", default=None)    id: Optional[StrictInt] = Field(description=_ID_DESCRIPTION)class WrapResponse(BaseApi):    """ Ответ.    """    success: bool = Field(description="Статус ответа.", default=True)    result: Any = Field(description="Результат ответа.")    id: Optional[StrictInt] = Field(description=_ID_DESCRIPTION)

Эти классы будут использоваться в классе для middleware при реализации логики оболочек.


2.3.6. Класс WrapsKwargsHandler для middleware


Класс WrapsKwargsHandler является наследником KwargsHandler и расширяет его возможности тем, что позволяет использовать оболочки для данных запросов и ответов и их валидацию (ссылка на код класса).


В этом классе переопределяются два метода run_handler и get_error_body.


2.3.6.1. Метод запуска обработчика

Переопределяется метод родительского класса:


async def run_handler(        self, request: web.Request, handler: Callable, request_body: Any    ) -> dict:        id_ = None        try:            # Проведем валидацию оболочки запроса            wrap_request = WrapRequest(**request_body)        except Exception as error:            message = f"{type(error).__name__} - {error}"            raise InputDataValidationError(message)        # Запомним поле id для ответов        id_ = wrap_request.id        request[KEY_NAME_FOR_ID] = id_        try:            result = await super().run_handler(                request, handler, wrap_request.data            )        except ValidationArgumentsError as error:            message = f"{type(error).__name__} - {error}"            raise InputDataValidationError(message)        # Проведем валидацию оболочки ответа        wrap_response = WrapResponse(success=True, result=result, id=id_)        return wrap_response.dict()

Сначала мы проверим оболочку запроса. Исключение InputDataValidationError поднимется в следующих случаях:


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

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


Затем мы запоминаем wrap_request.id в request. Это необходимо для формирования ответа с ошибкой на текущий запрос (если она произойдет).


После этого вызывается обработчик, но для его входящих данных передается только wrap_request.data (напомню, что во wrap_request.data сейчас объект python в том виде, как он был получен из json). При этом, исключение InputDataValidationError поднимается если получено исключение valdec.errors.ValidationArgumentsError.


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


Все просто, но хотел бы обратить внимание на такой момент. Можно было бы обойтись без создания wrap_response, а сразу сформировать словарь (как это и будет сделано для ответа с ошибкой). Но, в случае успешного ответа мы не знаем что пришло в ответе от обработчика, это может быть, например, как список словарей, так и список экземпляров BaseApi. А на выходе из метода мы должны гарантированно отдать объект, готовый для кодирования в json. Поэтому, мы "заворачиваем" любые данные с результом во WrapResponse.result и уже из wrap_response получаем окончательный ответ для метода при помощи wrap_response.dict() (ссылка на документацию).


2.3.6.2. Метод для получения данных ответа с ошибкой

Заменяется метод родительского класса:


def get_error_body(self, request: web.Request, error: Exception) -> dict:        """ Формирует и отдает словарь с телом ответа с ошибкой.        """        result = dict(error_type=str(type(error)), error_message=str(error))        # Так как мы знаем какая у нас оболочка ответа, сразу сделаем словарь        # с аналогичной "схемой"        response = dict(            # Для поля id используется сохраненное в request значение.            success=False, result=result, id=request.get(KEY_NAME_FOR_ID)        )        return response

Здесь можно было бы применить и наследование (вызвать super() для получения result), но для наглядности я оставил так. Вы можете сделать как сочтете нужным.


2.3.7. Примеры


Сигнатуры методов такие:


@validate("data", "return")async def create(    data: Union[PersonCreate, List[PersonCreate]], storage: dict,) -> Union[PersonInfo, List[PersonInfo]]:    # ...@validate("data", "return")async def read(storage: dict, req: web.Request, data: UUID) -> PersonInfo:    # ...@validate("info_id")async def info(info_id: int, request: web.Request) -> Any:    return f"info_id={info_id} and request={request}"

Первые два обслуживают POST запросы, последний GET (просто, для примера)


текст примеров...
2.3.7.1. Метод /create

  • Запрос 1:

{    "data": [        {            "name": "Ivan"        },        {            "name": "Oleg"        }    ],    "id": 1}

Ответ:


{    "success": true,    "result": [        {            "id": "af908a90-9157-4231-89f6-560eb6a8c4c0",            "name": "Ivan"        },        {            "id": "f7d554a0-1be9-4a65-bfc2-b89dbf70bb3c",            "name": "Oleg"        }    ],    "id": 1}

  • Запрос 2:

{    "data": {        "name": "Eliza"    },    "id": 2}

Ответ:


{    "success": true,    "result": {        "id": "f3a45a19-acd0-4939-8e0c-e10743ff8e55",        "name": "Eliza"    },    "id": 2}

  • Запрос 3:

Попробуем передать в data невалидное значение


{    "data": 123,    "id": 3}

Ответ:


{    "success": false,    "result": {        "error_type": "<class 'middlewares.exceptions.InputDataValidationError'>",        "error_message": "ValidationArgumentsError - Validation error <class 'valdec.errors.ValidationError'>: 2 validation errors for argument with the name of:\ndata\n  value is not a valid dict (type=type_error.dict)\ndata\n  value is not a valid list (type=type_error.list)."    },    "id": 3}

2.3.7.2. Метод /read

  • Запрос 1:

{    "data": "f3a45a19-acd0-4939-8e0c-e10743ff8e55",    "id": 4}

Ответ:


{    "success": true,    "result": {        "id": "f3a45a19-acd0-4939-8e0c-e10743ff8e55",        "name": "Eliza"    },    "id": 4

  • Запрос 2:

Попробуем сделать ошибку в оболочке.


{    "some_key": "f3a45a19-acd0-4939-8e0c-e10743ff8e55",    "id": 5}

Ответ:


{    "success": false,    "result": {        "error_type": "<class 'middlewares.exceptions.InputDataValidationError'>",        "error_message": "ValidationError - 1 validation error for WrapRequest\nsome_key\n  extra fields not permitted (type=value_error.extra)"    },    "id": null}

2.3.7.3. Метод /info/{info_id}

  • Запрос GET на /info/123:

{}

Ответ:


{    "success": true,    "result": "info_id=123 and request=<Request GET /info/123 >",    "id": null}

3. О нереализованной документации


У обработчиков, которые используются с классом WrapsKwargsHandler, есть всё, чтобы автоматически собрать документацию. К ним более не надо ничего добавлять. Так как классы pydantic.BaseModel позволяют получать json-schema, то остается только сделать скрипт сборки документации (если кратко, то надо: перед запуском приложения пройтись по всем обработчикам и у каждого заменить докстринг на swagger-описание, построенное на основе уже имеющегося докстринга и json-схем входящих данных и возврата).


И я эту документацию собираю. Но не стал рассказывать про это в статье. Причина в том, что я не нашел библиотеки для swagger и aiohttp, которая бы работала полностью как надо (или я не нашел способа заставить работать как надо).


Например, библиотека aiohttp-swagger некорректно отображает аргумент (в областях с примерами), если в аннотации есть алиас Union.


Библиотека aiohttp-swagger3, напротив, все прекрасно показывает, но не работает если в приложении есть sub_app.


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


4. Заключение


В итоге у нас имеются три класса для json middleware с разными возможностями. Любой из них можно изменить под свои нужды. Или создать на их основе новый.


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


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


Спасибо за уделенное время. Буду рад замечаниям, и уточнениям.


При публикации статьи использовал MarkConv

Подробнее..

CQS (CQRS) со своим блэкджеком

01.03.2021 00:05:02 | Автор: admin
Command-query separation (CQS) это разделение методов на read и write.

Command Query Responsibility Segregation (CQRS) это разделение модели на read и write. Предполагается в одну пишем, с нескольких можем читать. М масштабирование.

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

Размышления навеяны этой статьей Паттерн CQRS: теория и практика в рамках ASP.Net Core 5 и актуальны для анемичной модели. Для DDD все по-другому.

Историческая справка


Начать пожалуй стоит с исторической справки. Сначала было как-то так:

public interface IEntityService{    EntityModel[] GetAll();    EntityModel Get(int id);    int Add(EntityModel model);    void Update(EntityModel model);    void Delete(int id);}public interface IEntityRepository{    Entity[] GetAll();    Entity Get(int id);    int Add(Entity entity);    void Update(Entity entity);    void Delete(int id);}

С появлением CQS стало так:

public class GetEntitiesQuery{     public EntityModel[] Execute() { ... }}public class GetEntityQuery{     public EntityModel Execute(int id) { ... }}public class AddEntityCommand{     public int Execute(EntityModel model) { ... }}public class UpdateEntityCommand{     public void Execute(EntityModel model) { ... }}public class DeleteEntityCommand{     public void Execute(int id) { ... }}

Эволюция


Как видим, два потенциальных god-объекта разделяются на много маленьких и каждый делает одну простую вещь либо читает данные, либо обновляет. Это у нас CQS. Если еще и разделить на два хранилища (одно для чтения и одно для записи) это будет уже CQRS. Собственно что из себя представляет например GetEntityQuery и UpdateEntityCommand (здесь и далее условный псевдокод):

public class GetEntityQuery{    public EntityModel Execute(int id)    {        var sql = "SELECT * FROM Table WHERE Id = :id";        using (var connection = new SqlConnection(...connStr...))        {             var command = connection.CreateCommand(sql, id);             return command.Read();        }    }}public class UpdateEntityCommand{    public void Execute(EntityModel model)    {        var sql = "UPDATE Table SET ... WHERE Id = :id";        using (var connection = new SqlConnection(...connStr...))        {             var command = connection.CreateCommand(sql, model);             return command.Execute();        }    }}

Теперь к нам приходит ORM. И вот тут начинаются проблемы. Чаще всего сущность сначала достается из контекста и только затем обновляется. Выглядит это так:

public class UpdateEntityCommand{    public void Execute(EntityModel model)    {        var entity = db.Entities.First(e => e.Id == model.Id); // <-- опа, а что это? query?        entity.Field1 = model.Field1;        db.SaveChanges();    }}

Да, если ORM позволяет обновлять сущности сразу, то все будет хорошо:

public class UpdateEntityCommand{    public void Execute(EntityModel model)    {        var entity = new Entity { Id = model.Id, Field1 = model.Field1 };        db.Attach(entity);        db.SaveChanges();    }}

Так а что делать, когда надо достать сущность из базы? Куда девать query из command? На ум приходит сделать так:

public class GetEntityQuery{    public Entity Execute(int id)    {        return db.Entities.First(e => e.Id == model.Id);    }}public class UpdateEntityCommand{    public void Execute(Entity entity, EntityModel model)    {        entity.Field1 = model.Field1;        db.SaveChanges();    }}

Хотя я встречал еще такой вариант:
public class UpdateEntityCommand{    public void Execute(EntityModel model)    {        var entity = _entityService.Get(model.Id); // )))         entity.Field1 = model.Field1;        db.SaveChanges();    }}public class EntityService{    public Entity Get(int id)    {        return db.Entities.First(e => e.Id == model.Id);    }}

Просто перекладываем проблему из одного места в другое. Эта строчка не перестает от этого быть query.

Ладно, допустим остановились на варианте с GetEntityQuery и UpdateEntityCommand. Там хотя бы query не пытается быть чем-то другим. Но куда это все сложить и откуда вызывать? Пока что есть одно место это контроллер, выглядеть это будет примерно так:

public class EntityController{    [HttpPost]    public EntityModel Update(EntityModel model)    {        var entity = new GetEntityQuery().Execute(model.Id);                new UpdateEntityCommand().Execute(entity, model);        return model;    }}

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

public class EntityController{    [HttpPost]    public EntityModel Update(EntityModel model)    {        var entity = new GetEntityQuery().Execute(model.Id);                new UpdateEntityCommand().Execute(entity, model);                _notifyService.Notify(NotifyType.UpdateEntity, entity); // <-- А это query или command?        return model;    }}

В итоге контроллер у нас начинает толстеть.

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


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

image

И приведу пример одной реальной задачи: по гео-координатам получить погоду в заданной точке. Есть внешний условно-бесплатный сервис. Поэтому требуется оптимизация в виде кэша. Кэш не простой. Хранится в базе данных. Алгоритм выборки: сначала идем в кэш, если там есть точка в радиусе 10 км от заданной и в пределах 1 часа по времени, то возвращаем погоду из кэша. Иначе идем во внешний сервис. Здесь и query, и command, и обращение к внешнему сервису все-в-одном.

Решение


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

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

Одна бизнес-история это один из блоков на диаграмме IDEF0. Она может иметь вложенные бизнес-истории, как блок IDEF0 может иметь вложенные блоки. И она может обращаться к искомым понятиям CQS это к Query и Command.

Таким образом, код из контроллера мы переносим в Story:
public class EntityController{    [HttpPost]    public EntityModel Update(EntityModel model)    {        return new UpdateEntityStory().Execute(model);    }}public class UpdateEntityStory{    public EntityModel Execute(EntityModel model)    {        var entity = new GetEntityQuery().Execute(model.Id);                new UpdateEntityCommand().Execute(entity, model);                _notifyService.Notify(NotifyType.UpdateEntity, entity);        return model;    }}

И контроллер остается тонким.

Данная UpdateEntityStory инкапсулирует в себе законченный конкретный бизнес-процесс. Ее можно целиком использовать в разных местах (например в вызовах API). Она легко подвергается тестированию и никоим образом не ограничивает использование моков/фейк-объектов.

Диаграмму IDEF0/BPMN можно разбросать по таким Story, что даст более легкий вход в проект. Все изменения можно будет уложить в следующий процесс: сначала меняем документацию (диаграмму IDEF0) затем дописываем тесты а уже в конце дописываем бизнес-код. Можно наоборот, по этим Story автоматически построить документацию в виде IDEF0/BPMN диаграмм.

Но чтобы получить более стройный подход, необходимо соблюдать некоторые правила:
1. Story входная точка бизнес-логики. Именно на нее ссылается контроллер.
2. Но внутрь Story не должны попадать такие вещи как HttpContext и тому подобное. Потому что тогда Story нельзя будет легко вызывать в другом контексте (например в hangfire background job или обработчике сообщения из очереди там не будет никаких HttpContext).
3. Входящие параметры Story опциональны. Story может возвращать что-либо или не возвращать ничего (хотя для сохранения тестируемости хорошо бы она что-нибудь возвращала).
4. Story может работать как с бизнес-сущностями, так и с моделями и DTO. Может внутри вызывать соответствующие мапперы и валидаторы.
5. Story может вызывать другие Story.
6. Story может вызывать внешние сервисы. Хотя внешний вызов можно тоже оформить как Story. Об этом ниже с нашим сервисом погоды.
7. Story не может напрямую обращаться к контексту базы данных. Это область ответственности Query и Command. Если нарушить это правило, все запросы и команды вытекут наружу и размажутся по всему проекту.
8. На Story можно навешивать декораторы. Об этом тоже ниже.
9. Story может вызывать Query и Command.
10. Разные Story могут переиспользовать одни и те же Query и Command.
11. Query и Command не могут вызывать другие Story, Query и Command.
12. Только Query и Command могут обращаться к контексту базы данных.
13. В простых случаях можно обойтись без Story и из контроллеров вызывать сразу Query или Command.

Теперь тот самый пример с сервисом погоды:
public class GetWeatherStory{    public WeatherModel Execute(double lat, double lon)    {        var weather = new GetWeatherQuery().Execute(lat, lon, DateTime.NowUtc);        if (weather == null)        {             weather = _weatherService.GetWeather(lat, lon);             new AddWeatherCommand().Execute(weather);        }        return weather;    }}public class GetWeatherQuery{    public WeatherModel Execute(double lat, double lon, DateTime currentDateTime)    {        // Нативный SQL запрос поиска записи в таблице по условиям:        // * в радиусе 10 км от точки lat/lon        // * в пределах 1 часа от currentDateTime        // С использованием расширений PostGis или аналогичных        return result;    }}public class AddWeatherCommand{    public void Execute(WeatherModel model)    {        var entity = new Weather { ...поля из model... };        db.Weathers.Add(entity);        db.SaveChanges();    }}public class WeatherService{    public WeatherModel GetWeather(double lat, double lon)    {        var client = new Client();        var result = client.GetWeather(lat, lon);        return result.ToWeatherModel(); // маппер из dto в нашу модель    }}


Декораторы


И в заключении о декораторах. Чтобы Story стали более гибкими необходимо cложить их в DI контейнер / mediator. И добавить возможность декорировать их вызов.

Сценарии:

1. Запускать Story внутри транзакции scoped контекста базы данных:
public class EntityController{    [HttpPost]    public EntityModel Update(EntityModel model)    {        return _mediator.Resolve<UpdateEntityStory>().WithTransaction().Execute(model);    }}// или[Transaction]public class UpdateEntityStory{    ...}

2. Кэшировать вызов
public class EntityController{    [HttpPost]    public ResultModel GetAccessRights()    {        return _mediator            .Resolve<GetAccessRightsStory>()            .WithCache("key", 60)            .Execute();    }}// или[Cache("key", 60)]public class GetAccessRightsStory{    ...}

3. Политика повторов
public class GetWeatherStory{    public WeatherModel Execute(double lat, double lon)    {        var weather = new GetWeatherQuery().Execute(lat, lon, DateTime.NowUtc);        if (weather == null)        {             weather = _mediator                 .Resolve<GetWeatherFromExternalServiceStory>()                 .WithRetryAttempt(5)                 .Execute(lat, lon);             _mediator.Resolve<AddWeatherCommand>().Execute(weather);        }        return weather;    }}// или[RetryAttempt(5)]public class GetWeatherFromExternalServiceStory{    ...}

4. Распределенная блокировка
public class GetWeatherStory{    public WeatherModel Execute(double lat, double lon)    {        var weather = new GetWeatherQuery().Execute(lat, lon, DateTime.NowUtc);        if (weather == null)        {             weather = _mediator                 .Resolve<GetWeatherFromExternalServiceStory>()                 .WithRetryAttempt(5).                 .Execute(lat, lon);             _mediator.Resolve<AddWeatherStory>()                 .WithDistributedLock(LockType.RedLock, "key", 60)                 .Execute(weather);        }        return weather;    }}// или[DistributedLock(LockType.RedLock, "key", 60)]public class AddWeatherStory{    ...}

И тому подобное.
Подробнее..

Перевод Как подключить OLED дисплей к Raspberry Pi Pico быстрый старт

01.03.2021 14:22:08 | Автор: admin

У Raspberry Pi Pico несколько функциональных ограничений, включая отсутствие модуля беспроводной связи. Но, в целом, это отличная плата для реализации самых разных проектов. Одно из ее достоинств простота подключения дисплея. Она позволяет работать с Pico Display или Pico Explorer Base, но, как оказалось, есть и более недорогие альтернативы.

Под катом инструкция по подключению OLED-экрана с диагональю всего 0,96 дюйма. Для этого нужно немного попаять и написать небольшую программу на MicroPython. Подключение экрана производится по I2C-интерфейсу. Все очень просто: с задачей справится даже ребенок. В целом, эта инструкция для тех, кто только начинает знакомство с платой. Опытные пользователи и так знают, что делать. Если этот пост окажется востребованным, то мы будем регулярно публиковать несложные руководства подобного рода. Что же, давайте приступим.

Что нужно для работы?

  • Raspberry Pi Pico с MicroPython.
  • 4 Female to Female соединительных провода.
  • I2C OLED-экран с разрешением 128*64

Как подключить OLED-экран к малинке


Схема подключения очень простая.


  • Подключаем GND контакт экрана к GND платы.
  • Подключаем VDD к VCC 3V3.
  • Подключаем SCK / SCL и I2C0 SCL.
  • Подключаем SDA к I2C0 SDA.
  • Подключаем плату к компьютеру и открываем Thonny.Теперь нужно установить библиотеку для того, чтобы Python мог взаимодействовать с экраном.

  • Выбираем Tools > Manage Packages для получения доступа к библиотекам Python.

  • Набираем ssd1306 в поисковой строке и ищем.


  • Выбираем найденный модуль micropython-ssd1306 в списке результатов и устанавливаем, после чего возвращаемся в основной интерфейс.


Программное подключение OLED-экрана к малинке


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

Сначала импортируем Pin и I2C-классы. Они используются для взаимодействия с экраном, подключенным к GPIO и Pico.

from machine import Pin, I2C

Импортируем библиотеку для экрана

from ssd1306 import SSD1306_I2C

Создаем объект, i2c, для активации l2C-канала и задаем частоту подключения.

i2c=I2C(0,sda=Pin(0), scl=Pin(1), freq=400000)

Создаем еще один объект, oled, для взаимодействия с OLED -экраном. У него три параметра: высота, ширина экрана и характеристики подключения l2C.

oled = SSD1306_I2C(128, 64, i2c)

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

oled.text("Tom's Hardware", 0, 0)

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

oled.show()

В итоге наш код должен выглядеть вот так:

from machine import Pin, I2Cfrom ssd1306 import SSD1306_I2C i2c=I2C(0,sda=Pin(0), scl=Pin(1), freq=400000)oled = SSD1306_I2C(128, 64, i2c) oled.text("Tom's Hardware", 0, 0)oled.show()


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

Подробнее..

Trace, Info, Warning, Error, Fatal кто все эти люди..?

01.03.2021 16:18:41 | Автор: admin

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

Info: все ожидаемые события, учет которых запланирован.
Warning: неожиданные/подозрительные события - иначе говоря аномалии, после которых еще возможно продолжение работы приложения.
Error: событие, после которого невозможно дальнейшее выполнение программы.
Fatal: событие, требующее по-настоящему немедленного вмешательства.

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

"Продолжить работу"

Трактовать возможно продолжить работу можно очень по разному. Скажем, на экране пользователя можно настроить любимый цвет рамки вокруг экрана: пусть будет розовый. Если по каким-то причинам хранилище, где мы держим эти настройки цвета было недоступно и мы не можем отобразить этот цвет - можно ли это считать как возможно продолжить или это катастрофа? К сожалению, я еще ни разу не встречал хорошего универсального формального критерия, чтобы четко можно было разделять "катастрофа-терпимо". А раз не можешь остановить - значит нужно направить. Потому я предлагаю инвертировать споры об это неправильный уровень: если в коде написано, что отсутствие цвета - это Error - значит программист Вам говорит, что этот цвет чертовски важен в этом домене - возможно, именно этот цвет сигнализирует, что сейчас надо продавать акции на миллионы долларов, и наоборот. Соответственно, чтение кода немного меняется: когда видите место, где из-за какой-то на Ваш взгляд ерунды прерывается выполнение, вопрос, который должен возникать автору, Почему ты считаешь, что это так важно? вместо Истинно тебе говорю - ты программируешь неправильно!.

Схожая инверсия может помочь в вечных спорах на тему это исключительная ситуация или нет. Опять же, все довольно просто. Важна не техническая составляющая: нет соединения к базе - это исключение, а серверу прислали неправильный id - так это ожидаемо. Важно то,чья это будет головная больи как ее можно избежать или хотя бы минимизировать урон. Чьи планы на вечер пятницы пойдут к черту из-за того, что это сломалось? Если Вашим сервисом пользуются приложения, которые вне Вашего контроля, то Вам действительно плевать на то, что они присылают некорректные id и у них там что-то идет не так. Если Ваше приложение - это инструмент для управления базой данный - наподобие Sql Server Magement Studio - очевидно, что отсутствие доступа к базе - не Ваша печаль. А если Вашим сервисом пользуются приложения, за которые Вы же и в ответе - то это Ваши неприятности в конечном счете. Вопрос лишь в том, как и когда Вы об этом узнаете - быстро из сработавшей сигнализации или от звонка злого как черт владельца бизнеса, которому Вы пишете софт. А также вопрос в том, как дешево, надежно и сердито эту сигнализацию наладить.

"Error"

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

Жаль, что нет комикса о том, как программист ловит льва в пустыне - он здесь был бы очень уместенЖаль, что нет комикса о том, как программист ловит льва в пустыне - он здесь был бы очень уместен

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

Ошибка валидации имя-фамилии - это несомненно уровень Info - мы ждем, что пользователь будет норовить записать все что угодно, а мы - пресекать это. Ну и записывать все те разы, когда пользователь был неправ. Но от этого ошибки не перестают бесить людей, которые их видят. То, что напрямую связано с людьми и их UX - важно; и неплохо бы присматривать за этим, не допускать, чтобы сообщения об этом тонули в километрах унылых Info записей Пользователь такой-то залогинился. Иными словами, чтобы устранить путаницу, хочется иметь уровни Error+ и Info+.

Я предвижу восклицания Так погодите, ведь крэш приложения - это недопустимо! Надо сразу действовать! Это Fatal уровень!" На это я неспеша прикурю воображаемую сигарету, затянусь и задумчиво отвечу: Ну всех ведь все равно не спасти...". Ладно, я не курю, но, думаю, образ понятен. Появление сообщения Fatal должно быть эквивалентом запуска тревоги воздушной угрозы, когда в офисе разработки врубается сирена и это жуткое красное аварийное освещение. Вот честно, Вы именно так реагируете на то, что у кого-то из бухгалтерии на экране, который раз в сто лет запускают, упало приложение? Вполне может быть нормально, что у Вас сейчас даже и нет подобной ситуации, где уровень Fatal - согласно такой системы определений - нужен. Так вот трюк в том, чтобы не блокировать возможность добавить обработку такой потенциальной ситуации в будущем, забивая сейчас Fatal уровень сообщениями, которым важностьError+ в самый раз.

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

Info
Info+
Warning
Error
Error+
Fatal

Плюсовые уровни можно легко организовать в Вашей любимой библиотеке логгирования расширив ее существующие методы Error/Info, которые бы просто унифицировано добавляли какой-то хэштэг в обычные сообщения, скажем #IMPORTANT.

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

Подробнее..

Перевод Как я сократил время загрузки GTA Online на 70

01.03.2021 16:18:41 | Автор: admin
GTA Online. Многопользовательская игра, печально известная медленной загрузкой. Недавно я вернулся, чтобы завершить несколько ограблений и был потрясён, что она загружается настолько же медленно, как и в день своего выпуска, 7 лет назад.

Пришло время докопаться до сути.

Разведка


Сначала я хотел проверить, вдруг кто-то уже решил проблему. Но нашёл только рассказы о великой сложности игры, из-за чего она так долго загружается, истории о том, что сетевая p2p-архитектура мусор (хотя это не так), некоторые сложные способы загрузки в сюжетный режим, а потом в одиночную сессию, и ещё пару модов, чтобы скипнуть видео с логотипом R* во время загрузки. Ещё немного почитав форумы, я узнал, что можно сэкономить колоссальные 10-30 секунд, если использовать все эти способы вместе!

Тем временем на моём компе

Бенчмарк


Story mode load time:  ~1m 10sOnline mode load time: ~6m flatStartup menu disabled, time from R* logo until in-game (social club login time isn't counted).Old but decent CPU:   AMD FX-8350Cheap-o SSD:          KINGSTON SA400S37120GWe have to have RAM:  2x Kingston 8192 MB (DDR3-1337) 99U5471Good-ish GPU:         NVIDIA GeForce GTX 1070

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

Я (не) одинок


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



Я немного поискал информацию о тех ~20% счастливчиках, которые загружаются быстрее трёх минут, и нашёл несколько бенчмарков с топовыми игровыми ПК и временем загрузки онлайн-режима около двух минут. Я бы кого-нибудь убил хакнул за такой комп! Действительно похоже на железячную проблему, но что-то не складывается

Почему у них сюжетный режим по-прежнему загружается около минуты? (кстати, при загрузке с M.2 NVMe не учитывались видео с логотипами). Кроме того, загрузка из сюжетного режима в онлайн занимает у них всего минуту, в то время как у меня около пяти. Я знаю, что их железо гораздо лучше, но не в пять же раз.

Высокоточные измерения


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



Почти минута уходит на загрузку общих ресурсов, которые нужны и для сюжетного режима, и для онлайна (почти наравне с топовыми ПК), затем GTA в течение четырёх минут полностью нагружает одно ядро CPU, больше ничего не делая.

Использование диска? Нет! Использование сети? Есть немного, но через несколько секунд падает в основном до нуля (кроме загрузки вращающихся информационных баннеров). Использование GPU? Ноль. Память? Вообще ничего

Что это, майнинг биткоинов или что-то такое? Чую здесь код. Очень плохой код.

Единственный поток


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

Что странно, так это способ использования CPU. Я ожидал огромное количество операций чтения с диска или массу сетевых запросов, чтобы организовать сеансы в сети p2p. Но такое? Вероятно, здесь какая-то ошибка.

Профилирование


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

Итак, добро пожаловать в образцы стека (stack sampling). Для приложений с закрытым исходным кодом есть только такой вариант. Сбросьте стек запущенного процесса и местоположение указателя текущей инструкции, чтобы построить дерево вызовов в заданные интервалы. Затем наложите их и получите статистику о том, что происходит. Я знаю только один профилировщик, который может проделать это под Windows. И он не обновлялся уже более десяти лет. Это Люк Stackwalker! Кто-нибудь, пожалуйста, подарите Люку немножко любви :)



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

Вниз по кроличьей норе


Позаимствовав у моего друга совершенно законную копию стандартного дизассемблера (нет, я действительно не могу его себе позволить когда-нибудь освою гидру), я пошёл разбирать GTA.



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

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

Проблема первая: это strlen?!


Дальнейший разбор дампа выявил один из адресов с некоей меткой strlen, которая откуда-то берётся! Спускаясь вниз по стеку вызовов, предыдущий адрес помечен как vscan_fn, и после этого метки заканчиваются, хотя я вполне уверен, что это sscanf.

Куда ж без графика

Он что-то парсит. Но что? Логический разбор займёт целую вечность, поэтому я решил сбросить некоторые образцы из запущенного процесса с помощью x64dbg. Через несколько шагов отладки выясняется, что это JSON! Он парсит JSON. Колоссальные десять мегабайт JSON'а с записями 63 тыс. предметов.

...,{    "key": "WP_WCT_TINT_21_t2_v9_n2",    "price": 45000,    "statName": "CHAR_KIT_FM_PURCHASE20",    "storageType": "BITFIELD",    "bitShift": 7,    "bitSize": 1,    "category": ["CATEGORY_WEAPON_MOD"]},...

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

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

10 мегабайт? В принципе, не так уж и много. Хотя sscanf используется не самым оптимальным образом, но, конечно, это не так уж плохо? Что ж



Да, такая процедура займёт некоторое время Честно говоря, я понятия не имел, что большинство реализаций sscanf вызывают strlen, поэтому не могу винить разработчика, который написал это. Я бы предположил, что он просто сканировал байт за байтом и мог остановиться на NULL.

Проблема вторая: давайте использовать хэш-массив?


Оказывается, второго преступника вызывают сразу за первым. Даже в одной и той же конструкции if, как видно из этой уродливой декомпиляции:



Все метки мои, и я понятия не имею, как на самом деле называются функции/параметры.

Вторая проблема? Сразу после разбора элемента он хранится в массиве (или встроенном списке C++? не уверен). Каждая запись выглядит примерно так:

struct {    uint64_t *hash;    item_t   *item;} entry;

А перед сохранением? Он проверяет весь массив, сравнивая хэш каждого элемента, есть он в списке или нет. С 63 тыс. записей это примерно (n^2+n)/2 = (63000^2+63000)/2 = 1984531500, если я не ошибаюсь в расчётах. И это в основном бесполезные проверки. У вас есть уникальные хэши, почему не использовать хэш-карту.



Во время реверс-инжиниринга я назвал его hashmap, но это явно не_hashmap. И дальше ещё интереснее. Этот хэш-массив-список пуст перед загрузкой JSON. И все элементы в JSON уникальны! Им даже не нужно проверять, есть они в списке или нет! У них даже есть функция прямой вставки элементов! Просто используйте её! Серьёзно, ну ребята, что за фигня!?

PoC


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

План такой. 1. Написать .dll, 2. внедрить её в GTA, 3. зацепить некоторые функции, 4. ???, 5. профит. Всё предельно просто.

Проблема с JSON нетривиальная, я не могу реально заменить их парсер. Более реалистичным кажется заменить sscanf на тот, который не зависит от strlen. Но есть ещё более простой способ.

  • зацепить strlen
  • подождать длинной строки
  • закэшировать начало и длину
  • если поступит ещё вызов в пределах диапазона строки, вернуть закэшированное значение

Что-то вроде такого:

size_t strlen_cacher(char* str){  static char* start;  static char* end;  size_t len;  const size_t cap = 20000;  // if we have a "cached" string and current pointer is within it  if (start && str >= start && str <= end) {    // calculate the new strlen    len = end - str;    // if we're near the end, unload self    // we don't want to mess something else up    if (len < cap / 2)      MH_DisableHook((LPVOID)strlen_addr);    // super-fast return!    return len;  }  // count the actual length  // we need at least one measurement of the large JSON  // or normal strlen for other strings  len = builtin_strlen(str);  // if it was the really long string  // save it's start and end addresses  if (len > cap) {    start = str;    end = str + len;  }  // slow, boring return  return len;}


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

char __fastcall netcat_insert_dedupe_hooked(uint64_t catalog, uint64_t* key, uint64_t* item){  // didn't bother reversing the structure  uint64_t not_a_hashmap = catalog + 88;  // no idea what this does, but repeat what the original did  if (!(*(uint8_t(__fastcall**)(uint64_t*))(*item + 48))(item))    return 0;  // insert directly  netcat_insert_direct(not_a_hashmap, key, &item);  // remove hooks when the last item's hash is hit  // and unload the .dll, we are done here :)  if (*key == 0x7FFFD6BE) {    MH_DisableHook((LPVOID)netcat_insert_dedupe_addr);    unload();  }  return 1;}

Полный исходный код PoC здесь.

Результаты


Ну и как оно работает?

Original online mode load time:        ~6m flatTime with only duplication check patch: 4m 30sTime with only JSON parser patch:       2m 50sTime with both issues patched:          1m 50s(6*60 - (1*60+50)) / (6*60) = 69.4% load time improvement (nice!)

Да, чёрт возьми, получилось! :))

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

Краткое содержание


  • При запуске GTA Online есть узкое место, связанное с однопоточным вычислением
  • Оказалось, GTA изо всех сил пытается распарсить 1-мегабайтный файл JSON
  • Сам парсер JSON плохо сделан/наивен и
  • После парсинга происходит медленная процедура удаления дублей

R*, пожалуйста, исправьте


Если информация каким-то образом дойдёт до инженеров Rockstar, то проблему можно решить в течение нескольких часов силами одного разработчика. Пожалуйста, ребята, сделайте что-нибудь с этим :<

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

ty <3
Подробнее..

Перевод Искусственный интеллект обновит устаревшее программное обеспечение за вас

02.03.2021 00:19:35 | Автор: admin
Инструменты IBM на основе искусственного интеллекта дают инженерам возможность исследовать способы применения устаревшего корпоративного ПО.

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



Последние проекты IBM под названием Mono2Micro и Application Modernization Accelerator (AMA) предоставляют архитекторам приложений инструменты для обновления устаревших приложений и повторного их применения. По словам Ника Фуллера, директора по гибридным облачным сервисам в исследовательской лаборатории IBM Research, эти инициативы позволяют приблизить момент, когда ИИ сможет автоматически перевести программу с COBOL на Java.

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

А пока передовые инструменты IBM на основе искусственного интеллекта предлагают некоторые новые возможности. Что касается Mono2Micro, этот инструмент сначала анализирует старый код, чтобы выявить все скрытые связи внутри него, например, различные компоненты в базовой бизнес-логике, содержащие многочисленные вызовы и связи друг с другом. Самостоятельное выполнение такой задачи было бы для архитектора приложений очень сложным и затратным по времени.

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

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

Целью инструментария AMA является как анализ, так и рефакторинг ранее разработанных приложений, написанных на устаревших языках (COBOL, PL/I). Что касается инструментария AMA, он сочетает статический анализ исходного кода с пониманием структуры приложения для создания графа, который представляет устаревшее приложение. При использовании совместно с методами глубокого обучения этот подход на основе графов облегчает сохранение данных.


Изображение: IBM Research
Изображение интерфейса Mono2Micro

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

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

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

Фуллер отмечает: На этом этапе вы еще не можете выдохнуть, но 70% работы позади, а значит, вы стали гораздо ближе к рефакторирнгу важного приложения в архитектуру микросервисов.
Подробнее..

Популярность BPM в разных жанрах музыки. Анализ скорости исполнения 500 лучших песен

02.03.2021 02:04:15 | Автор: admin

Несколько лет назад, занимался изучением теории музыки, продавал и писал аудио-инструментал для аренды или заказов. Изначально, процесс явно творческий, но вскоре, мой интерес к коммерческой части превысил и возник вопрос: В каком же темпе создавать ритм музыки?.

Мною была замечена тенденция вариаций темпа популярных песен одного жанра, поэтому идея анализа крупной выборки лучших композиций, для определения популярного [часто: самого продаваемого] диапазонатемпа исполнения, не покидала с тех пор

BPM [в музыке] показатель, для определения скорости исполнения композиции, путём измерения количества тактовых долей в минуту.


1: Пролог

Устанавливаем Matplotlibи Pandas с необходимыми зависимостями через pip-менеджер в консоли/терминале.

python -m pip install -V matplotlib и pip install pandaspython -m pip install -V matplotlib и pip install pandas

Создаём директорию, а потом виртуальное окружение для проекта. После, подключаем библиотеки в IDE [в моём случае: PyCharm].

File Settings Project: [...] Python InterpreterFile Settings Project: [...] Python Interpreter

2: BPM

BPM будем вычислять через функцию Detect tempo в FL Studio и через сайт tunebat.com

ПКМ по верхней левой иконке на звуковой дорожке Detect tempo Выбрать диапазонПКМ по верхней левой иконке на звуковой дорожке Detect tempo Выбрать диапазон

3: DataSet

Начинаем создание DataSetа [выборки-коллекции данных] в Excel, для каждого жанра. Экспортируем в CSV-формат с настройками разделителя запятой. Следующие CSV-файлы создавал в IDE, так удобнее. Выборки перемещаем в директорию, где находится файл самой программы.

В первой строке CSV-файлов указываются параметры, которые разделяются запятыми. Следующие строки содержат уже значения этих параметров. При окончательной проверке, DataSet должен последовательно содержать данные: названия трека, BPM и год выхода композиции. Будем использовать информацию выборки в сто песен, для каждого жанра из выбранных пяти.

Параметры: name название трека; bpm темп; year год релизаПараметры: name название трека; bpm темп; year год релиза

4: Rap построение точечной диаграммы и гистограммы

Выборка взята здесь: rollingstone.com/100-greatest-hip-hop-songs-of-all-time
Сам CSV-DataSet: github.com/Rap.csv

На основе информации DataSet'а, создаём точечную диаграмму [Scatter Plots] для изучения взаимосвязи между BPM и годом выпуска, а также для отображения концентраций при ранжировании данных.

Видно, что с 1980 по 2005 гг. основным темпом был диапазон в 90-105 BPMВидно, что с 1980 по 2005 гг. основным темпом был диапазон в 90-105 BPMКод точечной диаграммы с комментариями
from matplotlib import pyplot as plt                              # Первый каноничный импортimport pandas as pd                                                    # Второй каноничный импорт для обработки DataSet'аplt.style.use('fivethirtyeight')                                         # Назначаем стилистику визуализацииdata_set = pd.read_csv('Rap.csv')                               # Считываем данные SCV-файла с DataSet'омbpm = data_set['bpm']                                                  # Переменная, для параметра BPM в каждой строкеyear = data_set['year']                                                  # Переменная, для параметра "год релиза" в каждой строкеplt.scatter(                                                                     # Построение точечного графика и его настройкаbpm, year,                                                                   # Данные для осей x и yc=bpm,                                                                        # Привязка цвета к нужной осиs=bpm*1.5,                                                                  # Зависимость размера точкиcmap='gist_heat',                                                        # Цветовая карта графикаedgecolor='black',                                                       # Цвет контура точкиlinewidth=.7                                                                 # Толщина контура точки)bar = plt.colorbar(                                                          # Построение шкалы BPMorientation='horizontal',                                            # Ориентация шкалыshrink=0.8,                                                               # Масштаб шкалыextend='both',                                                           # Скос краёв шкалыextendfrac=.1                                                           # Угол скоса краёв)bar.set_label('Шкала ударов в минуту', fontsize=18)   # Подпись шкалыplt.title('Популярность скорости '                                  # Заголовок графика  'исполнения в Rap\'е ', fontsize=25)plt.xlabel('BPM', fontsize=18)                                         # Ось абсциссplt.ylabel('Год релиза', fontsize=18)                               # Ось ординатplt.tight_layout()                                                              # Настройка параметров подзаголовков в области отображенияplt.show()                                                                        # Вывод на экран

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

Самый популярный диапазон: 80-100 BPMСамый популярный диапазон: 80-100 BPMКод гистограммы без комментариев
import pandas as pdfrom matplotlib import pyplot as pltfrom collections import Counterplt.style.use("fivethirtyeight")data_set = pd.read_csv('Rap end.csv')index = data_set['number']ranges = data_set['bpm_range']counter = Counter()for index in ranges:counter.update(index.split(';'))range_bpm = []value = []for item in counter.most_common(100):range_bpm.append(item[0])value.append(item[1])range_bpm.reverse()value.reverse()plt.barh(range_bpm, value,linewidth=.5,edgecolor='black',color='#e85b45',label='Количество точек на предыдущем графике')plt.legend()plt.title('Популярность интервала значений BPM в rap\'е', fontsize=25)plt.xlabel('Количество песен в диапазоне BPM', fontsize=18)plt.ylabel('Диапазоны BPM', fontsize=18)plt.tight_layout()plt.show()

5: Рок

Выборка взята здесь: rockfm.ru/top100
Сам CSV-DataSet: github.com/Rock.csv

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

Код точечной диаграммы с комментариями
from matplotlib import pyplot as plt                              # Первый каноничный импортimport pandas as pd                                                    # Второй каноничный импорт для обработки DataSet'аplt.style.use('fivethirtyeight')                                         # Назначаем стилистику визуализацииdata_set = pd.read_csv('Rock.csv')                             # Считываем данные SCV-файла с DataSet'омbpm = data_set['bpm']                                                  # Переменная, для параметра BPM в каждой строкеyear = data_set['year']                                                  # Переменная, для параметра "год релиза" в каждой строкеplt.scatter(                                                                     # Построение точечного графика и его настройкаbpm, year,                                                                   # Данные для осей x и yc=bpm,                                                                        # Привязка цвета к нужной осиs=bpm*1.5,                                                                  # Зависимость размера точкиcmap='gist_heat',                                                        # Цветовая карта графикаedgecolor='black',                                                       # Цвет контура точкиlinewidth=.7                                                                 # Толщина контура точкиalpha=.7                                                                      # Прозрачность точки)bar = plt.colorbar(                                                          # Построение шкалы BPMorientation='horizontal',                                            # Ориентация шкалыshrink=0.8,                                                               # Масштаб шкалыextend='both',                                                           # Скос краёв шкалыextendfrac=.1                                                           # Угол скоса краёв)bar.set_label('Шкала ударов в минуту', fontsize=18)    # Подпись шкалыplt.title('Популярность скорости '                                   # Заголовок графика  'исполнения в роке', fontsize=25)plt.xlabel('BPM', fontsize=18)                                          # Ось абсциссplt.ylabel('Год релиза', fontsize=18)                                # Ось ординатplt.tight_layout()                                                               # Настройка параметров подзаголовков в области отображенияplt.show()                                                                         # Вывод на экран
Самые популярные диапазоны: 120-140 и 100-120 BPMСамые популярные диапазоны: 120-140 и 100-120 BPMКод гистограммы без комментариев
import pandas as pdfrom matplotlib import pyplot as pltfrom collections import Counterplt.style.use("fivethirtyeight")data_set = pd.read_csv('Rock end.csv')index = data_set['number']ranges = data_set['bpm_range']counter = Counter()for index in ranges:counter.update(index.split(';'))range_bpm = []value = []for item in counter.most_common(100):range_bpm.append(item[0])value.append(item[1])range_bpm.reverse()value.reverse()plt.barh(range_bpm, value,linewidth=.5,edgecolor='black',color='#e85b45',label='Количество точек на предыдущем графике')plt.legend()plt.title('Популярность интервала значений BPM в роке', fontsize=25)plt.xlabel('Количество песен в диапазоне BPM', fontsize=18)plt.ylabel('Диапазоны BPM', fontsize=18)plt.tight_layout()plt.show()

6: Блюз

Выборка взята здесь: digitaldreamdoor.com/best_bluesong
Сам CSV-DataSet: github.com/Blues.csv

Видно высокую концентрацию использования темпа около 100 BPM в 90-хВидно высокую концентрацию использования темпа около 100 BPM в 90-хКод точечной диаграммы с комментариями
from matplotlib import pyplot as plt                              # Первый каноничный импортimport pandas as pd                                                    # Второй каноничный импорт для обработки DataSet'аplt.style.use('fivethirtyeight')                                         # Назначаем стилистику визуализацииdata_set = pd.read_csv('Blues.csv')                            # Считываем данные SCV-файла с DataSet'омbpm = data_set['bpm']                                                  # Переменная, для параметра BPM в каждой строкеyear = data_set['year']                                                  # Переменная, для параметра "год релиза" в каждой строкеplt.scatter(                                                                     # Построение точечного графика и его настройкаbpm, year,                                                                   # Данные для осей x и yc=bpm,                                                                        # Привязка цвета к нужной осиs=bpm*1.5,                                                                  # Зависимость размера точкиcmap='gist_heat',                                                        # Цветовая карта графикаedgecolor='black',                                                       # Цвет контура точкиlinewidth=.7                                                                 # Толщина контура точки)bar = plt.colorbar(                                                          # Построение шкалы BPMorientation='horizontal',                                            # Ориентация шкалыshrink=0.8,                                                               # Масштаб шкалыextend='both',                                                           # Скос краёв шкалыextendfrac=.1                                                           # Угол скоса краёв)bar.set_label('Шкала ударов в минуту', fontsize=18)    # Подпись шкалыplt.title('Популярность скорости '                                   # Заголовок графика  'исполнения в блюзе', fontsize=25)plt.xlabel('BPM', fontsize=18)                                          # Ось абсциссplt.ylabel('Год релиза', fontsize=18)                                # Ось ординатplt.tight_layout()                                                               # Настройка параметров подзаголовков в области отображенияplt.show()                                                                         # Вывод на экран
Самый популярный диапазон: 100-120 BPMСамый популярный диапазон: 100-120 BPMКод гистограммы без комментариев
import pandas as pdfrom matplotlib import pyplot as pltfrom collections import Counterplt.style.use("fivethirtyeight")data_set = pd.read_csv('Blues end.csv')index = data_set['number']ranges = data_set['bpm_range']counter = Counter()for index in ranges:counter.update(index.split(';'))range_bpm = []value = []for item in counter.most_common(100):range_bpm.append(item[0])value.append(item[1])range_bpm.reverse()value.reverse()plt.barh(range_bpm, value,linewidth=.5,edgecolor='black',color='#e85b45',label='Количество точек на предыдущем графике')plt.legend()plt.title('Популярность интервала значений BPM в блюзе', fontsize=25)plt.xlabel('Количество песен в диапазоне BPM', fontsize=18)plt.ylabel('Диапазоны BPM', fontsize=18)plt.tight_layout()plt.show()

7: Chillout

Выборка взята здесь: open.spotify.com
Сам CSV-DataSet: github.com/Chillout.csv

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

Код точечной диаграммы с комментариями
from matplotlib import pyplot as plt                              # Первый каноничный импортimport pandas as pd                                                    # Второй каноничный импорт для обработки DataSet'аplt.style.use('fivethirtyeight')                                         # Назначаем стилистику визуализацииdata_set = pd.read_csv('Chillout.csv')                         # Считываем данные SCV-файла с DataSet'омbpm = data_set['bpm']                                                  # Переменная, для параметра BPM в каждой строкеyear = data_set['year']                                                  # Переменная, для параметра "год релиза" в каждой строкеplt.scatter(                                                                     # Построение точечного графика и его настройкаbpm, year,                                                                   # Данные для осей x и yc=bpm,                                                                        # Привязка цвета к нужной осиs=bpm*1.5,                                                                  # Зависимость размера точкиcmap='gist_heat',                                                        # Цветовая карта графикаedgecolor='black',                                                       # Цвет контура точкиlinewidth=.7                                                                 # Толщина контура точкиalpha=.5                                                                      # Прозрачность точки)bar = plt.colorbar(                                                          # Построение шкалы BPMorientation='horizontal',                                            # Ориентация шкалыshrink=0.8,                                                               # Масштаб шкалыextend='both',                                                           # Скос краёв шкалыextendfrac=.1                                                           # Угол скоса краёв)bar.set_label('Шкала ударов в минуту', fontsize=18)   # Подпись шкалыplt.title('Популярность скорости '                                  # Заголовок графика  'исполнения в Chillout', fontsize=25)plt.xlabel('BPM', fontsize=18)                                          # Ось абсциссplt.ylabel('Год релиза', fontsize=18)                               # Ось ординатplt.tight_layout()                                                               # Настройка параметров подзаголовков в области отображенияplt.show()                                                        # Вывод на экран
Самый популярный диапазон: 80-100Самый популярный диапазон: 80-100Код гистограммы без комментариев
import pandas as pdfrom matplotlib import pyplot as pltfrom collections import Counterplt.style.use("fivethirtyeight")data_set = pd.read_csv('Chillout end.csv')index = data_set['number']ranges = data_set['bpm_range']counter = Counter()for index in ranges:counter.update(index.split(';'))range_bpm = []value = []for item in counter.most_common(100):range_bpm.append(item[0])value.append(item[1])range_bpm.reverse()value.reverse()plt.barh(range_bpm, value,linewidth=.5,edgecolor='black',color='#e85b45',label='Количество точек на предыдущем графике')plt.legend()plt.title('Популярность интервала значений BPM в Chillout', fontsize=25)plt.xlabel('Количество песен в диапазоне BPM', fontsize=18)plt.ylabel('Диапазоны BPM', fontsize=18)plt.tight_layout()plt.show()

8: EDM

Выборка взята здесь: edmcharts.net
Сам CSV-DataSet: github.com/EDM.csv

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

Довольно однозначно вышло...Довольно однозначно вышло...Код точечной диаграммы с комментариями
from matplotlib import pyplot as plt                              # Первый каноничный импортimport pandas as pd                                                    # Второй каноничный импорт для обработки DataSet'аplt.style.use('fivethirtyeight')                                         # Назначаем стилистику визуализацииdata_set = pd.read_csv('EDM.csv')                             # Считываем данные SCV-файла с DataSet'омbpm = data_set['bpm']                                                  # Переменная, для параметра BPM в каждой строкеyear = data_set['year']                                                  # Переменная, для параметра "год релиза" в каждой строкеplt.scatter(                                                                     # Построение точечного графика и его настройкаbpm, year,                                                                   # Данные для осей x и yc=bpm,                                                                        # Привязка цвета к нужной осиs=bpm*1.5,                                                                  # Зависимость размера точкиcmap='gist_heat',                                                        # Цветовая карта графикаedgecolor='black',                                                       # Цвет контура точкиlinewidth=.7                                                                 # Толщина контура точкиalpha=.2                                                                      # Прозрачность точки)bar = plt.colorbar(                                                          # Построение шкалы BPMorientation='horizontal',                                            # Ориентация шкалыshrink=0.8,                                                               # Масштаб шкалыextend='both',                                                           # Скос краёв шкалыextendfrac=.1                                                           # Угол скоса краёв)bar.set_label('Шкала ударов в минуту', fontsize=18)   # Подпись шкалыplt.title('Популярность скорости '                                  # Заголовок графика  'исполнения в EDM', fontsize=25)plt.xlabel('BPM', fontsize=18)                                          # Ось абсциссplt.ylabel('Год релиза', fontsize=18)                               # Ось ординатplt.tight_layout()                                                               # Настройка параметров подзаголовков в области отображенияplt.show()                                                        # Вывод на экран
Самый популярный диапазон: 120-140Самый популярный диапазон: 120-140Код гистограммы без комментариев
import pandas as pdfrom matplotlib import pyplot as pltfrom collections import Counterplt.style.use("fivethirtyeight")data_set = pd.read_csv('EDM end.csv')index = data_set['number']ranges = data_set['bpm_range']counter = Counter()for index in ranges:counter.update(index.split(';'))range_bpm = []value = []for item in counter.most_common(100):range_bpm.append(item[0])value.append(item[1])range_bpm.reverse()value.reverse()plt.barh(range_bpm, value,linewidth=.5,edgecolor='black',color='#e85b45',label='Количество точек на предыдущем графике')plt.legend()plt.title('Популярность интервала значений BPM в EDM', fontsize=25)plt.xlabel('Количество песен в диапазоне BPM', fontsize=18)plt.ylabel('Диапазоны BPM', fontsize=18)plt.tight_layout()plt.show()

9: Заключение

Самым простым графиком сравним количество попаданий в каждый диапазон, композиций, из всех проанализированных ранее жанров*.

* такие жанры как ethnic, ambient, folk, dubstep, reggae и др, не удалось к сожалению разобрать из-за отсутствия качественной выборки...

BPM/Кол-во треков

<60

60-80

80-100

100-120

120-140

140-160

160-180

Blues

2

9

25

35

15

6

8

Chillout

11

35

18

19

12

5

EDM

1

3

21

67

6

2

Rap

5

61

20

7

4

3

Rock

6

20

25

27

11

11

Итог:

2

32

144

119

135

39

29

Простой код, простого графика
from matplotlib import pyplot as pltplt.style.use('fivethirtyeight')x = ['<60', '60-80', '80-100', '100-120', '120-140', '140-160', '160-180']y = [2, 32, 144, 119, 135, 39, 29]plt.plot(x, y, label='BPM', c='#e85b45')plt.legend()plt.title('Сравнение всех диапазонов BPM во всех жанрах', fontsize=25)plt.xlabel('Диапазон BPM', fontsize=18)plt.ylabel('Количество треков', fontsize=18)plt.tight_layout()plt.show()
Подробнее..

Почему lsFusion, а не 1С?

02.03.2021 10:09:04 | Автор: admin


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


Как было отмечено в заключении предыдущей статьи, в качестве решения всех описанных в предыдущей статье проблем предлагается использовать платформу (и одноименный язык) lsFusion. Эта платформа имеет в основе несколько достаточно редких, но при этом наиболее высокодекларативных парадигм программирования: комбинаторное (function-level, не путать с functional), реактивное, событийно-ориентированное, программирование на ограничениях (constraint) и метапрограммирование. Впрочем, для неискушенного читателя это все не более чем набор красивых buzzwords, поэтому не будем уделять много внимания теории, а сразу перейдем на более практический уровень, а именно к решению описанных в предыдущей статье проблем.


Структура этой статьи полностью повторяет структуру статьи Почему не 1С? (с теми же названиями разделов и в том же порядке):


Оглавление


При этом в каждом разделе, с названием проблемы в 1С, рассказывается как данная проблема решается в lsFusion.


Объекты: Справочники, Документы и т.д.


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


  • Как правило, все эти механизмы достаточно эвристичны и либо включают лишние данные (например, какую-нибудь сугубо техническую информацию, если она хранится в самом объекте), либо, наоборот, какие-то данные не включают (например, если эти данные хранятся в отдельных таблицах / регистрах, но все равно логически относятся к объекту). Соответственно, эти автоматические механизмы все равно приходится донастраивать вручную, что уничтожает практически все преимущество их автоматичности.
  • Все эти механизмы всегда можно надстроить сверху (что в lsFusion уже делается и / или будет сделано в следующих версиях), получив тем самым преимущества обоих подходов.

Неэффективное получение данных объектов


Так как для выполнения логики вычислений lsFusion пытается максимально использовать SQL-сервер, а не сервер приложений (причем делает это максимально группируя запросы, чтобы выполнять их как можно меньше), операции чтения объекта целиком в lsFusion не существует в принципе. Как следствие, и проблема N+1 и проблема избыточного чтения в lsFusion возникают крайне редко. Например следующее действие:

fillSum(Invoicei){
FORinvoice(InvoiceDetailid)=iDO
sum(id)<-price(id)*quantity(id);
}
Скомпилируется в одно действие:
fillSum(Invoicei){
sum(InvoiceDetailid)<-price(id)*quantity(id)WHEREinvoice(id)=i
}
Которое, в свою очередь, выполнится одним запросом, в котором будут читаться / писаться только используемые ряды / колонки.

Таблицы / Представления: Регистры


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


  • Таблицы первичные свойства
  • Представления свойства, реализуемые при помощи остальных операторов (впрочем, материализованные свойства можно также рассматривать и как таблицы)
  • Работа с моментами / периодами времени частный случай представлений, свойства, реализуемые при помощи операторов группировки:
    • СрезПоследних GROUP LAST
    • Остатки, Обороты SUM (ОстаткиИОбороты в lsFusion не имеют смысла, так как оптимизатор lsFusion умеет сам объединять подзапросы, если это необходимо)

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


Единственное, что еще хотелось бы отметить. Возможно в lsFusion все же имеет смысл добавить синтаксический сахар по созданию всего этого комбайна из класса регистра, агрегации, а также набора готовых свойств остатков и оборотов. Что-то вроде:

LEDGERSalesGROUPStockstock,SkuskuSUMNUMERICquantity,NUMERICsum;

//автоматическисоздаетклассSalesисвойства:
//stock,sku,quantity,sum=ABSTRACTStock,Sku,NUMERIC,NUMERIC(Sales);-
соответствующиеизмерения/ресурсы
//quantitySales,sumSales(stock,sku)-текущийостаток(имя=имясвойства+имярегистра)
//quantitySales,sumSales(stock,sku,DATETIME)-текущийостатокнадату
//quantitySales,sumSales(stock,sku,DATETIME,DATETIME)-оборотысдатыподату
//ит.п.
И в следующих версиях такой синтаксический сахар скорее всего появится. Другое дело, что чаще всего в сложных проектах регистры имеют более сложную структуру (например, наследуются друг от друга, денормализуют данные для составных индексов, расширяются в разных модулях и так далее), поэтому такой сахар может быть важен разве что для RAD разработки (а точнее прототипирования), которая в современном IT-мире уже не так актуальна.

Регистры поддерживаются в очень частных случаях


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


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


  1. Композицию, что, например, позволяет прозрачно денормализовывать данные из разных таблиц (это будет показано в разделе "Статичная физическая модель").
  2. Максимумы / минимумы / последние значения по любому полю, что позволяет эффективно организовывать нумерацию и ранжирование данных.
  3. Рекурсию, что, например, позволяет разворачивать иерархию в плоские таблицы (с такой же высокой производительностью).
  4. Выбор (полиморфизм), что позволяет наследовать регистры друг от друга.
  5. И многие другие

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


lsFusion поддерживает ограничения и события в общем случае, причем, в том числе, для вычисляемых нематериализованных данных. Так, например, для создания ограничения, что остаток (который может вычисляться с использованием любого количества различных данных / операторов) должен быть больше 0, достаточно написать всего одну строку:

CONSTRAINTcurrentBalance(sku,stock)<0MESSAGE'Остатокнеможетбытьотрицательным';
Соответственно, платформа будет сама максимально эффективно (инкрементальными вычислениями) проверять, что никакое изменение (например, изменение склада прихода или количества расхода), это ограничение не нарушит.

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

WHENSET(currentBalance(Skusku,Stockstock)<0)//когдаостатокстановитсяменьше0
EMAILSUBJECT'Остатокнаскладе'+address(stock)+'длятовара'+name(sku)+
'сталменьше0'TOresponsibleEmail(group(sku));

В параметрах виртуальных таблиц можно использовать только константы


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

EXPORTFROMprice(Skusku),balance(date(sku),sku)WHEREname(sku)='Колбаса';
Платформа автоматически протолкнет условие ограничивающее наименование (и как следствие даты, на которые будет вычисляться остаток) внутрь подзапроса (и всех подзапросов внутри этого подзапроса), таким образом выполнив оптимизацию predicate push down . Причем в отличии от того же SQL, платформа умеет выполнять эту оптимизацию не только для группировок, но и для разбиений и даже для рекурсий. Впрочем это тема для отдельной статьи, подробно на ней здесь останавливаться не будем.

Запросы


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


Запросы в строках


И логика свойств и логика форм задаются непосредственно на языке lsFusion, соответственно для них:


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

Тут конечно иногда возникают вопросы с динамическими формируемыми запросами, но как правило они решаются использованием либо соответствующих условных операторов / опций (IF, SHOWIF и т.п.), либо оператора выполнения программного кода (EVAL), позволяющего выполнить любую заданную строку кода на lsFusion.


Отсутствие оптимизатора запросов


В lsFusion внутри очень мощный механизм оптимизации запросов, во многих случаях выполняющий оптимизации, которые не умеют выполнять даже дорогие коммерческие СУБД (не говоря уже о PostgreSQL). Так, все проблемы с производительностью описанные в статье Почему не SQL, lsFusion умеет решать самостоятельно без каких-либо дополнительных действий со стороны разработчика, который, соответственно, может сконцентрироваться на решении бизнес-задач, а не думать как правильно написать запрос и / или в какую временную таблицу положить его результат.


Так пример из статьи про 1С в lsFusion будет выглядеть следующим образом:


Пример из статьи
ВБРАТЬ    РасходнаяНакладнаяСостав.Номенклатура,    УчетНоменклатурыОстатки.КоличествоОстатокИЗ    Документ.РасходнаяНакладная.Состав КАК РасходнаяНакладнаяСостав        ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.УчетНоменклатуры.Остатки(,                             Номенклатура В (                                   ВБРАТЬ Номенклатура                                   ИЗ Документ.РасходнаяНакладная.Состав                                   ГДЕ Ссылка = &Документ)) КАК УчетНоменклатурыОстатки        ПО УчетНоменклатурыОстатки.Номенклатура = РасходнаяНакладнаяСостав.НоменклатураГДЕ    РасходнаяНакладнаяСостав.Ссылка = &Документ И    (УчетНоменклатурыОстатки.КоличествоОстаток < РасходнаяНакладнаяСостав.Количество ИЛИ        УчетНоменклатурыОстатки.КоличествоОстаток ЕСТЬ NULL)
currentBalance(InvoiceDetailid)=currentBalance(sku(id));

export(Invoicei){
EXPORTFROMsku(InvoiceDetailid),currentBalance(id)WHEREinvoice(id)=iAND
currentBalance(id)<quantity(id)ORNOTcurrentBalance(id);
}
Соответственно никаких Номенклатура В ( ВБРАТЬ Номенклатура ИЗ Документ.РасходнаяНакладная.Состав ГДЕ Ссылка = &Документ) в lsFusion писать не надо.

Отсутствие расширенных SQL возможностей


Кроме классического набора операций в SQL-92, состоящего из группировок и композиций (аналог в SQL соединения), в lsFusion также поддерживаются операции:


  • Разбиение / упорядочивание (аналог в SQL оконные функции)
  • Рекурсия (аналог в SQL рекурсивные CTE)
  • Полиморфизм (косвенный аналог в SQL наследование таблиц)

Причем те же рекурсии в lsFusion поддерживаются в общем случае без каких-либо дополнительных ограничений (вроде невозможности использования GROUP BY в запросе шага).


Отсутствие запросов на изменение


Большинство ERP-платформ хотя формально и поддерживают ORM механизмы, но на практике, из-за плохой производительности, решения на ERP-платформах все равно в абсолютном большинстве случаев используют запросы на SQL-подобных языках.


Впрочем, в том же 1С запросы поддерживаются только для операций чтения данных, для записи все-таки приходится использовать ORM механизмы, производительность которых оставляет желать лучшего. В lsFusion такой проблемы нет, и все операции, в том числе создание объектов, могут выполняться на сервере БД, причем одним запросом. Например:

generateCards(){
FORiterate(i,1,10000)NEWd=DiscountCardDO
number(d)Card:+i;
}

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

Тоже самое касается и механизма изменения / удаления большого количества данных / объектов:

FORsum(DiscountCardd)>10000DO
vip(d)TRUE;
FORsum(DiscountCardd)>10000DO
DELETEd;
Скомпилируется в:
sum(DiscountCardd)TRUEWHEREsum(d)>10000;
DELETEDiscountCarddWHEREsum(d)>10000;
И опять-таки выполнится одним запросом.

Отказ от автоматических блокировок


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


  1. Использовать версионные СУБД (или версионный режим в том же MS SQL).
  2. Повысить уровень изоляции базы до Repeatable Read или еще лучше до Serializable.
  3. Материализовать данные, для которых важна целостность.
  4. Все транзакции с конфликтами записей или дедлоками откатывать и повторять заново.

Именно этот способ решения проблемы используется в lsFusion. Правда, в отличие от того же 1С (как и остальных платформ) в lsFusion:


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

Формы


В lsFusion формы универсальный механизм, отвечающий за объединение данных вместе и вывод их пользователю / ИС в том или ином виде.


Отказ от единого потока выполнения: разделение логики на сервер и клиент


В отличие от 1С в lsFusion поток выполнения един и для сервера, и для клиента. Такое единство значительно упрощает взаимодействие с пользователем / клиентским устройством с точки зрения процесса разработки. Так, пример в статье про 1С написан именно на языке lsFusion, и, соответственно, выглядит следующим образом:

f()<-someData();//читаемданныеизбазынеобходимыедляmyForm
DIALOGmyFormOBJECTSaINPUTDO//ОткрытьФормуМодально,пользователь
выбираеткакой-тообъект
IFisSomething(a)DO//читаемданныедляэтогообъектаиеслиснимичто-то
нето
DIALOGotherFormOBJECTSb=aDO{//ОткрытьФормуМодально,открываем
другуюформугдепользовательвыбираетдругойобъектb
g(b)<-someInput(b);//записываемданныедляb
APPLY;//сохраняемизменениявбазу
}
В какой-то степени lsFusion использует подход обычных форм в 1С, только делает это гораздо более масштабируемо и производительно. Фактически, вся магия асинхронности остается под капотом, а разработчик может сконцентрироваться строго на решении бизнес-задач, а не думать о том, где и как должен выполнять написанный им код.

Тут может возникнуть впечатление, что в lsFusion вообще не существует возможности выполнять заданный код на клиенте (в смысле кроме уже встроенных в платформу операторов/действий). Но это не так, в lsFusion существует специальная опция CLIENT в операторе INTERNAL, которая позволяет выполнить заданный именно на клиенте код. Для десктоп клиента этот код задается на Java, а для веб-клиента на JavaScript. Правда обычно такая тонкая настройка нужна очень редко, поэтому подробно останавливаться на ней здесь особого смысла нет.


Отказ от синхронности


Синхронность в бизнес-приложении важна прежде всего для организации диалоговых процессов (когда от пользователя нужно получить ответ) и для обеспечения транзакционности процессов (когда необходимо гарантировать полное, от начала и до конца, выполнение некоторой операции). Проблема синхронности на клиенте прежде всего в однопоточности клиентской среды (например, браузера), и, как мы выяснили в предыдущей статье, в 1С эту проблему просто решили переложить на разработчика (справедливости ради, в последних версиях в платформу вроде начали добавлять async / await, но это лишь частичное решение проблемы).


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


Отказ от WYSIWYG: разделение интерфейса на запись и чтение


Как было подробно описано в предыдущей статье, у подхода 1С в построении интерфейсов, которые одновременно содержат и первичные и вычисляемые данные (для этого напомню используются динамические списки), есть как минимум 2 проблемы:


  • Из-за отсутствия оптимизатора, в динамических списках крайне противопоказано использовать подзапросы / виртуальные таблицы (представления).
  • Динамические списки нельзя редактировать.

Как следствие в 1С все интерфейсы по сути строго разделены на интерфейсы чтения и интерфейсы ввода. В lsFusion описанных двух проблем нет, и, как следствие, можно абсолютно декларативно строить максимально эргономичные и привычные пользователю Excel-style интерфейсы, вытаскивая на форму одновременно и данные, которые необходимо ввести, и данные, необходимые для принятия решения того, что именно надо вводить. Причем все построенные интерфейсы (как впрочем и остальные абстракции в lsFusion) реактивны из коробки автоматически обновляются при вводе / изменении данных пользователем. Впрочем на эту тему была отдельная статья, поэтому подробно останавливаться на этой теме здесь также не имеет особого смысла.


Невозможность обращаться в списках к реквизитам форм / текущим значениям других списков


В lsFusion при задании свойств, фильтров и других элементов на форме можно обращаться сразу ко всем объектам, даже если они отображаются в разных списках (или других представлениях). При этом платформа сама следит за изменениями объектов (также как и за изменениями данных) и автоматически обновляет данные формы использующие эти объекты. Например, если создать следующую форму:

FORMbalance
OBJECTSst=Stock,sk=Sku
PROPERTIES(st)name
PROPERTIESname(sk),currentBalance(st,sk)
FILTERScurrentBalance(st,sk)
;
При перемещении текущей записи в верхнем списке (складов), нижний список (товары, которые есть на выбранном складе) будет обновляться автоматически.

Избыточные уровни абстракции


Основным принципом при создании lsFusion был и остается принцип чистота и завершенность всех создаваемых абстракций. Так:


  • За один и тот же функционал в lsFusion отвечает ровно одна абстракция. Если для реализации некоторого функционала можно использовать уже существующие абстракции их нужно использовать. Такой подход значительно упрощает как изучение платформы разработчиками, так и разработку самой платформой.
  • Все абстракции выстроены в уровни (верхние используют нижние) таким образом, что если какая то абстракция разработчика не устраивает, он всегда может спуститься на уровень ниже, тем самым получить большую гибкость (правда при этом выполнив больше работы).
    Графически такая иерархия выглядит следующим образом:


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


  • Объекты / записи

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


  • Объекты / ссылки на объекты

Тут как я уже упоминал в предыдущей статье, 1С что-то сильно перемудрили на мой взгляд и естественно никаких различий между объектами и ссылками на них в lsFusion нет (и я даже не могу представить, зачем это различие может понадобиться).


  • Данные формы / Данные объектов

В lsFusion поддерживается практически абсолютная реактивность на всех уровнях, в том числе на уровне форм. Соответственно необходимости в каких-то дополнительных абстракциях вроде данных формы в lsFusion попросту нет. Есть просто данные, и соответственно при любом их изменение платформа автоматически обновляет все представления их использующие. Если же форме нужны какие-то свои локальные данные, разработчик просто создает необходимые локальные первичные свойства, и работает с ними также как и со всеми остальными свойствами (например хранящимися в базе). То есть никаких крышесносящих РеквизитФормыВЗначение в lsFusion нет.


  • Запросы / СКД / Аналитика (BI)

Тут все немного сложнее. Запросов, как уже упоминалось в разделе объекты / записи в lsFusion нет (за их формирование и выполнение отвечает сама платформа). Остальной функционал выбора, объединения и отображения данных (не только из этого пункта, а вообще) в lsFusion соответствует следующим механизмам:


  • Печатные формы печатное представление формы, дизайн которого задается при помощи JasperReports, одной из самых распространенных систем отчетности под Java. Позволяет строить pixel-perfect формы, и вообще обладает огромным количеством различных возможностей.
  • Встроенная аналитика одно из представлений списка объектов формы, поддерживает графики, диаграммы, сводные таблицы и многое другое.
  • Сложные интерактивные формы с вычисляемыми данными обычное интерактивное представление формы позволяет отображать как первичные, так и вычисляемые данные, а также создавать сразу много списков объектов и связывать их друг с другом одной строкой кода (в разделе выше был пример).
  • Программный интерфейс работы с данными структурированное представление формы, позволяет экспортировать (и наоборот импортировать) любую форму в JSON, XML, XLSX, DBF и другие распространенные форматы.

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


Закрытая физическая модель


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


Открытая и прозрачная физическая модель дает массу преимуществ:


  1. Простая и очень производительная интеграция со сторонними инструментами (например BI).
  2. Возможность использования стандартных средств администрирования БД (например, профайлеров)
  3. Читабельные запросы в журналах и логах.

Статичная физическая модель


PS: Так как большая часть статьи писалась на сутках (есть сейчас такое национальное развлечение в Беларуси) и самой статьи Почему не 1С? перед глазами у автора не было, многое писалось по памяти, и, как оказалось, этого и следующего недостатка в предыдущей статье не было вообще. Но, так как эти разделы уже были написаны, мы решили их все же включить в эту статью.


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

date=DATADATE(DocumentDetail)
barcode=DATASTRING(Sku);
sku=DATASku(DocumentDetail);

barcode(DocumentDetaildd)=barcode(sku(dd));
count(STRINGbc,DATEd)=GROUPSUM1IFdate(DocumentDetaildd)>dANDbarcode(dd)=bc;
FORMx
OBJECTSbc=STRINGPANEL,d=DATEPANEL
PROPERTIEScount(bc,d),VALUE(bc),VALUE(d)
;
При выполнении этой формы сформируется запрос в котором будет:
  1. JOIN с таблицей товаров, штрихкод в таблице SKU совпадает с заданным;
  2. подсчет количества строк документов по всем датам больше заданной.

При этом у SQL сервера будет два варианта: либо бежать по индексу по датам в таблице строк, либо бежать по индексу по штрихкодам в таблице товаров, находить товары, после чего бежать по индексу по Sku в таблице строк. В обоих случаях производительность будет оставлять желать лучшего (если движений одного товара очень много и товаров очень много). В lsFusion для решения этой проблемы достаточно изменить / добавить следующие строки:

barcode(DocumentDetaildd)=barcode(sku(d))MATERIALIZED;//помечает,что
должнобытьхранимое
INDEXbarcode(DocumentDetaildd),date(dd);//строимсоставнойиндекс
После такой оптимизации SQL сервер сможет начать использовать построенный составной индекс и производительность будет максимальной.

Закрытые исходники и лицензии


Открытые исходники и лицензия в последнее время стали де-факто стандартом в отрасли средств разработки. Даже Microsoft, известная ранее консервативностью в этом вопросе, открыла исходники .Net, сделала его бесплатным и выпустила версию под Linux.


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


lsFusion выпускается под LGPL v3 лицензией, которая подразумевает свободное использование, распространение и модификацию (за исключением выпуска коммерческой версии платформы), в общем практически все что угодно. Исходники доступны на GitHub. Это обычный Maven-проект, соответственно поддерживаются все стандартные циклы сборки Maven: compile, install, package и т.п. Также открыты исходники сервера сборки, плагина, управление проектами ведётся в GitHub Projects. То есть вся инфраструктура открыта настолько, насколько это возможно.


Отсутствие наследования и полиморфизма


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


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


Про то в каком именно виде в lsFusion поддерживаются наследование и полиморфизм уже неоднократно рассказывалось на хабре, как в формате tutorial, так и на конкретных примерах, поэтому повторяться тут не будем.


Отсутствие явной типизации в коде


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


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

invoice(InvoiceDetailid)=DATAInvoice;
sum=GROUPSUMsum(InvoiceDetailid)BYinvoice(id)//уsumсчитаетсяестьодин
параметрклассаInvoice(путемвыводаклассазначенияinvoce,вданномслучае-Invoice)

FORMmyForm
OBJECTSmyObject=MyClass
;
filtered=FILTERmyForm.myObject;//уfilteredсчитаетсяестьодинпараметркласса
MyClass(выводитсяизклассаобъектаmyObjectформыmyForm)

Отсутствие модульности


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


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


  1. События предметной области (и все то, что на них построено ограничения и агрегации) позволяют разбить всю бизнес-логику на множество небольших напрямую независимых друг от друга правил. Эти правила, в свою очередь, автоматически выстраиваются платформой в один большой поток выполнения, в зависимости от того, какие данные эти правила изменяют / используют.
  2. Расширения позволяют расширять практически все существующие в платформе абстракции. Такая возможность опять-таки позволяет максимально декомпозировать любую сложную бизнес-логику (например, сложные формы).
  3. Множественные наследование и полиморфизм дают все преимущества ООП, основным из которых является все та же декомпозиция (а значит и модульность). Отметим, что полиморфизм в какой-то степени являются частным случаям расширений (то есть они расширяют существующее абстрактное свойство / действие, добавлением к нему новых реализаций).
  4. Отказ от инкапсуляции и акцент на данных, а не объектах (это уже упоминалось в самом первом разделе). Впрочем, тут конечно важно не отсутствие синтаксического сахара в виде this, а то, что классы в lsFusion получаются по сути открытыми, то есть опять-таки расширяемыми.
  5. Метапрограммирование позволяет осуществлять шаблонизацию кода, тем самым значительно уменьшая его дублирование, а значит опять-таки (пусть и косвенно) повышая модульность создаваемых решений

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


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


Ставка на визуальное программирование


В предыдущей статье были подробно расписаны все недостатки ВП, и скорее всего, именно поэтому подход Everything as code стал золотым стандартом в мире современной разработки. Именно такой подход используется и в lsFusion.


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


Фатальный недостаток


Вообще, проблема переписывания всего, что только можно, это проблема не только 1С, но и многих других ERP-платформ. Обусловлено это, видимо, историческим наследием, и имеет как минимум две проблемы:


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

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


Итак, что и где используется в lsFusion:
  1. Язык. Тут мы получаем больше всего вопросов в стиле почему мы не используем какой-нибудь уже существующий язык. На самом деле мы изначально и пытались использовать Java для задания логики приложения. Но со временем стало очевидно, что в отличие от того же 1С, язык lsFusion фундаментально отличается от всех существующих языков. Он гораздо более декларативный и высокоуровневый, сочетает в себе и логику запросов, и логику процедур, и даже логику представлений. Соответственно преимущества от поддержки своего языка явно начали перевешивать риски того, что на начальном этапе многие разработчики негативно относятся к любым новым языкам. В итоге мы все же решились на свой язык. И на мой взгляд, приняли правильное решение.


    Но естественно, поддержка языка lsFusion делалась не с нуля для создания серверных парсера и лексера в lsFusion используется ANTLR, для того же самого в IDEA используется Grammar-Kit (парсер), JFlex (лексер).

  2. UI. Для реализации десктоп-клиента используется Java SE (Swing, Web Start), что позволило получить поддержку сразу всех ОС, а также обновление и установку клиентского приложения прямо из коробки. Впрочем, как уже говорилось в одной из предыдущих статей, основным клиентом в текущей и следующих версиях будет веб-клиент, поэтому подробно на особенностях реализации десктоп-клиента останавливаться не будем.


    Для реализации веб-клиента в lsFusion используется:

    • GWT позволяет использовать Java и на сервере(ах), и на клиенте. Плюс, что, наверное, все же более важно, GWT позволяет разрабатывать клиента на типизированном языке с полиморфизмом и наследованием, без чего разработка столь функционального и одновременно быстрого клиента была бы гораздо сложнее. Кроме того GWT имеет достаточно бесшовную интеграцию с JavaScript, и соответственно позволил нам использовать огромное количество существующих JavaScript библиотек


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


      а) при начале разработки веб-клиента TypeScript ещё только-только появился;


      б) разработчик на lsFusion напрямую с GWT не сталкивается, поэтому его наличие в структуре ни на что не влияет, и при необходимости клиентскую часть всегда можно перевести на любую другую технологию.


      Но когда-нибудь миграция на TypeScript, я думаю, все же случится.

    • Full Calendar, Leaflet используются для поддержки нестандартных представлений списков (календаря и карты).
    • Spring Security, MVC, DI используются для задач аутентификации, управления сессиями, а также инициализации серверов (веб, приложений).
  3. BI для задач внутренней аналитики в lsFusion используется представление сводная таблица. Для реализации этого представления используются:

    • pivot-table, subtotal для пользовательской настройки BI, отрисовки таблиц (в том числе с подитогами),
    • plotly для отрисовки графиков и диаграмм,
    • tableToExcel для выгрузки сводных таблиц в Excel (с сохранением форматирования, collapsible рядов и т.п.).

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

  4. Печатные формы. Для работы с печатными формами в lsFusion используется одна из самых популярных технологий в этой области JasperReports.


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


    а) если группировки и колонки постоянно изменяются, дизайн по определению является динамичным и, скажем, уместить его в А4 или просто сделать красивым весьма непросто;


    б) аналитические инструменты требуют определенную ячеистость, что с другой стороны усложняет построение pixel-perfect печатных форм.


    В lsFusion используется другой подход: инструменты аналитики объединены с обычными таблицами (используют те же rendererы, стили, события и т. п.), а печатные формы являются отдельным механизмом. Соответственно BI обладает интерактивностью обычных таблиц (переход по ссылке, детализация и т. п.), а печатные формы используются в основном для печати документов и периодичной отчетности (с минимальной кастомизацией).

  5. IDE. Когда мы начинали разработку плагина для IDE, IDEA ещё не была настолько популярна (Eclipse был существенно популярнее), поэтому выбирая IDEA мы изрядно рисковали. Ирония, но несмотря на меньшее сообщество, найти материал по разработке своего языка под IDEA оказалось проще, да и ее архитектура выглядела существенно более продуманно. Сейчас IDEA (а точнее IntelliJ Platform) практически без сомнения лидер на рынке IDE, обладает огромным количеством возможностей, практически все из которых поддерживаются при работе с lsFusion (либо из коробки, либо благодаря реализации необходимых доработок в lsFusion плагине). Плюс stub indexы, chameleon element'ы и многое другое позволяет обеспечить высокую производительность практически на любых объемах lsFusion кода (скажем, у меня в агрегированном проекте десятки проектов достаточно высокой сложности с сотнями тысяч строк кода, и все работает очень быстро).
  6. Система контроля версий. Everything as code позволяет использовать любую из существующих систем контроля версий, самой популярной из которых, безусловно, является Git. Впрочем, на некоторых проектах с непрерывной поставкой без крупных функциональных веток можно спокойно использовать тот же Subversion (что, например, мы и делаем на некоторых проектах).
  7. Система управления зависимости / сборки. Опять таки EaC позволяет использовать существующие системы управления зависимости / сборки в Java, наиболее распространенной из которых является Maven (так центральный репозиторий для lsFusion поддерживается на repo.lsfusion.org).


    Чтобы подключить сервер в Maven-проект достаточно в pom.xml добавить следующие строки:


    <repositories>        <repository>            <id>lsfusion</id>            <name>lsFusion Public Repository</name>            <url>http://repo.lsfusion.org</url>        </repository></repositories>
    

    Что еще важнее, через Maven очень легко подключать сторонние серверные Java библиотеки. Например, если нам надо решить СЛАУ, просто находим соответствующую библиотеку в центральном Maven репозитории, добавляем в pom.xml.


    <dependency>        <groupId>org.apache.commons</groupId>        <artifactId>commons-math3</artifactId>        <version>3.2</version></dependency>
    

    И эту зависимость автоматически подключат и IDE, и сервера сборки.

  8. Сервера приложений и БД. Для работы с серверами БД используется JDBC, при этом все проблемы с производительностью / соответствием спецификации решаются именно родными средствами СУБД. То есть никаких пропатченных версий Postgres не требуется (которых может не быть в центральных репозиториях Docker, yum и т.п.)


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



Неуважительные по отношению к разработчикам лицензирование и брендирование


Как уже упоминалось выше, lsFusion распространяется под лицензией LGPL v3.0, которая позволяет все что угодно, кроме, разве что, создания коммерческой версии lsFusion и ее дальнейшей продажи. Соответственно, для разработчика платформа lsFusion не более чем инструмент для решения его задач, а не манна небесная, на которую он должен молиться и с которой его должны ассоциировать. Как следствие, основной ценностью в экосистеме lsFusion является не платформа как таковая, а решения на этой платформе, и что, возможно даже более важно, компетенции людей / компаний их дорабатывающих, внедряющих и поддерживающих. Почему именно такие акценты актуальны в современном ИТ-мире? Дело в том, что большинство бизнесов сейчас уже как-то автоматизировано, и, соответственно, основной вопрос заключается как раз в качестве этой автоматизации ее глубине, гибкости и соответствии существующим бизнес-процессам. А обеспечение всех этих требований требует как хорошо спроектированных модульных специализированных решений, так и умение быстро и качественно дорабатывать эти решения прямо на лету (так как, как правило, в крупных проектах внедрение сначала идет as is, а только потом to be). Соответственно грести все компании, использующие платформу, под одну гребенку, превращая их во франчей, в конечном итоге, не выгодно никому:


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

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


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


Ну и еще один важный момент. Как отмечалось выше, качество платформы критично прежде всего либо в крупных проектах (где бизнес-процессы как правило более сложные и уникальные), либо в узкоспециализированных решениях (где сам бизнес достаточно уникальный / редкий). В типовых решениях для малого и среднего бизнеса (особенно не сильно притязательных в плане IT) ценность платформы, как впрочем и самого решения, зачастую не очень высоки, поэтому сама логика рынка подсказывала сделать такие решения такими же бесплатными и открытыми (как сама платформа). Что мы собственно и сделали с решением MyCompany. Более того это решение выпускается даже под более свободной лицензией, чем сама платформа, что дает возможность сторонним разработчикам создавать на его основе свои специализированные решения, и, что еще более важно, дает возможность их продавать, тем самым зарабатывая на лицензиях.


Заключение


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


  1. В современном мире бизнес-приложений реальный экономический эффект могут дать только очень гибкие решения (быстро меняющиеся вместе с компанией), плюс, обладающие очень высокой глубиной автоматизации / кастомизации. И тут уже платформа выходит на первый план.
  2. Глобальная цифровизация всех каналов общения привела к тому, что многие, даже очень сложные, проекты разрабатываются и внедряются практически полностью онлайн (с буквально единичными очными встречами с заказчиком, более того, часто у самого заказчика все коммуникации внутри осуществляются исключительно онлайн). Как следствие, необходимость иметь физическое присутствие во всех регионах, где представлен заказчик, если не отпала, то стала гораздо менее острой. Соответственно, уже нет такой большой разницы между существованием 30 или 3000 поставщиков решения, на первый план опять-таки выходят уже другие факторы.

Ну и наконец, хотелось отметить, что это ещё не все. В процессе подготовки и обсуждения первой статьи выяснилось ещё как минимум несколько важных пунктов, где lsFusion если не лучше, то уж точно сильно отличается от 1С и других существующих на рынке ERP-платформ.


Так, среди таких пунктов можно вспомнить:

Существенный оверхед при работе с большими объектами (например документами)


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


Ограниченность пользовательских настроек


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


Невозможность одновременной работы с объектами (по аналогии например с Google docs)


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


Неудобная работа с динамическим количеством колонок


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


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


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


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

Неочевидные / неэргономичные интерфейсы для выполнения многих базовых действий


Например:


  • Настройка группировок настройка через списки, вместо интуитивных drag-drop интерфейсов.
  • Групповое изменение данных вообще не WYSIWYG операция, а находится где-то в администрировании (часть БСП как я понял).
  • Фильтрация если скажем необходимо отфильтровать все записи с текущим полем > заданного значения (закопана в Еще -> Настроить список, где необходимо найти нужную колонку и сделать еще несколько нажатий).
  • Подсчет количества записей, суммы по колонке честно говоря так и не нашел где это делается (но может просто плохо искал)

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


Невозможность прозрачной подмены представлений списков / свойств


Прозрачно заменить представление в 1С не может:


  • ни пользователь например, просто нажав соответствующую кнопку (как, например, в Odoo или lsFusion)
  • ни даже разработчик декларативно выбрав для отображения данных другой renderer, отличный от таблицы / поля, например, карту, спидометр или вообще любую js-функцию (причем так, чтобы обновлением данных продолжала заниматься сама платформа).

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


Например, если необходимо:


  • организовать табличные части в табличных частях
  • реализовать классификатор на дату
  • реализовать несколько классификаторов (с выбором их пользователем)
  • организовать цепочку статусов документов (а не просто проведен / не проведен)
  • и т.п.

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



Соответственно, в ближайшем будущем скорее всего выйдет вторая часть статьи Почему не 1С, но она с большой вероятностью сразу будет в формате сравнения как что-то делается в 1С и как это же делается в lsFusion (то есть это будет скорее вторая часть этой статьи, а не предыдущей). Благо, так как у нас в команде появились бывшие 1С-разработчики с большим опытом, писать эту статью будет существенно проще.


Подробнее..

Запускаем Rust-приложение на мобильной ОС Аврора

02.03.2021 10:09:04 | Автор: admin

Всем привет! Меня зовут Шамиль, я ведущий инженер-разработчик в КРОК. Помимо всего прочего мы в компании занимаемся ещё и разработкой мобильных приложений для операционной системы Аврора, есть даже центр компетенций по ней.

Для промышленной разработки мы, конечно же, пока используем связку C++ и QML, но однажды подсев на "ржавую" иглу Rust, я не мог не попробовать применить свой любимый язык программирования для написания мобильных приложений. В этой статье я опишу эксперимент по написанию простейшего приложения на Rust, предназначенного для запуска на мобильном устройстве под управлением вышеупомянутой ОС. Сразу оговорюсь, что легких путей я не искал эксперименты проводил на сертифицированной версии Авроры, которая добавила огонька в этот процесс. Но, как говорится, только защищённая ОС, только хардкор.

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

Готовим окружение

Итак, работа будет вестись из-под Ubuntu Linux с уже установленным Rust. В качестве подопытного планшета выступает Aquarius NS220 с сертифицированной ОС Аврора последней (на момент написания статьи) версии 3.2.2 с включённым режимом разработчика, который обеспечивает связь по SSH, а также привилегированный доступ с правами суперпользователя.

Первым делом добавим средства кросскомпилятора для архитектуры ARM, так как на целевом планшете стоит именно такой процессор.

sudo apt install -y g++-arm-linux-gnueabihfrustup target add armv7-unknown-linux-gnueabihf

В сертифицированной версии ОС Аврора не разрешается запускать неподписанные приложения. Подписывать надо проприетарной утилитой из состава Aurora Certified SDK под названием ompcert-cli, которая поддерживает на входе только пакет в формате RPM. Поэтому сразу установим замечательную утилиту cargo-rpm, которая возьмёт на себя всю рутинную работу по упаковке приложения в RPM-пакет:

cargo install cargo-rpm

Саму процедуру подписывания RPM-пакета я описывать не буду, она неплохо документирована в справочных материалах ОС Аврора.

Aurora SDK можно скачать с сайта производителя.

Часть 1. Hello. World

TL;DR Исходники проекта можно найти в репозитории на Гитхабе.

Создаем минимальный проект

Создаём пустое приложение на Rust:

cargo new aurora-rust-helloworld

Пытаемся сгенерировать .spec файл для RPM-пакета:

cargo rpm init

Получаем ошибки, что не хватает некоторых полей в Cargo.toml, добавляем их:

Cargo.toml:

[package]name = "aurora-rust-helloworld"version = "0.1.0"authors = ["Shamil Yakupov <syakupov@croc.ru>"]edition = "2018"description = "Rust example for Aurora OS"license = "MIT"

Закидываем в папку .cargo конфигурационный файл с указанием правильного линкера для компоновки исполняемого файла под архитектуру ARM:

.cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]linker = "arm-linux-gnueabihf-gcc"

Собираем RPM-пакет:

cargo rpm initcargo rpm build -v --target=armv7-unknown-linux-gnueabihf

Всё собралось, забираем RPM из папки target/armv7-unknown-linux-gnueabihf/release/rpmbuild/RPMS/armv7hl, подписываем его, копируем на планшет и пытаемся установить:

$ devel-suPassword:# pkcon install-local ./aurora-rust-helloworld-0.1.0-1.armv7hl.rpm

Получаем ошибку:

Fatal error: nothing provides libc.so.6(GLIBC_2.32) needed by aurora-rust-helloworld-0.1.0-1.armv7hl

Смотрим версию glibc на устройстве, и понимаем, что она явно ниже той, что нам требуется:

$ ldd --versionldd (GNU libc) 2.28

Что ж, тогда попробуем забрать нужные библиотеки с планшета, закинуть их в директорию lib и слинковать с ними. Для верности будем пользоваться линкером, входящим в состав Aurora SDK, который закинем в директорию bin. Для начала посмотрим, какие именно библиотеки нам нужны. Меняем содержимое .cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]rustflags = ["-C", "link-args=-L lib"]linker = "bin/armv7hl-meego-linux-gnueabi-ld"

Пробуем собрать:

cargo build --release --target=armv7-unknown-linux-gnueabihf

Получаем ошибки:

aurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lgcc_saurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lutilaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lrtaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lpthreadaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lmaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -ldlaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lcaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find -lutil

Копируем недостающие библиотеки с планшета:

mkdir -p libscp nemo@192.168.2.15:/usr/lib/libgcc_s.so ./libscp nemo@192.168.2.15:/usr/lib/libutil.so ./libscp nemo@192.168.2.15:/usr/lib/librt.so ./libscp nemo@192.168.2.15:/usr/lib/libpthread.so ./libscp nemo@192.168.2.15:/usr/lib/libm.so ./libscp nemo@192.168.2.15:/usr/lib/libdl.so ./libscp nemo@192.168.2.15:/usr/lib/libc.so ./libscp nemo@192.168.2.15:/usr/lib/libutil.so ./lib

Снова пытаемся собрать, получаем новую порцию ошибок:

aurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: skipping incompatible /lib/libc.so.6 when searching for /lib/libc.so.6aurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find /lib/libc.so.6aurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: skipping incompatible /usr/lib/libc_nonshared.a when searching for /usr/lib/libc_nonshared.aaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find /usr/lib/libc_nonshared.aaurora-rust-helloworld/bin/armv7hl-meego-linux-gnueabi-ld: cannot find /lib/ld-linux-armhf.so.3

Копируем недостающее:

scp nemo@192.168.2.15:/lib/libc.so.6 ./libscp nemo@192.168.2.15:/usr/lib/libc_nonshared.a ./libscp nemo@192.168.2.15:/lib/ld-linux-armhf.so.3 ./lib

Ещё надо подредактировать файл libc.so (который является фактически скриптом линкера), чтобы дать понять линкеру, где надо искать библиотеки:

lib/libc.so:

/* GNU ld script   Use the shared library, but some functions are only in   the static library, so try that secondarily.  */OUTPUT_FORMAT(elf32-littlearm)GROUP ( libc.so.6 libc_nonshared.a  AS_NEEDED ( ld-linux-armhf.so.3 ) )

Запускаем сборку RPM-пакета, копируем, пытаемся установить.

Здесь позволю себе небольшое лирическое отступление. Перед установкой RPM-пакета на сертифицированной версии ОС Аврора запускается RPM-валидатор утилита, которая проверяет, насколько собранный пакет удовлетворяет требованиям системы. И до тех пор, пока пакет не пройдёт валидацию, установить приложение не получится. Безопасность превыше всего.

Итак, мы видим, что валидатор выдал несколько ошибок:

вот таких
Desktop file============ERROR [/usr/share/applications/aurora-rust-helloworld.desktop] File is missing - cannot validate .desktop filePaths=====WARNING [/usr/share/aurora-rust-helloworld] Directory not foundERROR [/usr/share/applications/aurora-rust-helloworld.desktop] File not foundWARNING [/usr/share/icons/hicolor/86x86/apps/aurora-rust-helloworld.png] File not foundWARNING [/usr/share/icons/hicolor/108x108/apps/aurora-rust-helloworld.png] File not foundWARNING [/usr/share/icons/hicolor/128x128/apps/aurora-rust-helloworld.png] File not foundWARNING [/usr/share/icons/hicolor/172x172/apps/aurora-rust-helloworld.png] File not foundERROR [/usr/share/icons/hicolor/[0-9x]{5,9}/apps/aurora-rust-helloworld.png] No icons found! RPM must contain at least one icon, see: https://community.omprussia.ru/doc/software_development/guidelines/rpm_requirementsLibraries=========ERROR [/usr/bin/aurora-rust-helloworld] Cannot link to shared library: libutil.so.1Symbols=======ERROR [/usr/bin/aurora-rust-helloworld] Binary does not link to 9__libc_start_main@GLIBC_2.4.Requires========ERROR [libutil.so.1] Cannot require shared library: 'libutil.so.1'

Что ж, будем бороться с каждой ошибкой по списку.

Добавляем недостающие файлы

Добавим иконки и ярлык (файл с расширением desktop) в директорию .rpm.

.rpm/aurora-rust-helloworld.desktop:

[Desktop Entry]Type=ApplicationX-Nemo-Application-Type=silica-qt5Icon=aurora-rust-helloworldExec=aurora-rust-helloworldName=Rust Hello-World

Для того, чтобы копировать нужные файлы на этапе сборки RPM-пакета, сделаем простенький Makefile:

Makefile
.PHONY: all clean install prepare release rpmall:@cargo build --target=armv7-unknown-linux-gnueabihfclean:@rm -rvf targetinstall:@scp ./target/armv7-unknown-linux-gnueabihf/release/aurora-rust-helloworld nemo@192.168.2.15:/home/nemo/@scp ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/RPMS/armv7hl/*.rpm nemo@192.168.2.15:/home/nemo/prepare:@rustup target add armv7-unknown-linux-gnueabihf@cargo install cargo-rpmrelease:@cargo build --release --target=armv7-unknown-linux-gnueabihfrpm:@mkdir -p ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/SOURCES@cp -vf .rpm/aurora-rust-helloworld.desktop ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/SOURCES@cp -rvf .rpm/icons ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/SOURCES@cargo rpm build -v --target=armv7-unknown-linux-gnueabihf

Обновим aurora-rust-helloworld.spec:

.rpm/aurora-rust-helloworld.spec
%define __spec_install_post %{nil}%define __os_install_post %{_dbpath}/brp-compress%define debug_package %{nil}Name: aurora-rust-helloworldSummary: Rust example for Aurora OSVersion: @@VERSION@@Release: @@RELEASE@@%{?dist}License: MITGroup: Applications/SystemSource0: %{name}-%{version}.tar.gzSource1: %{name}.desktopSource2: iconsBuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root%description%{summary}%prep%setup -q%installrm -rf %{buildroot}mkdir -p %{buildroot}cp -a * %{buildroot}mkdir -p %{buildroot}%{_datadir}/applicationscp -a %{SOURCE1} %{buildroot}%{_datadir}/applicationsmkdir -p %{buildroot}%{_datadir}/icons/hicolor/86x86/appsmkdir -p %{buildroot}%{_datadir}/icons/hicolor/108x108/appsmkdir -p %{buildroot}%{_datadir}/icons/hicolor/128x128/appsmkdir -p %{buildroot}%{_datadir}/icons/hicolor/172x172/appscp -a %{SOURCE2}/86x86/%{name}.png %{buildroot}%{_datadir}/icons/hicolor/86x86/appscp -a %{SOURCE2}/108x108/%{name}.png %{buildroot}%{_datadir}/icons/hicolor/108x108/appscp -a %{SOURCE2}/128x128/%{name}.png %{buildroot}%{_datadir}/icons/hicolor/128x128/appscp -a %{SOURCE2}/172x172/%{name}.png %{buildroot}%{_datadir}/icons/hicolor/172x172/apps%cleanrm -rf %{buildroot}%files%defattr(-,root,root,-)%{_bindir}/*%{_datadir}/applications/%{name}.desktop%{_datadir}/icons/hicolor/*/apps/%{name}.png

Для сборки пакета теперь достаточно выполнить:

make rpm

Убираем зависимость от libutil.so

Я не нашёл способа, как убедить cargo при вызове команды линкера не передавать ключ -lutil. Вместо этого я решил создать скрипт-заглушку для линкера с какой-нибудь незначащей командой.

lib/libutil.so:

/* GNU ld script   Dummy script to avoid dependency on libutil.so */ASSERT(1, "Unreachable")

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

Добавляем символ __libc_start_main

Перепробовав несколько способов, остановился на том, чтобы добавить при линковке стандартный объектный файл crt1.o. Копируем его с планшета:

scp nemo@192.168.2.15:/usr/lib/crt1.o ./lib

И добавляем в команды линкера:

.cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]rustflags = ["-C", "link-args=-L lib lib/crt1.o"]linker = "bin/armv7hl-meego-linux-gnueabi-ld"

Однако при попытке сборки получаем ошибки:

undefined reference to `__libc_csu_fini'undefined reference to `__libc_csu_init'

Добавим заглушки этих функций в main.rs:

src/main.rs:

#[no_mangle]pub extern "C" fn __libc_csu_init() {}#[no_mangle]pub extern "C" fn __libc_csu_fini() {}fn main() {    println!("Hello, world!");}

Ещё один быстрый и грязный хак, зато теперь RPM-пакет проходит валидацию и устанавливается!

Момент истины близок, запускаем на планшете и получаем очередную ошибку:

$ aurora-rust-helloworld-bash: /usr/bin/aurora-rust-helloworld: /usr/lib/ld.so.1: bad ELF interpreter: No such file or directory

Смотрим зависимости:

$ ldd /usr/bin/aurora-rust-helloworldlinux-vdso.so.1 (0xbeff4000)libgcc_s.so.1 => /lib/libgcc_s.so.1 (0xa707f000)librt.so.1 => /lib/librt.so.1 (0xa7069000)libpthread.so.0 => /lib/libpthread.so.0 (0xa7042000)libm.so.6 => /lib/libm.so.6 (0xa6fc6000)libdl.so.2 => /lib/libdl.so.2 (0xa6fb3000)libc.so.6 => /lib/libc.so.6 (0xa6e95000)/usr/lib/ld.so.1 => /lib/ld-linux-armhf.so.3 (0xa70e7000)

И видим динамическую линковку с библиотекой ld-linux-armhf.so.3. Если решать в лоб, то нужно создать символическую ссылку /usr/lib/ld.so.1 /lib/ld-linux-armhf.so.3 (и это даже будет неплохо работать). Но, к сожалению, такое решение не подходит. Дело в том, что строгий RPM-валидатор не пропустит ни пред(пост)-установочные скрипты в .spec-файле, ни деплой в директорию /usr/lib. Вообще список того, что можно, приведён здесь.

Долгое и разнообразное гугление подсказало, что у линкера GCC есть нужный нам ключ (dynamic-linker), который позволяет сослаться непосредственно на нужную зависимость. Правим config.toml:

.cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]rustflags = ["-C", "link-args=-L lib lib/crt1.o --dynamic-linker /lib/ld-linux-armhf.so.3"]linker = "bin/armv7hl-meego-linux-gnueabi-ld"

Собираем RPM-пакет, подписываем, копируем на планшет, устанавливаем и с замиранием сердца запускаем:

$ aurora-rust-helloworldHello, world!

Часть 2. Запускаем приложение с GUI

TL;DR Исходники проекта можно найти в репозитории.

В Авроре всё очень сильно завязано на Qt/QML, поэтому сначала я думал использовать крейт qmetaobject. Однако в комплекте с ОС идёт библиотека Qt версии 5.6.3, а qmetaobject, судя по описанию, требует минимум Qt 5.8. И действительно, попытка сборки крейта приводит к ошибкам.

Поэтому я пошёл по пути точечных заимствований из исходников qmetaobject благо, лицензия позволяет.

Для начала копируем проект, созданный в предыдущей части, и переименовываем его в aurora-rust-gui.

Приступаем

Чтобы не утомлять читателя, сразу скажу, что для сборки понадобится скопировать с планшета ещё множество разных библиотек:

вот таких
scp nemo@192.168.2.15:/usr/lib/libstdc++.so ./libscp nemo@192.168.2.15:/usr/lib/libQt5Core.so.5 ./lib/libQt5Core.soscp nemo@192.168.2.15:/usr/lib/libQt5Gui.so.5 ./lib/libQt5Gui.soscp nemo@192.168.2.15:/usr/lib/libQt5Qml.so.5 ./lib/libQt5Qml.soscp nemo@192.168.2.15:/usr/lib/libQt5Quick.so.5 ./lib/libQt5Quick.soscp nemo@192.168.2.15:/usr/lib/libGLESv2.so.2 ./libscp nemo@192.168.2.15:/usr/lib/libpng16.so.16 ./libscp nemo@192.168.2.15:/usr/lib/libz.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libicui18n.so.63 ./libscp nemo@192.168.2.15:/usr/lib/libicuuc.so.63 ./libscp nemo@192.168.2.15:/usr/lib/libpcre16.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libglib-2.0.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libsystemd.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libQt5Network.so.5 ./libscp nemo@192.168.2.15:/lib/libresolv.so.2 ./libscp nemo@192.168.2.15:/usr/lib/libhybris-common.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libicudata.so.63 ./libscp nemo@192.168.2.15:/usr/lib/libpcre.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libselinux.so.1 ./libscp nemo@192.168.2.15:/usr/lib/liblzma.so.5 ./libscp nemo@192.168.2.15:/usr/lib/libgcrypt.so.11 ./libscp nemo@192.168.2.15:/usr/lib/libgpg-error.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libcap.so.2 ./libscp nemo@192.168.2.15:/usr/lib/libsailfishapp.so.1 ./lib/libsailfishapp.soscp nemo@192.168.2.15:/usr/lib/libmdeclarativecache5.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libmlite5.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libdconf.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libgobject-2.0.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libQt5DBus.so.5 ./libscp nemo@192.168.2.15:/usr/lib/libdconf.so.1 ./libscp nemo@192.168.2.15:/usr/lib/libffi.so.6 ./libscp nemo@192.168.2.15:/usr/lib/libdbus-1.so.3 ./libscp nemo@192.168.2.15:/usr/lib/libgio-2.0.so.0 ./libscp nemo@192.168.2.15:/usr/lib/libgmodule-2.0.so.0 ./lib

А еще копируем заголовочные файлы, которые идут в составе Aurora SDK:

  • AuroraOS/mersdk/targets/AuroraOS-3.2.2.21-cert-armv7hl/usr/include/qt5 include/qt5

  • AuroraOS/mersdk/targets/AuroraOS-3.2.2.21-cert-armv7hl/usr/include/sailfishapp include/sailfishapp

  • AuroraOS/mersdk/targets/AuroraOS-3.2.2.21-cert-armv7hl/usr/include/GLES3 include/GLES3

  • AuroraOS/mersdk/targets/AuroraOS-3.2.2.21-cert-armv7hl/usr/include/KHR include/KHR

Для сборки проекта напишем скрипт build.rs и укажем его в Cargo.toml.

build.rs:

fn main() {    let include_path = "include";    let qt_include_path = "include/qt5";    let sailfish_include_path = "include/sailfishapp";    let library_path = "lib";    let mut config = cpp_build::Config::new();    config        .include(include_path)        .include(qt_include_path)        .include(sailfish_include_path)        .opt_level(2)        .flag("-std=gnu++1y")        .flag("-mfloat-abi=hard")        .flag("-mfpu=neon")        .flag("-mthumb")        .build("src/main.rs");    println!("cargo:rustc-link-search={}", library_path);    println!("cargo:rustc-link-lib=sailfishapp");    println!("cargo:rustc-link-lib=Qt5Gui");    println!("cargo:rustc-link-lib=Qt5Core");    println!("cargo:rustc-link-lib=Qt5Quick");    println!("cargo:rustc-link-lib=Qt5Qml");}

Cargo.toml:

[package]# ...build = "build.rs"[dependencies]cpp = "0.5.6"[build-dependencies]cpp_build = "0.5.6"#...

Теперь возьмёмся за само приложение. За создание инстанса приложения у нас будет отвечать структура SailfishApp по аналогии с приложением для Авроры, написанном на C++.

src/main.rs:

#[macro_use]extern crate cpp;mod qbytearray;mod qstring;mod qurl;mod sailfishapp;use sailfishapp::SailfishApp;#[no_mangle]pub extern "C" fn __libc_csu_init() {}#[no_mangle]pub extern "C" fn __libc_csu_fini() {}fn main() {    let mut app = SailfishApp::new();    app.set_source("main.qml".into());    app.show();    app.exec();}

SailfishApp это по сути обвязка (биндинги) к соответствующему классу на C++. Берём за образец структуру QmlEngine из крейта qmetaobject.

src/sailfishapp.rs
use crate::qstring::QString;cpp! {{    #include <sailfishapp.h>    #include <QtCore/QDebug>    #include <QtGui/QGuiApplication>    #include <QtQuick/QQuickView>    #include <QtQml/QQmlEngine>    #include <memory>    struct SailfishAppHolder {        std::unique_ptr<QGuiApplication> app;        std::unique_ptr<QQuickView> view;        SailfishAppHolder() {            qDebug() << "SailfishAppHolder::SailfishAppHolder()";            int argc = 1;            char *argv[] = { "aurora-rust-gui" };            app.reset(SailfishApp::application(argc, argv));            view.reset(SailfishApp::createView());            view->engine()->addImportPath("/usr/share/aurora-rust-gui/qml");        }    };}}cpp_class!(    pub unsafe struct SailfishApp as "SailfishAppHolder");impl SailfishApp {    /// Creates a new SailfishApp.    pub fn new() -> Self {        cpp!(unsafe [] -> SailfishApp as "SailfishAppHolder" {            qDebug() << "SailfishApp::new()";            return SailfishAppHolder();        })    }    /// Sets the main QML (see QQuickView::setSource for details).    pub fn set_source(&mut self, url: QString) {        cpp!(unsafe [self as "SailfishAppHolder *", url as "QString"] {            const auto full_url = QString("/usr/share/aurora-rust-gui/qml/%1").arg(url);            qDebug() << "SailfishApp::set_source()" << full_url;            self->view->setSource(full_url);        });    }    /// Shows the main view.    pub fn show(&self) {        cpp!(unsafe [self as "SailfishAppHolder *"] {            qDebug() << "SailfishApp::show()";            self->view->showFullScreen();        })    }    /// Launches the application.    pub fn exec(&self) {        cpp!(unsafe [self as "SailfishAppHolder *"] {            qDebug() << "SailfishApp::exec()";            self->app->exec();        })    }}

Биндинги для используемых классов QByteArray, QString, QUrl копируем из того же qmetaobject и расфасовываемым по отдельным файлам. Здесь приводить их не буду, если что, исходники можно посмотреть в репозитории на GitHub.

Немного скорректируем заголовочный файл sailfishapp.h, чтобы он искал заголовочные файлы Qt в правильных местах:

include/sailfishapp/sailfishapp.h:

// ...#ifdef QT_QML_DEBUG#include <QtQuick>#endif#include <QtCore/QtGlobal>  // Было `#include <QtGlobal>`#include <QtCore/QUrl>      // Было `#include <QUrl>`class QGuiApplication;class QQuickView;class QString;// ...

Осталось только добавить файлы QML и положить их в дистрибутив RPM.

все здесь

qml/main.qml:

import QtQuick 2.6import Sailfish.Silica 1.0ApplicationWindow {    cover: Qt.resolvedUrl("cover.qml")    initialPage: Page {        allowedOrientations: Orientation.LandscapeMask        Label {            anchors.centerIn: parent            text: "Hello, Aurora!"        }    }}

qml/cover.qml:

import QtQuick 2.6import Sailfish.Silica 1.0CoverBackground {    Rectangle {        id: background        anchors.fill: parent        color: "blue"        Label {            id: label            anchors.centerIn: parent            text: "Rust GUI"            color: "white"        }    }    CoverActionList {        id: coverAction        CoverAction {            iconSource: "image://theme/icon-cover-cancel"            onTriggered: Qt.quit()        }    }}

.rpm/aurora-rust-gui.spec:

# ...Source3: qml# ...%install# ...mkdir -p %{buildroot}%{_datadir}/%{name}cp -ra %{SOURCE3} %{buildroot}%{_datadir}/%{name}/qml%cleanrm -rf %{buildroot}%files# ...%{_datadir}/%{name}/qml

Makefile:

# ...rpm:# ...@cp -rvf qml ./target/armv7-unknown-linux-gnueabihf/release/rpmbuild/SOURCES# ...

Собираем:

make cleanmake releasemake rpm

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

$ devel-suPassword:# pkcon install-local ./aurora-rust-gui-0.1.0-1.armv7hl.rpmInstalling filesTesting changesFinishedInstalling filesStartingResolving dependenciesInstalling packagesDownloading packagesInstalling packagesFinishedDownloaded  aurora-rust-gui-0.1.0-1.armv7hl (PK_TMP_DIR)         Rust GUI example for Aurora OSInstalled   aurora-rust-gui-0.1.0-1.armv7hl (PK_TMP_DIR)            Rust GUI example for Aurora OS# exit$ aurora-rust-gui[D] __cpp_closure_14219197022164792912_impl:33 - SailfishApp::new()[D] SailfishAppHolder::SailfishAppHolder:15 - SailfishAppHolder::SailfishAppHolder()[D] unknown:0 - Using Wayland-EGLlibrary "libpq_cust_base.so" not found[D] __cpp_closure_16802020016530731597:42 - SailfishApp::set_source() "/usr/share/aurora-rust-gui/qml/main.qml"[W] unknown:0 - Could not find any zN.M subdirs![W] unknown:0 - Theme dir "/usr/share/themes/sailfish-default/meegotouch/z1.0/" does not exist[W] unknown:0 - Theme dir "/usr/share/themes/sailfish-default/meegotouch/" does not exist[D] onCompleted:432 - Warning: specifying an object instance for initialPage is sub-optimal - prefer to use a Component[D] __cpp_closure_12585295123509486988:50 - SailfishApp::show()[D] __cpp_closure_15029454612933909268:59 - SailfishApp::exec()

Вот так выглядит наше приложение с разных ракурсов:

 Рабочий стол с ярлыком Рабочий стол с ярлыком Главное окно приложенияГлавное окно приложения Панель задач Панель задач

Последние штрихи

Приложение отлично стартует из командной строки при подключении по SSH, однако никак не реагирует при попытке запуска с помощью ярлыка. Путём некоторых экспериментов удалось установить, что для этого надо экспортировать символ main (RPM-валидатор выдавал предупреждение на этот счёт, но некритичное).

Серия проб и ошибок показала, что надо добавить ещё один ключ линкера: -export-dynamic.

.cargo/config.toml:

[target.armv7-unknown-linux-gnueabihf]rustflags = ["-C", "link-args=-L lib lib/crt1.o -rpath lib --dynamic-linker /lib/ld-linux-armhf.so.3 -export-dynamic"]linker = "bin/armv7hl-meego-linux-gnueabi-ld"

После этого всё работает так, как и ожидается.

Заключение

Понятно, что до того, как использовать Rust в проде, ещё надо решить немало вопросов. Как минимум, я предвижу сложности с дополнительными зависимостями при подключении новых крейтов, извечные танцы с бубном вокруг сегфолтов при FFI-вызовах, увязывание систем владения Qt и Rust. Некоторые интересные подробности можно почерпнуть из статьи от автора qmetaobject-rs. Наверняка, время от времени будут всплывать и другие проблемы.

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

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

Буду рад вопросам и замечаниям в комментариях. И ставьте лайк, подписывайтесь на канал :-)

Подробнее..

Создание бизнес-приложений бесплатные вебинары марта

02.03.2021 10:09:04 | Автор: admin

Привет, Хабр! Сегодня делимся уже традиционной подборкой наших бесплатных мероприятий (вебинаров) по создания бизнес-приложений. В этом месяце эвентов будет два, один из которых будет проходить впервые. Ну и без бесплатных возможностей сдать наши сертификационные экзамены как всегда не обошлось. Заглядывайте под кат!

1. Основы Microsoft Power Platform

10 марта, на английском с субтитрами на русском.

Виртуальный учебный день Microsoft Power Platform: Основы охватит все, что вам нужно знать о создании инновационных бизнес-решений и управлении ими, чтобы ваша организация была готова ко всему, что будет в будущем. Вы узнаете, как связать свои данные для анализа эффективности бизнеса в режиме реального времени, действовать на основе данных и аналитики с помощью специально созданных приложений, а также автоматизировать рабочие процессы для улучшения работы людей как удаленно, так и в офисе. Этот тренинг научит вас создавать инновационные, перспективные решения для развивающейся бизнес-среды.

Во время этого учебного мероприятия вы узнаете, как:

  • Подготовиться к сертификационному экзамену по основам Microsoft Power Platform

  • Создавать инновационные бизнес-решения и управлять ими с помощью Power Platform

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

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

После прохождения этого бесплатного обучения вы сможете бесплатно сдать сертификационный экзамен по основам Microsoft Power Platform.

Подробности и регистрация.

2. Как активировать цифровые продажи (NEW)

31 марта, на английском с субтитрами на русском.

Сегодняшний мир, который становится все более цифровизованным, требует гибкой стратегии электронной коммерции. Виртуальный учебный день Microsoft Dynamics 365: Activate Digital Selling предназначен для ознакомления с продуктами для маркетинга, продаж и коммерции, которые работают в унисон для создания удобного опыта цифровых покупок. Ваши новые навыки и обучение могут быть применены сразу же, чтобы способствовать конструктивному взаимодействию на всех этапах пути к покупке.

Во время этого учебного мероприятия вы узнаете, как:

  • Соответствовать ожиданиям клиентов - от маркетинга до продаж и электронной коммерции

  • Использовать ИИ с помощью надстройки Dynamics 365 Sales Insights и Dynamics 365 Customer Insights

  • Получить навыки и знания, необходимые для сертификации Dynamics 365 в Microsoft Learn

Подробности и регистрация.

Подробнее..

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

02.03.2021 12:21:43 | Автор: admin


Познакомьтесь с Бобом


Боб чрезвычайно амбициозный и активный разработчик.

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

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

Другими словами, ему придётся делать меньше того, что он любит в обмен на то, чем он заниматься не умеет управлять другими.

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

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

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

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

Это явление называется принципом Питера:

В иерархической системе каждый индивидуум имеет тенденцию подняться до уровня своей некомпетентности. [] Сливки поднимаются кверху, пока не прокиснут.

Хотя Лоуренс Питер сформулировал эту идею в своём бестселлере как сатиру, она оказалась безусловно истинной: человека будут повышать до тех пор, пока он не достигнет своего уровня некомпетентности. Для разработчика эта ступенька может быть мидлом, сениором, техлидом, директором и так вплоть до CTO.

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

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

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

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

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

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

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

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

Творческая некомпетентность: способ быть профессионалом


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

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

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

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

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

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

Если вы думаете, что знаете всё, то ничего не знаете. Если вы думаете, что не знаете ничего, то кое-что знаете. Джейс О'Нил

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

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



На правах рекламы


Закажите сервер и сразу начинайте работать! Создание VDS любой конфигурации в течение минуты, в том числе серверов для хранения большого объёма данных до 4000 ГБ. Эпичненько :)

Подробнее..

Многопоточность на низком уровне

02.03.2021 14:06:25 | Автор: admin

Очень часто при обсуждении многопоточности на платформе .NET говорят о таких вещах, как детали реализации механизма async/await, Task Asynchronous Pattern, deadlock, а также разбирают System.Threading. Все эти вещи можно назвать высокоуровневыми (относительно темы хабрапоста). Но что же происходит на уровне железа и ядра системы (в нашем случае Windows Kernel)?


На конференции DotNext 2016 Moscow Гаэл Фретёр, основатель и главный инженер компании PostSharp, рассказал о том, как в .NET реализована многопоточность на уровне железа и взаимодействия с ядром операционной системы. Несмотря на то, что прошло уже пять лет, мы считаем, что никогда не поздно поделиться хардкорным докладом. Гаэл представил нам хорошую базу по работе процессора и атомным примитивам.



Вот репозиторий с примерами из доклада. А под катом перевод доклада и видео. Далее повествование будет от лица спикера.



Сегодня я хотел бы поговорить о многопоточности на самом низком уровне. В докладе не будет ничего принципиально нового, всё это уже было в версии .NET 1.0. Однако, я потратил несколько недель на то, чтобы изучить спецификацию AMD64 и структурировать информацию.


У этого доклада две цели:


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

Сначала мы разберем, что происходит на уровне железа: CPU и уровни памяти, всё, что с ними происходит. Далее перейдем к ядру Windows. А после этого обсудим на первый взгляд простые вещи, такие как lock. Скорее всего, вы и сами знаете, зачем нужно это ключевое слово. Я же собираюсь объяснить, как оно реализовано.


Микроархитектура


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



Это что-то вроде Intel Core i7 своего времени. Как вы видите, за 70 лет была проделана большая работа.


К чему это все? Обычно предполагают, что байт памяти представлен где-нибудь в ячейке оперативной памяти. Но на самом деле всё иначе: он может быть представлен в нескольких местах одновременно. Например, в шести ядрах, может быть в кэшах L1 и L2, в кэше L3, наконец, может быть в оперативной памяти. Важно упомянуть: в этой архитектуре процессора есть общий кэш L3, и между ними есть Uncore. Этот компонент отвечает за координацию ядер. Также есть QPI, который отвечает за разницу между CPU. Это относится только к мультипроцессорным системам.



Почему нас беспокоит архитектура процессора? По той же причине, по которой нам нужны несколько разных уровней кэшей. У каждого уровня своя задержка. Обращаю внимание на то, что цикл CPU на этой спецификации занимает 0,3 наносекунды и сравнивается с задержкой кэшей L1, L2, L3 и DRAM. Важно заметить, что извлечение конструкции из DRAM требует около 70 циклов процессора. Если вы хотите рассчитать эти значения для своего процессора, то могу посоветовать хороший бенчмарк SiSoft Sandra.



Теперь давайте посмотрим на это более подробно. Для простоты предположим, что здесь у меня есть двухпроцессорная система. У каждого из процессоров есть четыре ядра и L2-кэш, а L1-кэш отсутствует. Также есть L3-кэш и общая оперативная память.



Хорошая аналогия для понимания многопоточных систем распределенные системы.


Если вы знаете, что такое очередь сообщений (message queue) или шина данных (message bus), вам будет легче понять многопоточность: похожие вещи есть внутри вашего процессора. Там есть очередь сообщений, шина сообщений, буферы из-за всего этого у нас возникают проблемы с синхронизацией. При взаимодействии ядер информация распределена между L2 и L3-кэшами и памятью. Поскольку шины сообщений асинхронны, порядок обработки данных не гарантирован. Кроме кэширования процессор буферизует данные и может решить оптимизировать операции и отправлять сообщения на шину в другом порядке.


Переупорядочивание памяти


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



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



Итак, давайте рассмотрим одну архитектуру. Пусть это будет x86 или AMD64, здесь не так много Y. Это означает, что она дает нам много гарантий в разных вопросах. На самом деле, x86 изначально не разрабатывался под многоядерные процессоры, поэтому когда Intel начали добавлять новые ядра, они решили сохранить ту же семантику, что раньше.


По таблице видим, что процессор с этой архитектурой может переупорядочить операции сохранить (store), после операций загрузить (load). Когда вы хотите сохранить значение, достаточно отправить сообщение. Ждать ответа не обязательно. А когда вы загружаете данные, вам нужно отправить запрос и ждать ответа. Для этой операции процессор может применить различные оптимизации.


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


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


На Intel x86 вы можете быть уверены, что переменные A и B идут строго последовательно, то есть если удалось загрузить B, то переменная A точно инициализирована. А если всё это выполняется на ARM, то у нас нет никаких гарантий, в каком порядке все это будет выполняться и как процессор проведет оптимизацию.



Барьеры памяти



Помимо оптимизации процессора, существует ещё оптимизация компилятора и среды выполнения. В контексте данной статьи под компилятором и средой я буду понимать JIT и CLR. Среда может кэшировать значения в регистрах процессора, переупорядочить операции и объединять операции записи (coalesce writes).


Например, для цикла for CLR может решить заранее рассчитать значение и вынести локальную переменную за пределы цикла. Во избежание этого, конечно же, можно использовать ключевое слово volatile, но бывают и более сложные случаи.


Иногда бывает нужно, чтобы весь код был точно выполнен до какой-либо определенной инструкции, и для этого используются барьеры памяти. Барьер памяти это инструкция, которая реализуется процессором. В .NET она доступна с помощью вызова Thread.MemoryBarrier(). И что же он делает? Вызов этого метода гарантирует, что все операции перед этим методом точно завершились.


Давайте снова вернемся к программе с двумя переменными. Предположим, что эти процессоры выполняют команды сохранить A и загрузить B. Если между этими двумя командами находится барьер памяти, то процессор сначала отправит запрос сохранить B, подождет, пока она не выполнится. Барьеры памяти дают возможность поставить что-то вроде чекпоинта или коммита, как в базе данных.



Есть еще одна причина использовать барьеры памяти. Если вы пишете код на 32-битной машине с использованием long, DateTime или struct, то атомарность выполнения операций может нарушаться. Это значит, что даже когда в коде записана одна инструкция, на самом деле могут произойти две операции вместо одной, причем они могут выполняться в разное время.



Атомарные операции


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


Когда мы говорим, что операция атомарна, мы имеем в виду, что она не может быть прервана. Это означает, что во время выполнения операции не может произойти переключение потока, операция не может частично завершиться. О разнице между атомарностью, эксклюзивностью и изменением порядка выполнения вы можете узнать из доклада Саши Гольдштейшна Модели памяти C++ и CLR. Более подробно об атомарности рассказывал Карлен szKarlen Симонян в докладе Атомарные операции и примитивы в .NET.

Interlocked содержит такие операции, как инкрементирование (Increment), обмен (Exchange) и обмен через сравнение (CompareExchange).


Я собираюсь разобрать не очень часто используемую операцию CompareExchange, также известную как CAS. Она изменяет значение поля на новое только в том случае, если текущее поле равно какому-либо определенному значению. Эта операция необходима для всех параллельных структур.


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



Не стоит забывать про стоимость выполнения Interlocked-операций. Стоимость инкрементирования в кэше L1 примерно равна удвоенной задержке кэша L1. Вполне быстро! Но стоимость использования Interlocked-инкремента составляет уже целых 5,5 наносекунд, даже если это происходит в единственном потоке. Этот показатель близок к показателю задержки кэша L3. А если у вас два потока обращаются к одной кэш-линии, то стоимость удваивается: ядрам приходится ждать друг друга.



Проанализируем случай, когда двум ядрам приходится ждать друг друга. Для этого используем Intel Vtune Amplifier профайлер со счетчиками производительности процессора. Он подсчитывает частоту цикла процессора и частоту процессора, нужную для выполнения одной инструкции. На картинке ниже показатель подсвечен красным. Профайлер выдает дополнительную информацию, почему соответствующее значение это плохо. Если несколько ядер обращаются к одной кэш-линии, то возникнут проблемы с производительностью. Это особенно критично для приложений, которые обрабатывают большое количество транзакций в секунду.



Многозадачность


На уровне процессора нет таких понятий, как поток и процесс: всё, что мы называем многопоточностью, предоставляет операционная система. А Wait(), на самом деле, не может заставить процессор ждать. Эта команда переводит его в режим пониженного энергопотребления (lower energy state).


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


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



Можно использовать compareExchange на примере очень простой реализации ConcurrentStack.


Весь код (финальная версия):


internal sealed class MyConcurrentStack<T>{   private volatile Node head;   public void Push(T value)   {       SpinWait wait = new SpinWait();       Node node = new Node {Value = value};       for ( ;; )       {           Node localHead = this.head;           node.Next = localHead;           if (Interlocked.CompareExchange(ref this.head, node, localHead)                == localHead)               return;           wait.SpinOnce();       }   }   public bool TryPop(out T value)   {       SpinWait wait = new SpinWait();       for ( ;; )       {           Node localHead = this.head;           if (localHead == null )           {               value = default(T);               return false;           }           if (Interlocked.CompareExchange(ref this.head,               localHead.Next, localHead) == localHead )           {               value = localHead.Value;               return true;           }           wait.SpinOnce();       }   }   #region Nested type: Node   private sealed class Node   {       public Node Next;       public T Value;   }   #endregion}

Класс Node (внутри класса MyConcurrentStack<T>) хранит значение и содержит ссылку на следующий элемент.


Давайте сначала посмотрим на неблокирующую реализацию стека:


private sealed class Node{   public Node Next;   public T Value;}

Посмотрим на неблокирующую реализацию стека, здесь мы не используем ключевое слово lock и wait-операции:


private volatile Node head;public void Push(T value){// первым делом создаем элемент стека, тут все просто   Node node = new Node {Value = value};   for ( ;; )   {// записываем ссылку на текущую верхушку стека в локальную//переменную       Node localHead = this.head;// для нового элемента указываем ссылку на следующий элемент,// которым будет являться текущая вершина стека       node.Next = localHead;// меняем верхушку стека (this.head) на новый элемент (node),// если верхушка стека уже не была изменена       if (Interlocked.CompareExchange(           ref this.head, node, localHead ) == localHead )           return;   }}

Зачем здесь нужен условный оператор? Может случиться так, что два потока пытаются запушить новое значение в стек. Эти два потока видят одно и то же поле head. Первый поток начал вставлять новое значение до того, как это начал делать второй поток. После того как первый поток вставил значение, актуальная ссылка на headесть только у этого потока. У второго потока будет ссылка на элемент, который теперь считается следующим после head, то есть head.Next. Такая ситуация показывает, насколько важно бывает иметь такие атомарные операции, как CompareExchange.


Этот способ решения задачи основан на неблокирующей структуре данных. В методе TryPop() мы используем тот же прием:


public bool TryPop(out T value)   {       for ( ;; )       {           Node localHead = this.head;           if (localHead == null)           {               value = default(T);               return false;           }           if (Interlocked.CompareExchange(ref this.head, localHead.Next, localHead)               == localHead )           {               value = localHead.Value;               return true;           }       }   }

Берем head и заменяем её следующим узлом, если, конечно, она уже не была изменена.


В время теста MyConcurentStack участвовало два ядра. Одно ядро выполняло операцию Push(), другое операцию Pop(), ожидание отсутствовало в течение 6 миллионов операций по обмену сообщениями между двумя ядрами.


У этой структуры данных есть два недостатка:


  1. необходимость очистки
  2. необходимость выделения памяти для элементов коллекции

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


В результате все работает гораздо быстрее: 9 миллионов транзакций в секунду.


public class TestMyConcurrentStack : TestCollectionBase{   private readonly MyConcurrentStack<int> stack =        new MyConcurrentStack<int>();   protected override void AddItems(int count)   {       for (int i = 0; i < count; i++)       {           this.stack.Push(i);       }   }   protected override void ConsumeItems(int count)   {       SpinWait spinWait = new SpinWait();       int value;       for (int i = 0; i < count; )       {           if (this.stack.TryPop(out value))           {               i++;               spinWait.Reset();           }           else           {               spinWait.SpinOnce();           }       }   }}


Операционная система (ядро Windows)



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



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


Давайте докажу это. На этом компьютере у меня четыре ядра с гипертредингом, то есть восемь логических процессоров. Всего в системе 2384 потока.Так как логических процессоров всего 8, то получается, что все 2376 потоков в данный момент ожидают, пока до них дойдет очередь выполнения. В 99,9% случаев основное занятие потоков это ожидание.



Одна из функций ядра Windows состоит в том, чтобы заставлять потоки ждать. У внутреннего ядра Windows есть граф зависимостей между потоками и объектами-диспетчерами (dispatcher objects) Среди этих объектов могут быть таймеры, объекты, семафоры и события.


В какой-то момент некоторые зависимости исчезают, и соответствующие потоки переходят из состояния ожидания (wait) в состояние готовности (ready). Потоки в состоянии готовности отправляются в очередь ожидания, а после этого они уходят на разные ядра и начинают выполняться.


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


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


Когда вы запускаете поток ожидания, фактически создается зависимость между потоком и объектом диспетчера.



Стоимость диспетчеризации уровня ядра


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



Посмотрите на график, посмотрите на стоимость инкрементирования или доступа к кэшу L1, а затем на стоимость присваивания объекта диспетчера ядра. Две наносекунды и 295 наносекунд огромная разница! А создание объекта диспетчера вообще занимает 2093 наносекунды.


При разработке важно писать код, который не будут создавать объекты диспетчера лишний раз, иначе можно ожидать большую потерю производительности. На уровне .NET у нас есть Thread.Sleep, который использует внутренний таймер. Thread.Yield возвращает выполняющийся поток обратно в очередь ожидания потоков. Thread.Join блокирует вызывающий поток. Но сейчас я хочу более подробно рассказать про Thread.SpinWait.



SpinWait


Представьте, что у вас есть у вас есть два процессора, и на нулевом вы выполняете присваивание A = 1, потом устанавливаете барьер памяти, а затем снова выполняете присваивание B = 1.


На первом процессоре вы получаете А, равное 1, а затем вы ждете, пока B не присвоят значение 1. Казалось бы, такие операции должны быстро выполниться и в кэше L1, и даже в кэше L3. Здесь загвоздка в том, что нулевой процессор может превентивно прерваться, и между операциями из строк 1 и 3 может пройти несколько миллисекунд. SpinWait() способен решать такие проблемы.



При использовании Thread.SpinWait() не происходит переключение контекста потока, текущий поток не передает управление планировщику задач Windows. Вместо этого запускается холостой цикл, который при каждой итерации возвращает поток обратно в очередь ожидания (прямо как Thread.Yield()), передавая управление потоку с таким же приоритетом, но не фоновому потоку.


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


Монитор и ключевое слово lock


Теперь давайте вернемся к ключевому слову lock. Скорее всего, вы знаете, что это просто синтаксический сахар для try/catch Monitor.Enter/Monitor.Exit.


  1. Выполняется Interlocked-операция: каждый объект в .NET имеет хедер, который говорит, на каком потоке он находится и для чего он заблокирован.


  2. Идёт SpinWait, если операция завершится неудачей.


  3. CRL создает kernel event, если выполнение SpinWait не дало ожидаемого результата. После нескольких миллисекунд ожидания создается еще один kernel event, но только в другом потоке.



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


Структуры данных из пространства имен System.Collections.Concurrent


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


Concurrent Stack


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



Concurrent Queue


В другой структуре данных ConcurrentQueue потребители конкурируют за разные узлы. Это увеличивает пропускную способность в два раза: производители и потребители конкурируют между собой только в своей группе.



Concurrent Bag


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



Заключение


  1. На аппаратном уровне есть только операции Interlocked и барьеры памяти.
  2. На уровне ядра Windows вводится такая абстракция, как поток. Основное предназначение потока это ожидание и хранение набора зависимостей.
  3. Также мы рассмотрели некоторые коллекции из стандартной библиотеки.

Если доклад показался вам достаточно хардкорным, загляните на сайт конференции Dotnext 2021 Piter, которая пройдёт с 20 по 23 апреля. Основными темами станут настоящее и будущее платформы .NET, оптимизация производительности, внутреннее устройство платформ, архитектура и паттерны проектирования, нетривиальные задачи и best practices. На сайте начинают появляться первые доклады, со временем список будет пополняться.
Подробнее..

Код ревью как быть хорошим автором

02.03.2021 16:05:07 | Автор: admin

Привет! Меня зовут Сергей Загурский, я работаю в Joom в команде инфраструктуры. В своей практике ревьюера кода я регулярно сталкиваюсь с тем, что автор не понимает, что ревьюер не является волшебным чёрным ящиком, в который можно закинуть любые изменения и получить по ним обратную связь. Ревьюер, как и автор, будучи человеком, обладает рядом слабостей. И автор должен (если, конечно, он заинтересован в качественном ревью), помочь ревьюеру настолько, насколько это возможно.

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

Зачем мы делаем ревью кода

Вы наверняка и без меня это знаете. Поэтому я не буду углубляться в подробности и растекаться мыслью по древу. Расскажу о самых основах.

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

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

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

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

Дальше мы сосредоточимся на самой фундаментальной составляющей процесса ревью на понимании кода.

Как ревьюер делает ревью

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

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

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

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

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

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

Как помочь ревьюеру провести качественное ревью

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

Перед тем, как превратить свои изменения в Pull Request, следует разбить их на логические куски, если в этом есть необходимость. Комфортный объём ревью заканчивается примерно на 500 строках кода изменений. Допустимый примерно на 1000 строках. Всё, что за пределами 1000 строк, должно быть разбито на более мелкие Pull Requestы.

Если вы, как и я, не разбиваете свои изменения на куски комфортного размера заранее, то это придётся проделать перед отправкой ваших изменений на ревью. Отмазка, что на это нужно потратить время, не катит. Принимая решение об отправке на ревью 1000+ строк кода, вы, фактически, оцениваете своё время дороже времени ревьюера. По нашим правилам у ревьюера всегда есть право потребовать разбить изменения на более мелкие куски. Мы всегда просим коллег относиться с пониманием, если он этим воспользуется. С опытом становится проще строить свою работу так, чтобы у вас не появлялись гигантские Pull Requestы, в которых ничего нельзя отделить.

Отдельно стоит упомянуть изменения, внесённые в код с помощью авторефакторинга или sedа. Объём таких изменений может быть очень большим. Наличие автоматических изменений вместе с осмысленными изменениями усложняет ревью. Всегда отделяйте авторефакторинги в отдельные Pull Requestы, если их объём сопоставим с объёмом нового кода, написанного вами.

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

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

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

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

В целом, я считаю допустимым потратить порядка 10% от времени, затраченного на написание кода, на подготовку к ревью. Это время автора, которое мы обменяем на экономию времени ревьюера, и на улучшение качества ревью. Следует помнить, что ревьюеру на качественное ревью легко может потребоваться и 20%, и 50% от времени, затраченного автором на написание кода.

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

Дальше начинается жизненный цикл Pull Requestа. Ревьюер должен либо одобрить его, либо попросить внести изменения. Чтобы упростить работу ревьюера, автору стоит добавить комментарий к каждому запрошенному изменению. Это вполне может быть краткое OK или Исправил, если ничего другого не требуется. Убедитесь, что вы понимаете, что просит ревьюер и что вам понятна его аргументация. Не стоит безоговорочно принимать любые запросы на внесение изменений, возводя тем самым ревьюера в околобожественный ранг. Ревью это обоюдный процесс. Если ревьюеру что-то не понятно, то он спрашивает об этом автора, и наоборот. В случае, если автор не очень опытен, ревьюеру следует приложить особенные усилия к описанию запросов на изменения, чтобы воспользоваться возможностью поделиться с автором своим опытом. Бывают и спорные моменты, когда аргументация недостаточно сильная, чтобы склонить обе стороны к единой точке зрения. С учётом того, что автор находится в более уязвимой позиции, считаю, что при прочих равных преимущество должно оставаться за автором.

Не вносите изменений в свой Pull Request, не относящихся к тому, что попросил вас ревьюер. Это крайне сбивает с толку. Также следует воздержаться от rebase и подобных действий.

Получили одобрение от ревьюера? Отлично, ещё одно качественно написанное и оформленное изменение было добавлено в проект!

Подробнее..

Категории

Последние комментарии

© 2006-2021, personeltest.ru